祝愿大家身体健康!

 找回密码
 站点注册
查看: 6|回复: 0

[学习笔记] PowerBuilder SQL 注入防线全解:参数化两条路、标识符白名单与 LIKE 通配符转义(PB10 基准 · PB12.5 实测)

[复制链接]
发表于 14 分钟前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
本帖最后由 pbai 于 2026-11-11 06:12 编辑

PowerBuilder SQL 注入防线全解:参数化两条路、标识符白名单与 LIKE 通配符转义(PB10 基准 · PB12.5 实测)

阅读说明
1. 适用版本:基准版本 PB10;兼容/实测:PB 12.5(不涉及 PBIDEA 组件,无需 PbIdea.dll)
2. 支持数据库:Microsoft SQL Server(实测 2008 R2,OLE DB / SQLNCLI10 接口);参数化写法本身与 DBMS 无关,但 ESCAPE 子句与占位符细节各家不同,换库请自行验证
3. 操作系统与环境要求:Windows 7 及以上;PB 运行库(PBVM);文中的连库示例需要一台可连的 SQL Server 实例(本文用本机实例 127.0.0.1,18433,库 master)
4. 难度系数:★★★☆☆(需懂嵌入式 SQL 的 DECLARE / OPEN / FETCH 与动态 SQL 的四种格式;不涉及界面)
5. 其它阅读说明:示例表 dbo.t_s94 的建表与数据 SQL 在第十六节,照抄即可复现全部读数;文中读数均为 PB12.5 实机运行结果,PB10 轨的验证口径在第十五节逐条说明




一、先把结论摆在前面

PowerBuilder 里能把外部输入拼进 SQL 的地方只有三种,防线也只有三种,一一对应,没有第四条路:

注入面典型写法正确防线能否参数化
值(字符串、数字、日期)where name = '" + ls_in + "'绑定参数(静待 :变量 / 动态 ?)能,必须用
标识符(表名、列名、排序方向)order by " + ls_col白名单校验(Match 正则或固定枚举)不能
结构(IN 列表长度、列清单、UNION 段数)in (" + ls_ids + ")逐项类型校验后重建不能


记住一句话:参数化只保护"值",凡是拼进 SQL 语法骨架的部分,只能靠白名单。 后面九节的实测读数,基本都是在验证这句话以及它的边界。

另外三件常被忽略的事,本文也会给读数:


  • 手工把单引号双写(O'Brien → O''Brien)能挡住引号类注入,但 PB 的 Replace() 不是查找替换函数,想双写必须自己写循环;
  • 参数化挡不住 %、_ 这类 LIKE 通配符语义,用户输入一个 % 照样能把整表捞出来;
  • 拼接失败时数据库返回的报错文本会原样落进 SQLCA.SQLErrText,把库名、语句片段、甚至除零错误暴露给调用方——这本身就是泄漏。





二、注入在 PB 里长什么样(先复现,再防)
演示说明:本例是一个非可视对象 nvo_s01 的 of_run(),直接 create 后调用即可。它先连库并建好 4 行示例数据,然后把一段"用户输入"拼进 where,用动态游标 COUNT。
步骤:1) 把下面代码放进 of_run();2) 单元测试里 create nvo_s01、调用 of_run();3) 观察弹窗里的 ROWS=。
  1. // 示例输入:模拟攻击者在查询框里敲进去的内容
  2. string ls_in
  3. ls_in = "x' OR '1'='1"
  4. // 示例输入:PB 侧拼出来的"完整 SQL",本来就是想查某一个人的名字
  5. string ls_sql, ls_name
  6. long ll_cnt
  7. ll_cnt = 0
  8. ls_sql = "select name from dbo.t_s94 where name = '" + ls_in + "'"
  9. DECLARE cur94 DYNAMIC CURSOR FOR SQLSA;
  10. PREPARE SQLSA FROM :ls_sql;
  11. OPEN DYNAMIC cur94;
  12. DO WHILE TRUE
  13.         FETCH cur94 INTO :ls_name;
  14.         if SQLCA.SQLCode <> 0 then exit
  15.         ll_cnt = ll_cnt + 1
  16. LOOP
  17. CLOSE cur94;
  18. MessageBox("拼接注入", "SQL=[" + ls_sql + "] ROWS=" + String(ll_cnt))
复制代码

PB12.5 实机读数:
  1. IN=[x' OR '1'='1] SQL=[select name from dbo.t_s94 where name = 'x' OR '1'='1'] OPEN=0 ROWS=4
复制代码

示例表只有 4 行,全被捞出来了。原因很朴素:x' OR '1'='1 把 ' 闭合后,OR '1'='1' 成了永真条件。注意 OPEN=0 —— 这条 SQL 语法完全合法,PB 与数据库都不会报错,你连"出事了"都感知不到。




三、参数化第一条路:静态嵌入式 SQL 的 :变量

PB 的嵌入式 SQL 支持宿主变量绑定。写 where name = :ls_in 时,PB 把 SQL 文本与变量值分开交给数据库接口(MSSQL 侧等价于一次参数化批次),值永远不会被当成 SQL 语法解析。
演示说明:同样是 nvo_s02 的 of_run(),输入串与上一节完全相同,唯一差别是 where 里用 :ls_in。
  1. // 示例输入:与上一节一模一样的注入串
  2. string ls_in
  3. ls_in = "x' OR '1'='1"
  4. string ls_name
  5. long ll_cnt
  6. ll_cnt = 0
  7. DECLARE cur94 CURSOR FOR SELECT name FROM dbo.t_s94 WHERE name = :ls_in ;
  8. OPEN cur94;
  9. if SQLCA.SQLCode = 0 then
  10.         DO WHILE TRUE
  11.                 FETCH cur94 INTO :ls_name;
  12.                 if SQLCA.SQLCode <> 0 then exit
  13.                 ll_cnt = ll_cnt + 1
  14.         LOOP
  15.         CLOSE cur94;
  16. end if
  17. MessageBox("静态参数化", "OPEN=" + String(SQLCA.SQLCode) + " ROWS=" + String(ll_cnt))
复制代码

实机读数:
  1. IN=[x' OR '1'='1] STATIC_PARAM OPEN=0 ROWS=0
复制代码

ROWS=0 —— 数据库认认真真去查"名字恰好等于 x' OR '1'='1 的人",当然没有。这就是参数化的全部魔法:把"值"降级成值。

静态参数的代价是:SQL 结构必须在编译期定死(列、表、条件形状都不能变)。要动态拼结构,就得走第二条路。




四、参数化第二条路:动态 SQL 的 ? 占位

动态 SQL 用问号占位,值通过 OPEN DYNAMIC ... USING 传入。这条路的 SQL 文本可以在运行时拼,但"拼的只是骨架",值仍然是绑定进去的。
演示说明:nvo_s03 的 of_run()。PREPARE 的语句串里 where name = ?,值单独用 USING :ls_in 传。
  1. // 示例输入:注入串,第三遍
  2. string ls_in
  3. ls_in = "x' OR '1'='1"
  4. // 示例输入:动态 SQL 骨架,值的位置只留一个问号
  5. string ls_sql
  6. ls_sql = "select name from dbo.t_s94 where name = ?"
  7. string ls_name
  8. long ll_cnt
  9. ll_cnt = 0
  10. DECLARE cur94 DYNAMIC CURSOR FOR SQLSA;
  11. PREPARE SQLSA FROM :ls_sql;
  12. OPEN DYNAMIC cur94 USING :ls_in;
  13. if SQLCA.SQLCode = 0 then
  14.         DO WHILE TRUE
  15.                 FETCH cur94 INTO :ls_name;
  16.                 if SQLCA.SQLCode <> 0 then exit
  17.                 ll_cnt = ll_cnt + 1
  18.         LOOP
  19.         CLOSE cur94;
  20. end if
  21. MessageBox("动态参数化", "OPEN=" + String(SQLCA.SQLCode) + " ROWS=" + String(ll_cnt))
复制代码

实机读数:
  1. IN=[x' OR '1'='1] DYNPARAM OPEN=0 ROWS=0
复制代码

写 UPDATE / DELETE 时用动态 SQL 的 Format 2

不改动结果集、只带参数执行,用 PREPARE + EXECUTE ... USING(动态 SQL 格式 2)。同一个注入串,拼接与参数化的差别在这里最刺眼:
  1. // 示例输入:攻击者填进"备注"框的内容
  2. string ls_note
  3. ls_note = "x' where 1=1 --"
  4. // 示例输入:本意只想改 id=2 这一行
  5. long ll_id
  6. ll_id = 2
  7. string ls_sql
  8. long ll_n
  9. ls_sql = "update dbo.t_s94 set note = ? where id = ?"
  10. PREPARE SQLSA FROM :ls_sql;
  11. EXECUTE SQLSA USING :ls_note, :ll_id;
  12. ll_n = SQLCA.SQLNRows
  13. MessageBox("参数化 UPDATE", "rc=" + String(SQLCA.SQLCode) + " 影响行数=" + String(ll_n))
复制代码

实机读数(拼接版 vs 参数化版,同一张表、同一段输入):
  1. CONCAT UPDATE rc=0 nrows=4 hit=4 || PARAM UPDATE rc=0 nrows=1 hit=0
复制代码

拼接版 nrows=4,4 行备注全被改成 x(-- 把后面的 where id = 2 注释掉了,hit=4 表示有 4 行备注正好等于 x);参数化版 nrows=1,只有 id=2 一行受影响,且它的备注整串是 x' where 1=1 --(hit=0 是因为没有哪行备注等于单个 x)。返回值 rc=0 两者都一样 —— 又一次说明"没报错"不等于"没出事"。

坑:EXECUTE IMMEDIATE 只认字面量与 :变量

这条是编译期就拦下来的,不算注入问题,但很容易把人卡住。实测三种写法:

写法结果
EXECUTE IMMEDIATE "update dbo.t set a=1" USING SQLCA;通过(字面量)
EXECUTE IMMEDIATE :ls_sql USING SQLCA;通过(冒号前缀)
EXECUTE IMMEDIATE ls_sql USING SQLCA;C0031 Syntax error
EXECUTE IMMEDIATE ("a" + ls_var) USING SQLCA;C0031 Syntax error


所以拼接出来的 SQL 必须先落到变量里,再用 :变量 形式执行 —— 这也侧面说明:凡是需要拼接的 SQL,PB 都留了个"必须显式声明"的口子,别图省事内联。




五、参数化救不了的第一件事:LIKE 通配符

用户输入 %,参数化仍然把它当"值"原样送进去,可数据库对 LIKE 的模式串做的是通配解释,% 依旧是"任意字符"。实测三种写法:
  1. // 示例输入:用户想搜"百分号"这个字符,于是输入一个 %
  2. string ls_in
  3. ls_in = "%"
  4. // 示例输入:反斜杠用 Char(92) 取,避免编辑器/论坛把反斜杠吃掉
  5. string ls_esc, ls_ch, ls_sql, ls_name, ls_bs
  6. long ll_cnt
  7. integer li_i
  8. ls_bs = Char(92)
  9. ls_esc = ""
  10. FOR li_i = 1 TO Len(ls_in)
  11.         ls_ch = Mid(ls_in, li_i, 1)
  12.         if ls_ch = "%" or ls_ch = "_" or ls_ch = ls_bs then ls_esc = ls_esc + ls_bs
  13.         ls_esc = ls_esc + ls_ch
  14. NEXT
  15. ls_sql = "select name from dbo.t_s94 where name like '%" + ls_esc + "%' escape '" + ls_bs + "'"
  16. ll_cnt = 0
  17. DECLARE cur94c DYNAMIC CURSOR FOR SQLSA;
  18. PREPARE SQLSA FROM :ls_sql;
  19. OPEN DYNAMIC cur94c;
  20. if SQLCA.SQLCode = 0 then
  21.         DO WHILE TRUE
  22.                 FETCH cur94c INTO :ls_name;
  23.                 if SQLCA.SQLCode <> 0 then exit
  24.                 ll_cnt = ll_cnt + 1
  25.         LOOP
  26.         CLOSE cur94c;
  27. end if
  28. MessageBox("LIKE 转义", "ESCAPED=[" + ls_esc + "] ROWS=" + String(ll_cnt))
复制代码

实机读数(三个版本一次跑出来):
  1. IN=[%] CONCAT=4/0 PARAM_LIKE=4/0 ESCAPED=[\%] ROWS=0/0
复制代码

三个数字连起来读:


  • CONCAT=4:直接拼进 LIKE 模式,'%%%' 匹配全部 4 行;
  • PARAM_LIKE=4:改成参数化绑定的 like ?,仍然 4 行 —— 参数化不改变"这个值是模式串"这个事实;
  • ESCAPED 后 ROWS=0:把 % 转成 \% 并加 escape '\',才真正变成"名字里含百分号字符"。


结论:LIKE 场景要两道工序——先转义 % _ 与反斜杠本身,再参数化或拼接模式串。只做其中一道都不够。

顺带一个实操细节:代码里的反斜杠建议一律用 Char(92) 取,不要直接写字面反斜杠。它会在复制粘贴、编辑器转义、论坛渲染这三层里被吃掉(我在同一份稿子上就踩过:贴到页面后 "\" 变成了 "",代码直接坏掉)。改成 Char(92) 之后编译与读数完全一致,而且不会因为渲染差异走样。




六、参数化救不了的第二件事:标识符与排序方向

order by ? 这种写法在 PB 里直接失败。实测:
  1. ORDERBY_PARAM OPEN=-1 WHITELIST_IDENT=false OK_IDENT=[name] MATCH=true OPEN=0 ROWS=4
复制代码

order by ? 打开游标返回 -1(数据库端语法错),说明列名、排序方向这类语法骨架元素不能参数化。唯一正解是白名单:
  1. // 示例输入:攻击者试图塞一个分号命令
  2. string ls_col
  3. ls_col = "name; drop table dbo.t_s94"
  4. boolean lb_ok
  5. lb_ok = Match(ls_col, "^[A-Za-z_][A-Za-z0-9_]*$")
  6. if lb_ok then
  7.         MessageBox("白名单", "允许拼接:" + ls_col)
  8. else
  9.         MessageBox("白名单", "拒绝:" + ls_col)
  10. end if
复制代码

读数:WHITELIST_IDENT=false(被拒),而合法列名 name → MATCH=true,拼接后 OPEN=0 ROWS=4 正常。

这里有个必须知道的坑:PB 的 Match() 不是完整正则

实测七种写法(同一台机器,PB10 与 PB12.5 读数逐字一致):
  1. 模式                                    输入         结果
  2. ^[A-Za-z_][A-Za-z0-9_]*$                name         true
  3. ^[A-Za-z_][A-Za-z0-9_]+$                name         true
  4. ^[A-Za-z_][A-Za-z0-9_.]*$               a.b          true
  5. ^(id|name|note)$                        name         false   不支持竖线分支
  6. ^[A-Za-z_][A-Za-z0-9_]{0,39}$           name         false   不支持花括号计数
  7. ^[a-z]+$                                NAME         false   区分大小写
  8. ^[A-Za-z_][A-Za-z0-9_]*$                name;drop    false
复制代码

所以白名单正则要写成上面第一条那种(字母/下划线开头、后续字母数字下划线、末尾锚定)这一条,长度上限用 Len() 单独判,不要指望花括号计数;要枚举几个固定列名,就写循环比字符串逐个比对 —— 分支语法在这里不可用。




七、参数化救不了的第三件事:IN 列表

in (?) 只能塞一个值。多值列表必须重建字符串,此时每个元素都要过类型校验:
  1. INJ=[1,2,3) OR (1=1] CONCAT=4/0 SAFE=[1,2] ROWS=2/0
复制代码

直接拼 in (1,2,3) OR (1=1 得到 4 行(又全表);逐项 IsNumber() 过滤后只剩 1,2,ROWS=2,正确。这里的关键不是"转义",而是只允许数字字符进入 SQL 文本——白名单式重建。




八、手工转义:能救命,但不该当主路

8.1 Replace() 不是查找替换

很多人第一反应是 Replace(ls, "'", "''")。PB 里这句编译不过:
  1. C0052: Bad argument list for function: replace
复制代码

因为 PB 的 Replace() 只有一种签名:Replace(string1, start, n, string2) —— 按位置替换,不是按内容查找。实测位置形态确实可用:
  1. IN=[O'Brien] POSREPLACE=[O''Brien] OPEN=0 ROWS=1
复制代码

Replace(ls_in, 2, 1, "''") 把第 2 个字符(那个单引号)换成两个单引号,拼出来的 SQL 合法且能查到人。但它只对"我知道引号在第 2 位"这种玩具场景成立,通用转义必须自己写循环:
  1. // 示例输入:名字里天然带单引号
  2. string ls_in
  3. ls_in = "O'Brien"
  4. string ls_q, ls_ch
  5. integer li_i
  6. ls_q = ""
  7. FOR li_i = 1 TO Len(ls_in)
  8.         ls_ch = Mid(ls_in, li_i, 1)
  9.         if ls_ch = "'" then ls_q = ls_q + "'"
  10.         ls_q = ls_q + ls_ch
  11. NEXT
  12. MessageBox("手工双写", "QUOTED=[" + ls_q + "]")
复制代码

读数:双写后得到 O''Brien(两个单引号),拼进 SQL 后 OPEN=0 ROWS=1,正确命中那一行。

8.2 不转义会怎样:语法错,不是"查不到"

同一个 O'Brien,不双写直接拼:
  1. CONCAT OPEN=-1 ROWS=0 ERR=[SQLSTATE = 42000
  2. Microsoft SQL Server Native Client 10.0
  3. ...] || PARAM OPEN=0 ROWS=1
复制代码

SQLSTATE 42000 是语法错(不是"0 行"),而参数化版本 OPEN=0 ROWS=1。注意这两种失败的区别:语法错会让后续逻辑以为"查询失败",而注入成功的永真条件会返回一堆不该给的数据——后者才致命,前者至少会暴露。

8.3 为什么不把手写转义当主路

四个理由,都跟"能不能长期不出事"有关:


  • 编码与 N 前缀:GBK 客户端拼进去的中文,跨排序规则/跨库时可能变问号,转义管不了这个;
  • 数字不需要引号,字符串需要,datetime 需要,blob 走另一条路——一套转义逻辑覆盖不全;
  • 二次注入:转义后的值如果被存进库、之后又被拼出来用,双写会翻倍,'''' 之类会越滚越多;
  • 维护成本:每个团队自己写一份转义函数,总有一处漏。


参数化把这些问题一次性消掉,所以顺序永远是:参数化 → 白名单 → 手工转义(仅作兜底,且必须写对)。




九、中文与 N 前缀:三种写法这次都命中了

实测把 张三(用 Char(24352) + Char(19977) 构造,避免源码编码干扰)分别用三种方式查:
  1. CN=[张三] PARAM=1/0 CONCAT_NO_N=1/0 CONCAT_N=1/0
复制代码

参数化、拼接不带 N 前缀、拼接带 N 前缀,三种都命中 1 行。原因是 PB 的 ANSI 客户端把中文按本地代码页(GBK)送出,MSSQL 端对 nvarchar 列做隐式转换后仍能匹配。

但这不代表"不带 N 也安全":这条读数是在"客户端代码页 = 数据库排序规则可兼容"的前提下得到的。换库、换排序规则(例如 Latin1_General)、或列类型是 varchar 时,中文很可能落成问号——这一步我没有实测,只作风险提示。能参数化就别拼中文串,这是唯一稳的做法。




十、数字型参数:别把 '1 OR 1=1' 拼进 id = ...
  1. INJ=[1 OR 1=1] CONCAT=4/0 ISNUMBER_BAD=false ISNUMBER_OK=true LEGAL=[2] ROWS=1/0
复制代码

数字比较同样会被注入:id = 1 OR 1=1 返回 4 行。防法是先校验再拼:IsNumber("1 OR 1=1") 返回 false,IsNumber("2") 返回 true,合法值 2 得到 ROWS=1。

注意一个细节:不要用 Integer(ls_in) 去"试转"非法输入——PB 会抛运行期数值转换错误,直接打断流程。先 IsNumber() 判断,再 Long() 转换,这个顺序不能反。




十一、报错信息本身也是泄漏

拼接版把 x' AND 1=(select 1/0) -- 送进去,得到:
  1. CONCAT OPEN=-1 ERR=[SQLSTATE = 22012
  2. Microsoft SQL Server Native Client 10.0
  3. 遇到以零作除数错误。] || PARAM OPEN=0 ROWS=0 ERR=[]
复制代码

SQLSTATE 22012 + 数据库原始报错文本,原样躺在 SQLCA.SQLErrText 里。这条文本会顺着日志、界面提示、接口返回一路飘到最终用户面前,等于免费告诉攻击者"你的语句被数据库执行了,而且它是什么形状"。

三条处置建议:


  • 界面只显示"查询失败,请稍后重试",原始报错只进日志;
  • 日志里落 SQLCA.SQLCode / SQLDBCode,需要时再查 SQLErrText;
  • 参数化版本的报错文本为空(读数里写作 ERR= 后接空串):正常查询路径根本不该产生报错文本。





十二、可复用的封装:nvo_sqlsafe94

把上面所有结论收进一个非可视对象,七个方法,直接可用。全部方法已随本文实测跑通(读数见本节末)。
演示说明:新建一个 NonVisualObject,命名 nvo_sqlsafe94,把下面代码整体贴进 Source(注意方法顺序无关),保存后 create nvo_sqlsafe94 即可调用。of_run() 是自验证入口,会自己建表、跑全部用例、返回一串读数。
  1. forward
  2. global type nvo_sqlsafe94 from nonvisualobject
  3. end type
  4. end forward
  5. global type nvo_sqlsafe94 from nonvisualobject
  6. end type
  7. global nvo_sqlsafe94 nvo_sqlsafe94
  8. type variables
  9. end variables
  10. forward prototypes
  11. public function string of_escapeQuote (string as_in)
  12. public function string of_likeEscape (string as_in)
  13. public function boolean of_isSafeIdent (string as_in)
  14. public function long of_countByName (string as_name)
  15. public function long of_sumQtyByIds (string as_ids)
  16. public function long of_updateNote (long al_id, string as_note)
  17. public function string of_run ()
  18. end prototypes
  19. public function string of_escapeQuote (string as_in);
  20. string ls_out, ls_ch
  21. integer li_i
  22. ls_out = ""
  23. FOR li_i = 1 TO Len(as_in)
  24.         ls_ch = Mid(as_in, li_i, 1)
  25.         if ls_ch = "'" then ls_out = ls_out + "'"
  26.         ls_out = ls_out + ls_ch
  27. NEXT
  28. return ls_out
  29. end function
  30. public function string of_likeEscape (string as_in);
  31. string ls_out, ls_ch, ls_bs
  32. integer li_i
  33. ls_bs = Char(92)
  34. ls_out = ""
  35. FOR li_i = 1 TO Len(as_in)
  36.         ls_ch = Mid(as_in, li_i, 1)
  37.         if ls_ch = "%" or ls_ch = "_" or ls_ch = ls_bs then ls_out = ls_out + ls_bs
  38.         ls_out = ls_out + ls_ch
  39. NEXT
  40. return ls_out
  41. end function
  42. public function boolean of_isSafeIdent (string as_in);
  43. string ls_s
  44. ls_s = Trim(as_in)
  45. if IsNull(as_in) then return false
  46. if Len(ls_s) = 0 or Len(ls_s) > 64 then return false
  47. return Match(ls_s, "^[A-Za-z_][A-Za-z0-9_]*$")
  48. end function
  49. public function long of_countByName (string as_name);
  50. long ll_cnt
  51. string ls_name
  52. ll_cnt = 0
  53. DECLARE cur_sql94a CURSOR FOR SELECT name FROM dbo.t_s94s WHERE name = :as_name ;
  54. OPEN cur_sql94a;
  55. if SQLCA.SQLCode <> 0 then return -1
  56. DO WHILE TRUE
  57.         FETCH cur_sql94a INTO :ls_name;
  58.         if SQLCA.SQLCode <> 0 then exit
  59.         ll_cnt = ll_cnt + 1
  60. LOOP
  61. CLOSE cur_sql94a;
  62. return ll_cnt
  63. end function
  64. public function long of_sumQtyByIds (string as_ids);
  65. string ls_rest, ls_item, ls_safe, ls_sql, ls_v
  66. long ll_sum, ll_id, li_p
  67. integer li_n
  68. ll_sum = 0
  69. li_n = 0
  70. ls_safe = ""
  71. ls_rest = Trim(as_ids)
  72. DO WHILE Len(ls_rest) > 0
  73.         li_p = Pos(ls_rest, ",")
  74.         if li_p = 0 then
  75.                 ls_item = ls_rest
  76.                 ls_rest = ""
  77.         else
  78.                 ls_item = Left(ls_rest, li_p - 1)
  79.                 ls_rest = Mid(ls_rest, li_p + 1)
  80.         end if
  81.         ls_item = Trim(ls_item)
  82.         if IsNumber(ls_item) then
  83.                 ll_id = Long(ls_item)
  84.                 if ll_id > 0 then
  85.                         li_n = li_n + 1
  86.                         if li_n > 1 then ls_safe = ls_safe + ","
  87.                         ls_safe = ls_safe + String(ll_id)
  88.                 end if
  89.         end if
  90. LOOP
  91. if li_n = 0 then return 0
  92. ls_sql = "select qty from dbo.t_s94s where id in (" + ls_safe + ")"
  93. DECLARE cur_sql94b DYNAMIC CURSOR FOR SQLSA;
  94. PREPARE SQLSA FROM :ls_sql;
  95. OPEN DYNAMIC cur_sql94b;
  96. if SQLCA.SQLCode <> 0 then return -1
  97. DO WHILE TRUE
  98.         FETCH cur_sql94b INTO :ls_v;
  99.         if SQLCA.SQLCode <> 0 then exit
  100.         ll_sum = ll_sum + Long(ls_v)
  101. LOOP
  102. CLOSE cur_sql94b;
  103. return ll_sum
  104. end function
  105. public function long of_updateNote (long al_id, string as_note);
  106. long ll_n
  107. string ls_sql
  108. ls_sql = "update dbo.t_s94s set note = ? where id = ?"
  109. PREPARE SQLSA FROM :ls_sql;
  110. EXECUTE SQLSA USING :as_note, :al_id;
  111. if SQLCA.SQLCode <> 0 then return -1
  112. ll_n = SQLCA.SQLNRows
  113. return ll_n
  114. end function
  115. public function string of_run ();
  116. string ls_out
  117. long ll_n
  118. SQLCA.DBMS = "SNC SQL Native Client(OLE DB)"
  119. SQLCA.LogId = "sa"
  120. // 按实际环境填写口令
  121. SQLCA.LogPass = "your_password"
  122. SQLCA.ServerName = "127.0.0.1,18433"
  123. SQLCA.Database = "master"
  124. SQLCA.AutoCommit = true
  125. SQLCA.DBParm = "Provider='SQLNCLI10',DataSource='127.0.0.1,18433'"
  126. CONNECT USING SQLCA;
  127. if SQLCA.SQLCode <> 0 then return "CONN_FAIL " + String(SQLCA.SQLCode)
  128. execute immediate "if object_id('dbo.t_s94s') is not null drop table dbo.t_s94s" using sqlca;
  129. execute immediate "create table dbo.t_s94s(id int primary key, name nvarchar(50), qty int, note nvarchar(100))" using sqlca;
  130. execute immediate "insert into dbo.t_s94s values(1,'Alice',10,'n1')" using sqlca;
  131. execute immediate "insert into dbo.t_s94s values(2,'Bob',20,'n2')" using sqlca;
  132. execute immediate "insert into dbo.t_s94s values(3,'O''Brien',30,'n3')" using sqlca;
  133. ls_out = "ESC=[" + of_escapeQuote("O'Brien") + "]"
  134. ls_out = ls_out + " LIKE=[" + of_likeEscape("50%_off") + "]"
  135. ls_out = ls_out + " IDENT_OK=" + String(of_isSafeIdent("name"))
  136. ls_out = ls_out + " IDENT_BAD=" + String(of_isSafeIdent("name; drop table dbo.t_s94s"))
  137. ls_out = ls_out + " CNT_QUOTE=" + String(of_countByName("O'Brien"))
  138. ls_out = ls_out + " CNT_INJ=" + String(of_countByName("x' OR '1'='1"))
  139. ls_out = ls_out + " SUM_IDS=" + String(of_sumQtyByIds("1,2,1 OR 1=1"))
  140. ll_n = of_updateNote(2, "x' where 1=1 --")
  141. ls_out = ls_out + " UPD_NROWS=" + String(ll_n)
  142. ls_out = ls_out + " CNT_NOTE=" + String(of_countByName("x' where 1=1 --"))
  143. DISCONNECT USING SQLCA;
  144. return ls_out
  145. end function
复制代码

调用与验证(在任意按钮或测试脚本里):
  1. nvo_sqlsafe94 lnv
  2. string ls_out
  3. lnv = create nvo_sqlsafe94
  4. ls_out = lnv.of_run()
  5. destroy lnv
  6. MessageBox("自验证读数", ls_out)
复制代码

PB12.5 实机读数(与封装版逐字一致):
  1. ESC=[O''Brien] LIKE=[50\%\_off] IDENT_OK=true IDENT_BAD=false CNT_QUOTE=1 CNT_INJ=0 SUM_IDS=30 UPD_NROWS=1 CNT_NOTE=0
复制代码

逐项解释:ESC 双写生效;LIKE 把 % 与 _ 都加了反斜杠;IDENT_OK 放行合法列名、IDENT_BAD 拦下分号命令;CNT_QUOTE=1 参数化按值精确查到 O'Brien;CNT_INJ=0 同一串注入内容查不到东西;SUM_IDS=30 说明 1,2,1 OR 1=1 只取到 1,2 两行(qty 10+20);UPD_NROWS=1 只改一行;CNT_NOTE=0 表示没有哪行备注被写成单个 x。




十三、补一节:DataWindow / DataStore 路线同样吃拼接

很多人以为"只要跑 DataWindow 就安全了"。不是。SyntaxFromSQL() 收的就是一条 SQL 字符串,你拼什么它就生成什么,注入照样进去:
  1. // 示例输入:注入串
  2. string ls_in
  3. ls_in = "x' OR '1'='1"
  4. // 示例输入:拼好的 select,交给 SyntaxFromSQL 生成 DataWindow 语法
  5. string ls_sql, ls_syn, ls_err
  6. long ll_rows
  7. datastore lds
  8. ls_sql = "select name from dbo.t_s94 where name = '" + ls_in + "'"
  9. ls_err = ""
  10. ls_syn = SQLCA.SyntaxFromSQL(ls_sql, "style(type=grid)", ls_err)
  11. lds = create datastore
  12. lds.Create(ls_syn)
  13. lds.SetTransObject(SQLCA)
  14. ll_rows = lds.Retrieve()
  15. destroy lds
  16. MessageBox("DataWindow 路线", "ROWS=" + String(ll_rows))
复制代码

实机读数:
  1. SYNTAX_CONCAT_ROWS=4 SYNTAX_ARG_ROWS=0/-1 ARGERR=[Arguments for select are invalid or inco]
复制代码

SYNTAX_CONCAT_ROWS=4:动态生成的数据窗口把 4 行全捞出来了。

那能不能在 SyntaxFromSQL 的 SQL 里写检索参数(where name = :nm)再 Retrieve(ls_in)?实测不行:SyntaxFromSQL 直接报 Arguments for select are invalid or incomplete,Retrieve 返回 -1(SYNTAX_ARG_ROWS=0/-1)。检索参数必须在数据窗口对象本身里预先声明,动态生成的语法串没有参数定义,这条路走不通。

所以动态 DataWindow 场景的正确做法是二选一:


  • 值不交给 DataWindow 拼:用参数化 SQL 自己取数(第三节/第四节两条路),再用 ImportString() 或逐行 InsertRow / SetItem 灌进 dw;
  • 必须在 SQL 里出现值的(比如 SetSQLSelect 动态换条件),先用 nvo_sqlsafe94.of_escapeQuote() 之类的封装处理,并且把它当作"兜底"而不是"方案"——能改成参数化就一定改。


顺带一句:Modify("DataWindow.Table.Select=...") 与 SetSQLSelect() 属于同一类口子(运行时换 SQL),同样要过白名单或参数化,别以为绕开 + 拼接就安全了。




十四、检查清单(贴到 code review 模板里)


  • 任何 SQL 字符串里出现 + ls_ / + s_ / + sle_ 就是红旗,先问"这个值能不能绑参数";
  • 字符串值一律用 :变量(静态)或 ? + USING(动态),不拼引号;
  • 数字值先 IsNumber() 再 Long(),不要用 Integer() 试转;
  • 日期值同样走参数绑定,不拼 convert();
  • LIKE 的模式串:先转义 %、_ 与反斜杠本身,再绑参数或拼(两道工序);
  • 表名/列名/排序方向:用第六节那条白名单正则(Match 判断「字母或下划线开头、只含字母数字下划线」)或固定枚举,绝不参数化也绝不直接拼;
  • IN 列表:逐项类型校验后重建,元素数上限也要设(防超长 SQL);
  • EXECUTE IMMEDIATE 拼出来的语句先落变量,再用 :变量 执行;
  • 手工双写只在"确定要拼、且拼完整"时用,别写成半套;
  • 界面不要回显 SQLErrText,日志里存 SQLCode / SQLDBCode;
  • SQLCA.SQLNRows 要核对,别只看 SQLCode=0;
  • 交付前跑一遍"输入 x' OR '1'='1 / % / 1 OR 1=1 / name; drop table"四条攻击串,看返回行数是否仍为预期。





十五、PB10 基准 · PB12.5 差异(如实口径)

验证方式:同一套源码(15 个探针 + 1 个封装对象共 16 个对象)分别以 --pb-version 100 与 --pb-version 125 导入、全量重建、跑 .pbtest。

项目PB10 轨PB12.5 轨
导入(16 对象)0 错0 错
全量重建(--type full)0 错0 错
单元测试(16 个 .pbtest)16/16 PASS16/16 PASS
纯语言用例(Match 白名单七种模式)运行 PASS,读数 STAR=true ALT=false RANGE=false PLUS=true CASE=false DOT=true SEMI=false运行 PASS,读数逐字一致
连库用例(15 个对象)运行级 CONN_FAIL -1运行 PASS,读数见正文各节


关于 PB10 轨连库:本机 PB10 运行库目录(只带 pbvm100.dll / pbdwe100.dll 及依赖)不含任何数据库接口 DLL(无 pbsnc100 / pbmss100 / pbado100)。实测三种接口逐一尝试:
  1. SNC11=-1 ADO=-1 OLEDB=-1
复制代码

因此 PB10 轨的连库读数一律为 CONN_FAIL -1,这是环境缺接口,不是代码差异;连库行为各节读数全部来自 PB12.5 实测。语言层(Match、Replace、Pos/Mid、FOR/DO 结构、编译期 C0052 / C0031 判定)两版无差异。

PB12.5 差异:无。除上述"PB10 运行库缺数据库接口"这一环境事实外,未发现两版在本文涉及的任何语法与函数行为上有差异。




十六、示例数据(照抄即可复现全部读数)
  1. if object_id('dbo.t_s94') is not null drop table dbo.t_s94;
  2. create table dbo.t_s94(id int primary key, name nvarchar(50), note nvarchar(100));
  3. insert into dbo.t_s94 values(1,'Alice','n1');
  4. insert into dbo.t_s94 values(2,'Bob','n2');
  5. insert into dbo.t_s94 values(3,'O''Brien','n3');
  6. insert into dbo.t_s94 values(4,N'张三','n4');
复制代码

封装对象用的是另一张同构表 dbo.t_s94s,多一个 qty 列:
  1. if object_id('dbo.t_s94s') is not null drop table dbo.t_s94s;
  2. create table dbo.t_s94s(id int primary key, name nvarchar(50), qty int, note nvarchar(100));
  3. insert into dbo.t_s94s values(1,'Alice',10,'n1');
  4. insert into dbo.t_s94s values(2,'Bob',20,'n2');
  5. insert into dbo.t_s94s values(3,'O''Brien',30,'n3');
复制代码




十七、一句话收尾

PB 里的注入防线其实只有两条:值走参数,骨架走白名单。剩下的都是这两条的细节——LIKE 要转义、IN 要逐项校验、Match 不给分支语法、Replace 不是查找替换、报错文本要收好。把这些细节都落实了,"用户的输入"才真正只是数据。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-11-11 06:22 , Processed in 0.020445 second(s), 6 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表