必须校验动态拼接的表名,因绑定变量无法保护SQL结构,仅防值注入;直接拼接用户输入会导致SQL注入,DBMS_ASSERT.SQL_OBJECT_NAME是唯一推荐的白名单校验方式。
动态拼接表名必须校验,否则直接执行 execute immediate 就等于给 sql 注入开了后门。
为什么不能直接拼接表名
绑定变量(USING)只保护「值」,不保护 SQL 结构。比如 SELECT * FROM :table_name 是非法语法,Oracle 不允许把对象名当绑定变量传。一旦用 'SELECT * FROM ' || p_table 拼接,而 p_table 来自用户输入或配置表,攻击者就能传入 'employees; DROP TABLE users --' 这类恶意字符串。
-
DBMS_ASSERT.SQL_OBJECT_NAME是唯一推荐的校验方式:合法对象名原样返回,非法则抛ORA-44003 - 白名单校验更安全但维护成本高,适合固定范围(如只允许
'log_202605'、'tmp_data'等预设前缀) - 别用
REPLACE或正则简单过滤——绕过手段太多,根本不可靠
正确拼接并执行带表名的动态SQL
校验只是第一步,后续拼接、执行、错误处理要一气呵成。常见场景是按日期分表查询或清空:
- 拼接时用
DBMS_ASSERT.SQL_OBJECT_NAME(p_table)包裹所有动态对象名,包括表、视图、字段、索引名 - DML 语句(
INSERT/UPDATE/DELETE)必须用USING绑定值,禁止拼接值 - DDL(如
TRUNCATE TABLE)自动提交,执行前需确认事务上下文是否允许隐式提交 - 示例:安全清空分表
DECLARE
v_table VARCHAR2(100) := 'log_' || TO_CHAR(SYSDATE, 'yyyymmdd');
BEGIN
EXECUTE IMMEDIATE 'TRUNCATE TABLE ' || DBMS_ASSERT.SQL_OBJECT_NAME(v_table);
EXCEPTION
WHEN OTHERS THEN
RAISE_APPLICATION_ERROR(-20001, 'Truncate failed on ' || v_table || ': ' || SQLERRM);
END;多行查询别硬扛 EXECUTE IMMEDIATE
EXECUTE IMMEDIATE 只支持单行 INTO 或批量 BULK COLLECT INTO。若表结构不确定、行数不可控或需逐行处理(比如调用另一个过程),硬写 BULK COLLECT 容易 PGA 内存溢出或逻辑断裂。
- 改用
OPEN FOR+SYS_REFCURSOR,复用静态游标习惯,结果集不全加载进内存 - 游标变量可作为输出参数返回给调用方,由上层决定怎么取数据
- 字段名也得校验:若
p_col是动态传入的,同样要过DBMS_ASSERT.SIMPLE_SQL_NAME - 示例:动态查某列
DECLARE
v_cursor SYS_REFCURSOR;
v_sql VARCHAR2(500);
v_col VARCHAR2(30) := DBMS_ASSERT.SIMPLE_SQL_NAME('status');
v_table VARCHAR2(30) := DBMS_ASSERT.SQL_OBJECT_NAME('orders_202605');
v_val VARCHAR2(50);
BEGIN
v_sql := 'SELECT ' || v_col || ' FROM ' || v_table || ' WHERE ROWNUM = 1';
OPEN v_cursor FOR v_sql;
FETCH v_cursor INTO v_val;
CLOSE v_cursor;
END;DDL 自动提交会破坏事务一致性
这是最容易被忽略的坑:在同一个存储过程中,先执行 INSERT,再用 EXECUTE IMMEDIATE 执行 CREATE INDEX,前面的 INSERT 就会立刻提交,哪怕你没写 COMMIT。
- DDL 执行即提交,无法回滚 —— 即使加了
SAVEPOINT也没用 - 如果业务要求「全成功或全失败」,就绝不能在事务中混用 DDL 和 DML
- 替代方案:把 DDL 操作拆到独立过程,由调度层控制执行时机;或改用
DBMS_METADATA导出定义,人工审核后再执行 - 临时表(
CREATE GLOBAL TEMPORARY TABLE)例外:它不触发隐式提交
真正难的不是写对一行 EXECUTE IMMEDIATE,而是整条链路上每个拼接点、每个输入源、每次提交边界都保持防御意识——尤其当表名来自配置表或外部接口时,少一次 DBMS_ASSERT 调用,就多一分风险。


















