数据迁移脚本虽然不像Web接口那样直接面向终端用户,但其潜在风险同样不容忽视——只要脚本拼接了未经验证的外部输入,例如配置文件中的参数、命令行参数、CSV文件中的字段值,甚至是环境变量,就依然存在被注入攻击的可能。正确的防御策略是:将SQL结构与数据严格分离,表名和列名必须通过白名单校验机制,错误日志也需进行脱敏处理。

或许你会认为迁移脚本属于内部工具,不易遭受攻击。然而现实情况是,攻击者只要获取到低权限账号(例如Jenkins构建账号),便能够通过污染配置信息间接操控迁移逻辑。因此,切莫以为“仅在内网运行”就能高枕无忧。
迁移脚本中哪些环节会拼接 SQL?
常见的风险点远不止在 INSERT INTO 语句中直接拼接数据那么简单:
- 利用
sys.argv或os.environ读取表名、库名以及 WHERE 条件(例如--target-table users_2024) - 从 CSV 或 Excel 文件中提取字段值后,未经类型校验或引号转义便直接插入到
VALUES (...)中 - 依据配置文件生成动态
CREATE TABLE语句,其中表名或列名直接来源于 JSON 配置项 - 执行
mysqldump或pg_dump后,通过 shell 脚本拼接mysql -e "USE $DB_NAME; SOURCE ..."命令
这些输入源虽然不同于用户提交的表单数据,但同样不可轻信。一旦攻击者能够控制部署环境变量或篡改迁移配置文件,就完全有可能触发注入攻击。
为什么不能仅凭“脚本只在内网运行”来推卸责任?
内网环境绝不等于绝对安全。实际运维中容易踩中的陷阱包括:
docker run -e DB_NAME='test; DROP TABLE users; --'—— 环境变量未经清理就直接用于 SQL 拼接- 在 CI/CD 流水线中,分支名或标签名被直接用作数据库后缀(例如
prod_v2对应users_prod_v2),而分支名往往可由 PR 提交者控制 - 运维人员手动执行迁移时,复制粘贴的命令中可能包含隐藏字符或换行符,导致语句意外截断或注释失效
一旦攻击者获得低权限账号(比如 Jenkins 构建账号),就能通过污染配置来间接操控迁移逻辑的执行流程。
如何编写安全的迁移脚本?优先采用参数化查询,杜绝字符串拼接
不同语言编写的迁移脚本具体实现各异,但核心原则始终如一:SQL 结构与数据必须彻底分离。
- Python +
psycopg2:使用cursor.execute("SELECT * FROM %s WHERE id = %s", (table_name, user_id))❌ 这是错误的——%s占位符不能用于表名;正确的做法是先对table_name进行白名单校验,再通过sql.SQL("SELECT * FROM {}").format(sql.Identifier(table_name))来构建查询 - Bash +
mysql:避免使用mysql -e "INSERT INTO $TABLE ..."这种拼接方式;推荐改用mysql --defaults-file=cred.cnf -e "INSERT INTO users VALUES (?, ?)",并配合mysqlimport或LOAD DATA INFILE(需确保文件路径可控) - Go +
database/sql:所有用户输入都必须通过db.Query("SELECT * FROM users WHERE name = ?", name)方式处理,绝对避免使用fmt.Sprintf来拼接 SQL 语句
对于表名、列名等无法参数化的结构部分,必须严格执行白名单校验或正则表达式验证(例如 ^[a-zA-Z][a-zA-Z0-9_]{1,63}$),同时拒绝任何包含点号、美元符、分号或反引号的输入内容。
最容易被忽视的细节:错误信息与日志记录
迁移操作失败时,切勿将原始 SQL 语句和数据库报错信息原封不动地输出到 stdout 或日志文件中。否则可能带来以下风险:
- 报错内容可能泄露表结构信息(例如
Unknown column 'password_hash' in 'field list') - 攻击者可以通过构造异常输入,观察系统响应差异来探测字段名称或业务逻辑
- 日志文件若未做脱敏处理,一旦泄露就等于为攻击者留下了清晰的攻击痕迹地图
真正应该记录的日志内容包括:操作人员、操作时间、目标数据库、影响行数以及是否执行了回滚操作;除非出于调试需要,否则 SQL 语句本身不应记录在日志中。
