环境
| 项 |
值 |
| 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 被归入"任何开关状态下都不会开放"的永久禁止层。
对照证据
- 字面量被剔除、裸词被命中——同一工具下:
SELECT LENGTHB('ABC_TRUNCATE_XYZ') AS r FROM DUAL; -- 正常返回 16(字面量已剔除)
SELECT 1 AS r FROM DUAL WHERE 'x' = 'x' AND 1 = TRUNCATE_OFFSET; -- 任意含裸词 TRUNCATE 的子句都会被拦
- 被拦语句本身在数据库侧合法且只读——去掉 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)与数据库本身把关。
环境
npx -y mcp-server-dmdb@latest,stdio)DM_ALLOW_WRITE/DM_ALLOW_DDL均关闭)dm_query(read 通道)复现步骤
向
dm_query发送一条纯只读 SELECT,其中 LISTAGG 使用溢出截断子句(最小复现,任何 DM8 实例可跑):真实场景版(把长列表聚合成逗号串,靠 ON OVERFLOW TRUNCATE 兜底避免超长报错):
实际结果
客户端守卫直接拒绝,语句未发往数据库:
期望结果
该语句是 SELECT 聚合查询,不含任何写操作,应进入后续语句类型判定并正常下发数据库执行:
ON OVERFLOW TRUNCATE语法,返回聚合结果(超长时在 32767 字节处截断);根因分析
dist/db/sql-guard.js的第 3 层守卫:先剔除注释、把字符串字面量/带引号标识符替换为占位符得到 skeleton,再用词边界正则对 skeleton 做ALWAYS_DENIED扫描:ON OVERFLOW TRUNCATE是聚合函数 LISTAGG 的溢出处理子句(Oracle 12c+ 语法,DM8 新版亦支持),其中的TRUNCATE是子句关键字,与TRUNCATE TABLEDDL 毫无关系。守卫只认词、不辨语法,于是纯 SELECT 被归入"任何开关状态下都不会开放"的永久禁止层。对照证据
[-6108] 字符串截断即该子句所兜底的正是这个 32767 字节上限;守卫拦截它等于禁用了 LISTAGG 在超长场景下的标准解法。
建议修复
在
ALWAYS_DENIED扫描之前,把聚合溢出子句从 skeleton 中剥离(skeleton 已做字面量占位,短语级剥离即可覆盖ON OVERFLOW TRUNCATE ''、ON OVERFLOW TRUNCATE '...' WITH COUNT等变体):这样
TRUNCATE TABLEDDL 仍被永久拦截,而聚合子句交由语句类型判定(SELECT)与数据库本身把关。