Javascript is required
踩坑避坑发布于 2026-07-28审校于 2026-08-086 分钟阅读

JSON 转 SQL:PostgreSQL 参数化查询与字面量转义边界

把 JSON 写入数据库和生成一份离线 INSERT 文本,是两个不同的任务。应用程序执行 SQL 时,应让驱动绑定参数;只有确实需要导出文本时,才需要按指定方言生成字面量。本文以 PostgreSQL 为例,专门讨论单引号、NULL、布尔值和有限数值,不讨论嵌套 JSON 展平或数据库建表推断。

JSON to SQLPostgreSQLParameterized QuerySQL Literal

一、问题概述:字符串拼接为什么会破坏 SQL

JSON 字符串可以包含单引号,例如 O'Reilly。把它直接插入 SQL 单引号包裹的文本中,会提前结束字面量,后续字符就不再属于值。若这段 SQL 来自用户输入并被执行,字符串拼接还会让输入影响 SQL 语法;这不是 JSON.parse 的问题,而是 SQL 代码与数据没有分离。

二、最小复现:PostgreSQL 中直接拼接不是参数化

下面的字符串仅用于展示错误边界,不应发送给数据库。PostgreSQL 参数占位符使用 $1$2,值数组由驱动负责编码。

const user = { name: "O'Reilly", active: true };
const unsafe = "INSERT INTO users (name, active) VALUES ('" + user.name + "', " + user.active + ")";
console.log(unsafe);
// INSERT INTO users (name, active) VALUES ('O'Reilly', true)

const text = "INSERT INTO users (name, active) VALUES ($1, $2)";
const values = [user.name, user.active];
// PostgreSQL driver: await client.query(text, values)

三、根因:SQL 语法和数据值必须由不同层处理

参数化查询把 SQL 模板作为代码,把 values 作为数据,驱动会按 PostgreSQL 协议传输。单纯做字符串字面量的引号替换,只能讨论某一方言的导出规则,不能处理标识符、关键字、类型转换或动态 SQL 结构,也不能替代参数绑定。

四、推荐方案:写库使用参数化,导出才生成字面量

在线写库时固定 SQL 结构并绑定参数;不要让 JSON 的键名成为未经白名单验证的列名。离线导出可使用受限的 PostgreSQL literal encoder,并明确导出的文件只能交给相同方言的解析器。若需求包含动态列、表名或排序字段,应使用白名单映射,而不是对名称做字符串转义。

五、完整代码:参数化写库与 PostgreSQL 离线字面量

以下 TypeScript 的 runQuery 是驱动适配函数,示例不伪造数据库连接。toPgLiteral 只用于生成离线文本;它拒绝非有限数值和反斜杠文本,遇到这类内容应交给驱动或明确配置过的导出工具。

type Query = (text: string, values: readonly unknown[]) => Promise<unknown>;

async function insertUser(query: Query, user: { name: string; active: boolean | null }) {
  const text = "INSERT INTO users (name, active) VALUES ($1, $2)";
  return query(text, [user.name, user.active]);
}

function toPgLiteral(value: unknown): string {
  if (value === null) return "NULL";
  if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
  if (typeof value === "number") {
    if (!Number.isFinite(value)) throw new TypeError("PostgreSQL numeric literal must be finite");
    return String(value);
  }
  const text = typeof value === "string" ? value : JSON.stringify(value);
  if (text === undefined) throw new TypeError("Value is not JSON-serializable");
  if (text.includes("\\")) throw new TypeError("Backslash requires explicit PostgreSQL literal settings");
  return "'" + text.replaceAll("'", "''") + "'";
}

console.log(toPgLiteral("O'Reilly")); // 'O''Reilly'
console.log(toPgLiteral(null)); // NULL
// Use toPgLiteral only for an offline PostgreSQL script, never as a replacement for query parameters.

六、常见错误方案

把所有值都包在单引号中会把 NULL 变成字符串 NULL,也会改变布尔值和数值的语义。只替换单引号并声称支持 MySQL、PostgreSQL 和 SQLite,会忽略各方言的反斜杠、扩展语法和参数协议。把用户提供的列名直接拼入 SQL,即使值已参数化,仍可能改变 SQL 结构。

七、边界条件:undefined、JSON 值与方言

JSON 没有 undefined;输入转换层应先决定缺失字段是省略列还是写入 NULL。NaN 和 Infinity 不是标准 JSON 数字,也不应悄悄写成字符串。数组/对象写入 PostgreSQL jsonb 需要明确列类型和编码策略;本文的离线函数只保证生成一个字符串字面量,不保证目标列类型。

八、如何验证安全边界

先用参数化接口测试包含单引号、反斜杠、换行、NULL、布尔值和 Unicode 的输入,确认数据库收到的值与输入一致。再对离线文本做字符串级断言:O'Reilly 应出现 O''Reilly,NULL 不应出现引号。测试必须连接明确的 PostgreSQL 方言,不能把一份导出文本拿到另一种数据库执行。

九、FAQ

问:把单引号替换成两个单引号能防注入吗?答:它只属于特定 SQL 方言的字面量规则,应用写库仍应使用参数化。

问:参数能绑定表名吗?答:通常不能;表名和列名要用固定白名单映射后拼入 SQL。

问:为什么 null'null' 不一样?答:前者是 SQL NULL,后者是四个字符组成的字符串。

十、总结

PostgreSQL 应用写库的首选是固定 SQL 加 $1 参数绑定。单引号双写只适合明确方言下的离线字面量导出,不能处理动态标识符,也不能替代参数化。验证时覆盖引号、NULL、布尔值、数值和 Unicode,并确认导入目标确实使用同一方言。

来源与延伸阅读

技术审校所依据的规范与权威参考资料。

相关文章

继续阅读

可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。

打开关联工具