传奇NPC脚本中@@InPutString非法字符过滤实战:从输入拦截到数据落盘

来源: 作者: 点击:
一、引擎内置过滤机制与触发链路

@@InPutStringX 弹框输入后,客户端把字符串发回服务端,引擎先做第一层校验:命中 M2 过滤列表 → 直接跳转 [@IsInFilterList](Hero端)或 [@MsgFilter](3K端),不写入 S 变量、不执行后续脚本。这一层是硬编码在引擎里的,脚本层面无法绕过,但过滤列表的内容完全由服务端维护。

过滤列表位置:M2控制器 → 查看 → 列表信息 → 其他设置 → 过滤字符(部分版本存为 GuildRankNameFilter.txt 或 FilterChar.txt)。每行一个词条,支持纯文本匹配。玩家输入包含任意词条即触发跳转。

二、脚本层二次校验:长度、字符集、格式

内置过滤只管"有没有命中词条",不管长度超限、纯数字、特殊符号组合等问题。实际脚本必须在回调段里加 #if 条件判断:

[@CallInPutString10]
if

EQUAL S10
act

MESSAGEBOX 输入不能为空
GOTO @main
elsesay

if

LARGE $STRLEN(S10) 20
act

MESSAGEBOX 输入长度不能超过20个字符
GOTO @main
elsesay

; 继续正常逻辑
act

SENDMSG 6 输入内容:S10

说明:部分引擎支持 $STRLEN() 函数取字符串长度,不支持的用自定义计数或限制输入源(客户端输入框本身有字节上限)。

三、逐字符扫描检测特殊符号

对于需要严格限制字符集的场景(如行会名只允许中文+字母+数字),用循环或多次 CHECKCONTAINS 判断:

if

CHECKCONTAINS S10 ~
act

MESSAGEBOX 不能包含~符号
GOTO @main
elsesay

if

CHECKCONTAINS S10 `
act

MESSAGEBOX 不能包含`符号
GOTO @main
elsesay

if

CHECKCONTAINS S10 |
act

MESSAGEBOX 不能包含|符号
GOTO @main
elsesay

if

CHECKCONTAINS S10 <
act

MESSAGEBOX 不能包含<符号
GOTO @main
elsesay

if

CHECKCONTAINS S10 >
act

MESSAGEBOX 不能包含>符号
GOTO @main
elsesay

if

CHECKCONTAINS S10 &
act

MESSAGEBOX 不能包含&符号
GOTO @main
elsesay

; 全部通过才执行
act

GOTO @process

需要屏蔽的字符清单(按危害程度排序):
• |:部分版本脚本解析器把 | 当命令分隔符,注入后可执行额外指令

• < >:脚本标签语法符号,输入含 < 可能被误解析为标签跳转

• ~:变量展开符号,某些引擎里 ~S10 会展开为全局变量

• &:条件连接符,可能干扰 #if 解析

• ' " :SQL注入场景(如果后端用文本数据库且未转义)

• \r \n:换行注入,破坏脚本块结构

四、数字型输入的双重保险

即使弹的是 @@InPutString,玩家也可能输入纯数字。如果需要数字,回调段里用 EQUAL/NOTEQUAL + 范围判断:

[@CallInPutString11]
if

EQUAL S11 0
act

MESSAGEBOX 不能为0
GOTO @main
elsesay

if

SMALL N11 1
act

MESSAGEBOX 不能小于1
GOTO @main
elsesay

if

LARGE N11 9999
act

MESSAGEBOX 不能超过9999
GOTO @main
elsesay

act

; S11 已确认为合法数字字符串,N11 为对应数值
SENDMSG 6 确认数值:N11

注意:@@InPutString 的结果在 S 变量里,但引擎同时会尝试解析为数字存入同名 N 变量(如 S11 → N11)。如果输入含非数字字符,N11 为 0,可以用这个特性做"纯数字校验"——N11==0 且 S11 不为 "0" 说明含字母。

五、数据库落盘前的转义处理

输入最终要写入文本文件或数据库时,必须做转义:

1. 写文本文件(SAVEVAR):
变量内容含换行符会破坏文件格式。在写入前用 REPLACE 函数清除或替换危险字符:
CALCVAR GLOBAL Z1 = $REPLACE(S10, "|", "")
CALCVAR GLOBAL Z1 = $REPLACE(Z1, "<", "")
CALCVAR GLOBAL Z1 = $REPLACE(Z1, ">", "")
SAVEVAR GLOBAL Z1 ..\QuestDiary\数据文件\InputLog.txt

2. 写 DBC2000 / Access:
通过脚本命令写入数据库的记录,字段长度限制要在脚本层提前拦截。比如 HeroDB 的 Guild 表 Name 字段 varchar(20),脚本里用 $STRLEN 限制在20字符内,超长截断或拒绝。

六、行会名/角色名注册完整过滤示例

[@CallInPutString5]
; 第一层:空值
if

EQUAL S5
act

MESSAGEBOX 行会名不能为空
GOTO @main
elsesay

; 第二层:长度
if

LARGE $STRLEN(S5) 16
act

MESSAGEBOX 行会名最多16个字符
GOTO @main
elsesay

; 第三层:非法字符
if

CHECKCONTAINS S5 ~
act

MESSAGEBOX 不能包含~符号
GOTO @main
elsesay

if

CHECKCONTAINS S5 <
act

MESSAGEBOX 不能包含<符号
GOTO @main
elsesay

if

CHECKCONTAINS S5 >
act

MESSAGEBOX 不能包含>符号
GOTO @main
elsesay

if

CHECKCONTAINS S5 |
act

MESSAGEBOX 不能包含|符号
GOTO @main
elsesay

; 第四层:重复检查
if

CHECKTEXTLIST ..\QuestDiary\Guild\GuildNames.txt S5
act

MESSAGEBOX 该行会名已被注册
GOTO @main
elsesay

; 第五层:引擎内置过滤(自动跳@IsInFilterList,此处不用写)
; 全部通过
act

ADDTEXTLIST ..\QuestDiary\Guild\GuildNames.txt S5
SENDMSG 6 行会名[ S5 ]注册成功

[@IsInFilterList]
act

MESSAGEBOX 行会名含非法词汇,请重新输入
GOTO @main

七、M2过滤列表维护规范

过滤列表不是越长越好,维护原则:
• 按功能分区:行会名过滤、聊天过滤、任务输入过滤分开建表(不同版本支持多个过滤列表文件)

• 定期清理:过时的敏感词删掉,避免误拦正常输入

• 测试流程:每次加新词条后,用测试号走一遍完整输入流程,确认不误伤

• 备份:过滤列表文件改前复制一份 .bak,改崩了能回滚

八、多引擎差异汇总

项目 Hero引擎 3K引擎 GOM/GEE

回调段名 [@CallInPutStringX] [@InPutStringX] [@InPutStringX]

过滤跳转段 [@IsInFilterList] [@MsgFilter] [@MsgFilter]

过滤列表位置 M2→列表信息→其他设置 同左 同左

变量存储 SX + NX SX + NX SX + NX

CHECKCONTAINS 支持 支持 支持

$STRLEN 部分版本支持 部分版本支持 商业版支持

最大输入长度 客户端限制约60字节 同左 同左

九、调试与日志

输入异常时排查步骤:
1. 确认是否触发了 [@IsInFilterList] → 说明命中引擎过滤列表,去 M2 列表里查对应词条
2. 没触发过滤但后续逻辑异常 → 在回调段第一行加 SENDMSG 6 调试:S10,看实际写入内容
3. 变量读出来是空的 → 检查回调段名是否拼对(Hero 必须带 Call)
4. 中文变乱码 → 脚本文件存成 UTF-8 了,改 ANSI
5. 特殊符号没被拦住 → CHECKCONTAINS 的字符可能被引擎吃掉了,换成十六进制比较或升级引擎版本

十、完整防御纵深总结

玩家按键 → 客户端输入框(长度限制) → @@InPutStringX 发包 → 引擎层过滤列表匹配(@IsInFilterList 拦截) → 写入 S 变量 → 回调段脚本校验(空值/长度/字符集/格式/重复) → 转义处理 → 落盘(文本/DB)。五层防线,任意一层漏了还有下一层兜底。脚本层校验永远不能省——引擎过滤列表只管"有没有这个词",不管业务逻辑合不合法。