Skip to content

sql-guard 误报:纯 SELECT 中 LISTAGG 的 ON OVERFLOW TRUNCATE 子句被 ALWAYS_DENIED 的 TRUNCATE 规则永久拦截 #1

Description

@ZLKF2015

环境

项 值
mcp-server-dmdb 1.0.5(npx -y mcp-server-dmdb@latest,stdio)
Node.js v22.22.0(npx 11.3.0)
数据库 DM Database Server 64 V8 / 8.4 / 企业版 / DB Version: 0x7000d / 03134284368-20251126-303585-20149
连接模式 纯只读(DM_ALLOW_WRITE / DM_ALLOW_DDL 均关闭)
调用工具 dm_query(read 通道)

复现步骤

向 dm_query 发送一条纯只读 SELECT,其中 LISTAGG 使用溢出截断子句(最小复现,任何 DM8 实例可跑):

SELECT LISTAGG('a', ',') WITHIN GROUP (ORDER BY level) ON OVERFLOW TRUNCATE '' AS r
FROM DUAL CONNECT BY level <= 10;

真实场景版(把长列表聚合成逗号串,靠 ON OVERFLOW TRUNCATE 兜底避免超长报错):

SELECT LENGTHB(LISTAGG(s, ',') WITHIN GROUP (ORDER BY s) ON OVERFLOW TRUNCATE '') AS r
FROM (SELECT RPAD('x', 100, 'x') AS s FROM DUAL CONNECT BY level <= 340) t;

实际结果

客户端守卫直接拒绝,语句未发往数据库:

Error: 错误: 语句包含被永久禁止的操作: TRUNCATE。
该操作在任何开关状态下都不会开放。

期望结果

该语句是 SELECT 聚合查询,不含任何写操作,应进入后续语句类型判定并正常下发数据库执行:

  • 若当前 DM8 版本支持 ON OVERFLOW TRUNCATE 语法,返回聚合结果(超长时在 32767 字节处截断);
  • 若版本不支持该语法,也应返回数据库的语法错误,由使用者判断,而非客户端"永久禁止"。

根因分析

dist/db/sql-guard.js 的第 3 层守卫:先剔除注释、把字符串字面量/带引号标识符替换为占位符得到 skeleton,再用词边界正则对 skeleton 做 ALWAYS_DENIED 扫描:

const word = (keyword) => new RegExp(`\\b${keyword}\\b`, 'i');

/** Denied in every channel, regardless of configuration. */
const ALWAYS_DENIED = [
    [word('SHUTDOWN'), 'SHUTDOWN'],
    [/\bALTER\s+(DATABASE|SYSTEM|SESSION)\b/i, 'ALTER DATABASE/SYSTEM/SESSION'],
    [/\bDROP\s+(DATABASE|USER|SCHEMA|TABLESPACE|INSTANCE|ROLE|LINK)\b/i, 'DROP ...'],
    [word('TRUNCATE'), 'TRUNCATE'],   // ← 裸词匹配,无上下文
    ...
];

ON OVERFLOW TRUNCATE 是聚合函数 LISTAGG 的溢出处理子句(Oracle 12c+ 语法,DM8 新版亦支持),其中的 TRUNCATE 是子句关键字,与 TRUNCATE TABLE DDL 毫无关系。守卫只认词、不辨语法,于是纯 SELECT 被归入"任何开关状态下都不会开放"的永久禁止层。

对照证据

  1. 字面量被剔除、裸词被命中——同一工具下:
SELECT LENGTHB('ABC_TRUNCATE_XYZ') AS r FROM DUAL;   -- 正常返回 16(字面量已剔除)
SELECT 1 AS r FROM DUAL WHERE 'x' = 'x' AND 1 = TRUNCATE_OFFSET; -- 任意含裸词 TRUNCATE 的子句都会被拦
  1. 被拦语句本身在数据库侧合法且只读——去掉 overflow 子句后同款聚合可正常下发,并顺带实测了 DM8 LISTAGG 的长度边界:
聚合结果长度 结果
32767 字节 ✅ 正常返回
32768 字节 ❌ [-6108] 字符串截断

即该子句所兜底的正是这个 32767 字节上限;守卫拦截它等于禁用了 LISTAGG 在超长场景下的标准解法。

建议修复

在 ALWAYS_DENIED 扫描之前,把聚合溢出子句从 skeleton 中剥离(skeleton 已做字面量占位,短语级剥离即可覆盖 ON OVERFLOW TRUNCATE ''、ON OVERFLOW TRUNCATE '...' WITH COUNT 等变体):

const neutralizedSkeleton = skeleton.replace(/\bON\s+OVERFLOW\s+(TRUNCATE|ERROR)\b/gi, 'ON OVERFLOW REMOVED');
const always = firstHit(neutralizedSkeleton, ALWAYS_DENIED);

这样 TRUNCATE TABLE DDL 仍被永久拦截,而聚合子句交由语句类型判定(SELECT)与数据库本身把关。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions