免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

pg-sql2 的 sql.identifier() 完全指南:安全构建动态 SQL 标识符

pg-sql2 的 sql.identifier() 完全指南:安全构建动态 SQL 标识符 后端API网关【免费下载链接】crystal Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址https://gitcode.com/gh_mirrors/cry/crystal点击查看免费下载sql.identifier()是 Graphile 生态中 PostgreSQL 查询构建库 pg-sql2 提供的核心 API用于把表名、列名、schema 名等数据库对象名安全地转义为合法的 SQL 标识符从根本上杜绝动态拼接标识符引发的 SQL 注入。本文以 utils/website/pg-sql2/api/sql-identifier.md 文档为主体结合 utils/pg-sql2/src/index.ts 源码实现与 utils/pg-sql2/tests/general.test.ts 测试用例系统讲解其语法、全部用法场景、Symbol 别名机制以及底层实现原理。读完本文你将能够在自己的动态 SQL 场景中安全、正确地使用标识符并理解 pg-sql2 如何将标识符问题与值绑定问题彻底分离。为什么需要专门处理 SQL 标识符在构建动态 SQL 时开发者经常需要把运行期才确定的表名、列名拼进查询字符串例如const tableName getUserInput(); // 用户输入不可信 const query SELECT * FROM ${tableName};一旦tableName是users; DROP TABLE users; --之类的恶意输入整个查询就会被注入攻击者控制的 SQL。即使使用参数化查询占位符$1也只能保护值字符串、数字等字面量无法用于表名、列名这类标识符——PostgreSQL 不允许把占位符当作标识符使用。sql.identifier()正是为此而生它接收一个或多个名称将每个名称分别安全转义后以点号连接如schema.table.column返回一个可嵌入其他 SQL 片段的SQL片段对象。这样动态表名/列名就可以安全地参与查询构建同时完全避免注入风险。语法与参数sql.identifier在 utils/pg-sql2/src/index.ts 中定义类型签名如下sql.identifier(name: string | symbol): SQL sql.identifier(name1: string | symbol, name2: string | symbol, ...): SQL参数name— 字符串或 Symbol表示一个标识符名称表名、列名、schema 名、函数名等。可传多个名称用于构建点号分隔的限定标识符qualified identifier。返回值返回一个SQL片段表示转义后的标识符可直接嵌入sql模板字面量或其他 SQL 表达式中最终通过sql.compile()得到可执行的text与values。参数约束源码实现细节至少传入一个参数否则抛出错误[pg-sql2] Invalid call to sql.identifier() - you must pass at least one argument.src/index.ts。每个参数必须是字符串或 Symbol传入其他类型如数字、对象会抛出错误例如[pg-sql2] Invalid argument to sql.identifier - argument 1 (0-indexed) should be a string or a symbol...src/index.ts。这意味着你无法直接用数字构造标识符需先转成字符串。基本用法单个标识符表名import { sql } from pg-sql2; // 表名 const tableName users; const query sqlSELECT * FROM ${sql.identifier(tableName)}; console.log(sql.compile(query).text); // SELECT * FROM users列名// 列名 const columnName user_name; const query sqlSELECT ${sql.identifier(columnName)} FROM users; console.log(sql.compile(query).text); // SELECT user_name FROM users注意编译结果中的标识符总是被双引号包裹。测试 general.test.ts 也验证了这一点sql.identifier(foo)对应节点为{ t: foo }。限定标识符多参数点号连接当传入多个参数时每个参数会被单独转义再用点号连接从而生成schema.table、table.column、schema.table.column等限定形式import { sql } from pg-sql2; // schema.table const schema public; const table users; const column name; const query sqlSELECT ${sql.identifier(column)} FROM ${sql.identifier(schema, table)}; console.log(sql.compile(query).text); // SELECT * FROM public.users// table.column const query sqlSELECT ${sql.identifier(table, column)} FROM users; console.log(sql.compile(query).text); // SELECT users.name FROM users// schema.table.column const query sqlCOMMENT ON COLUMN ${sql.identifier(schema, table, column)} IS ; console.log(sql.compile(query).text); // COMMENT ON COLUMN public.users.name IS ;测试用例同样覆盖了多参数场景sql.identifier(foo, bar, bz)被编译为foo.bar.bzgeneral.test.ts其中内部的被正确转义为。使用 Symbol 生成唯一别名sql.identifier()还接受 Symbol 参数用于生成唯一且安全的别名。这在同一张表需要被引用多次自连接、重复子查询时尤其有用——你不需要手工维护不会撞名的别名库会替你保证唯一性import { sql } from pg-sql2; const worker sql.identifier(Symbol(worker)); const boss sql.identifier(Symbol(boss)); const query sql SELECT ${worker}.name, ${boss}.salary/${worker}.salary as boss_multiplier FROM employees AS ${worker} INNER JOIN employees AS ${boss} ON ${worker}.manager_id ${boss}.id ; console.log(sql.compile(query).text); /* SELECT __worker__.name, __boss__.salary/__worker__.salary as boss_multiplier FROM employees AS __worker__ INNER JOIN employees AS __boss__ ON __worker__.manager_id __boss__.id */给 Symbol 起有意义的名字上述示例即使使用无描述的空 Symbol 也能工作只是编译出的别名会退化为__local_0__、__local_1__这类形式可读性较差const worker sql.identifier(Symbol()); const boss sql.identifier(Symbol()); // FROM employees AS __local_0__ // INNER JOIN employees AS __local_1__因此强烈建议在 Symbol 的 description 中填写语义化名称如Symbol(worker)既方便调试也让生成的 SQL 更易读。Symbol 别名的底层规则从源码看Symbol 别名机制包含以下关键点src/index.ts、src/index.ts名称整理mangleSymbol 的 description 会经mangleName()处理只保留[0-9a-z_]字符长度限制在 50 个字符以内去掉首尾与连续下划线空描述回退为local。这样生成的别名无需转义即可安全使用与字符串标识符总是加引号的策略形成互补。编译期分配字符串标识符在sql.identifier()调用时就完成转义而 Symbol 标识符要等到sql.compile()阶段才真正生成名字。编译时用symbolToIdentifier这个 Map 记录每个 Symbol 对应的别名并配合descCounter计数器保证同一个 Symbol 在单次编译中始终映射到同一个别名即使该片段出现多次而不同 Symbol 即使 description 相同也会得到不同别名。命名格式第一个实例命名为__name__后续同名实例依次为__name_2、__name_3……测试 general.test.ts 精确验证了这些行为多个Symbol(foo)依次编译为__foo__、__foo_2、__foo_3Symbol()编译为__local__而同一 Symbol 引用多次时别名保持一致__bar__出现三次。这一机制让同一张表在查询中出现多次的自连接场景变得完全无脑安全也正是 Grafast 的 dataplan-pg 在底层大量使用sql.identifier(this.symbol)生成表别名的原因参见 grafast/dataplan-pg/src/steps/pgSelect.ts 等处。特殊字符与保留字处理包含空格、引号或其他特殊字符的标识符同样会被安全转义因为整个名称总是被双引号包裹内部的双引号会被加倍转义import { sql } from pg-sql2; // 含空格的标识符被安全转义 const query sqlSELECT * FROM ${sql.identifier(user data)}; console.log(sql.compile(query).text); // SELECT * FROM user data// 含特殊字符的标识符也被安全转义 const query sqlSELECT * FROM ${sql.identifier(bz)}; console.log(sql.compile(query).text); // SELECT * FROM bz转义逻辑集中在escapeSqlIdentifier()src/index.ts把双引号和空字符\0统一替换为双引号对再用双引号包裹整个名称。这与 PostgreSQL 服务端处理带引号标识符的规则一致源码注释说明其移植自 PostgreSQL 的fe-exec.c并经正则优化比原实现快约 11 倍。保留字处理由于字符串标识符一律加双引号输出PostgreSQL 保留字如select、from、order等作为标识符使用时会被当作普通名称而非关键字天然规避了保留字冲突问题——这正是文档 Notes 中Reserved SQL keywords are handled correctly through escaping的底层原因。深入底层identifier 的节点模型与编译流程pg-sql2 的所有 API 都返回统一的SQL节点对象sql.identifier也不例外。理解节点模型有助于排查问题字符串参数在调用时立即通过escapeSqlIdentifier()转义生成RAW节点{ [$$type]: RAW, t: foo }多个字符串会被拼入同一段原始文本之间以.连接。Symbol 参数生成IDENTIFIER节点{ [$$type]: IDENTIFIER, s: symbol, n: mangle 后的名字 }见 src/index.ts。混合传参如sql.identifier(public, Symbol(tbl))会生成一个包含RAW节点与IDENTIFIER节点的QUERY节点组合。到了sql.compile()阶段遍历节点树时src/index.tsRAW节点原样输出IDENTIFIER节点先去symbolToIdentifierMap 中查当前 Symbol 已分配的别名没有则调用makeIdentifierForSymbol()现场分配并缓存由于别名只由安全字符组成直接输出而无需再次转义。编译结果text与values即可直接交给pg等驱动执行返回对象中还带有一个[$$symbolToIdentifier]的 Map见 src/index.ts用于查询 Symbol 到最终别名的映射关系。注意事项与边界单个编译内一致性、跨编译非确定性Symbol 标识符生成的字符串表示在同一次编译的查询内始终一致同一 Symbol 多次出现会复用同一别名但如果同一个sql.identifier(Symbol(...))片段被复用到多个查询中分别编译各次编译得到的别名可能不同因为 Symbol 每次都是新对象且计数从 1 重新开始。文档 Notes 中对此有明确说明。字符串标识符总会加引号即使sql.identifier(users)这种完全合法的名称输出也是users而非users。这在功能上无碍PostgreSQL 中带引号与不带引号的标识符指向同一对象前提是名字不含大写/特殊字符但若你追求生成 SQL 的简洁性需要注意这一行为。相比之下Symbol 生成的别名因为字符安全而不加引号。务必与sql.value()分工标识符表名/列名/别名用sql.identifier()数据值字面量用sql.value()或sql.literal()。把用户输入当标识符直接拼进sql模板、或把标识符当值传入占位符都是典型的错误用法。不要用sql.raw()替代sql.raw()是绕过一切保护的逃生门直接输出未经处理的动态文本会彻底破坏 pg-sql2 的注入防护体系几乎任何合法场景都能用sql.identifier()与sql.value()的组合完成详见 utils/website/pg-sql2/api/index.md 中的警告。在真实项目中的典型应用pg-sql2 是 Graphile Crystal 全家桶Grafast、PostGraphile、pg-introspection 等的 SQL 构建基石sql.identifier()在其中被广泛使用。仅以 grafast/dataplan-pg 为例为每个数据步骤生成表别名this.alias sql.identifier(this.symbol)pgInsertSingle.ts、pgDeleteSingle.ts动态拼装 schema/表/列限定名sqlType: sql.identifier(...type.split(.))codecs.ts在过滤条件中安全引用属性列sql${this.parent.alias}.${sql.identifier(attr)} ...pgManyFilter.ts。这些场景的共同特点是名称来自运行时数据数据库 introspection 结果、用户传入的排序/过滤字段等无法在编译期写死但又绝不能信任其内容——正是sql.identifier()的设计目标。相关 API 一览sql.identifier()通常与以下 API 配合使用完整清单见 utils/website/pg-sql2/api/index.mdsql...— 模板字面量函数构建查询主体sql.compile() — 将 SQL 片段编译为text与valuessql.value() — 用占位符嵌入用户值防注入sql.literal() — 安全时直接内联简单值否则回退到sql.value()sql.join() — 用分隔符拼接多个片段常与sql.identifier组合构建动态列清单sql.symbolAlias() 与 sql.replaceSymbol() — 片段合并场景下的 Symbol 别名对齐与替换例如在JOIN中把两个子查询的同一 Symbol 视为相同。总结sql.identifier()用字符串即转义、Symbol 即别名的双轨设计配合编译期的符号映射机制为动态 SQL 中最容易出问题的标识符环节提供了既安全又灵活的标准答案。凡是需要把运行期名称引入查询的地方都应优先想到它。赞分享后端API网关【免费下载链接】crystal Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址https://gitcode.com/gh_mirrors/cry/crystal点击查看免费下载相关推荐IP-Adapter 轻量图像生成5 步从零到出图效果糊了调什么IP Adapter 轻量图像生成5 步从零到出图效果糊了调什么 手里有一张角色设定图想换个场景、换个姿态但不想重画整张图IP Adapter 就是干后端API网关Crystal的pg-sql2完全指南构建注入免疫的动态SQL告别拼接字符串Crystal的pg sql2完全指南构建注入免疫的动态SQL告别拼接字符串 在 Graphile Crystal 这个开源 Monorepo 中 pg后端API网关Apache Spark SQL IDENTIFIER 子句完全指南参数化标识符与动态 SQL 对象引用Apache Spark SQL IDENTIFIER 子句完全指南参数化标识符与动态 SQL 对象引用 本文全面讲解 Spark SQL 的 IDENTIF大数据数据分析批处理流处理机器学习图计算上一篇使用 aws_codebuild_fleet 数据源查询 AWS CodeBuild 计算舰队信息下一篇5分钟实现Proxmox VE脚本自动化GitLab CI/CD集成指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表