@comate/zulu 1.3.3 → 1.3.4-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/comate-engine/assets/skills/auto-commit-comate/SKILL.md +42 -62
  2. package/comate-engine/assets/skills/auto-commit-comate/references/data_structures.md +69 -75
  3. package/comate-engine/assets/skills/auto-commit-comate/references/interaction_instruction.md +220 -0
  4. package/comate-engine/assets/skills/auto-commit-comate/scripts/build_git_commit_payload.py +195 -0
  5. package/comate-engine/assets/skills/auto-commit-comate/scripts/build_icafe_cards_payload.py +80 -0
  6. package/comate-engine/assets/skills/auto-commit-comate/scripts/cache_manager.py +69 -0
  7. package/comate-engine/assets/skills/auto-commit-comate/scripts/git_diff_cli.py +5 -0
  8. package/comate-engine/assets/skills/auto-commit-comate/scripts/match_card_cli.py +5 -1
  9. package/comate-engine/assets/skills/auto-commit-comate/scripts/payload_validators.py +309 -0
  10. package/comate-engine/assets/skills/code-security-comate/SKILL.md +2 -1
  11. package/comate-engine/assets/skills/code-security-comate/references/vul_repair-go_sql_injection.md +627 -5
  12. package/comate-engine/assets/skills/code-security-comate/references/vul_repair-java_sql_injection.md +545 -21
  13. package/comate-engine/assets/skills/code-security-comate/references/vul_repair-php_sql_injection.md +596 -13
  14. package/comate-engine/assets/skills/code-security-comate/references/vul_repair-python_sql_injection.md +480 -82
  15. package/comate-engine/assets/skills/code-security-comate/scripts/http_client.py +10 -2
  16. package/comate-engine/assets/skills/code-security-comate/scripts/repair_vulnerability.py +12 -10
  17. package/comate-engine/assets/skills/code-security-comate/scripts/report_chat.py +1 -1
  18. package/comate-engine/assets/skills/comate-docs-comate/SKILL.md +70 -105
  19. package/comate-engine/assets/skills/comate-docs-comate/references/doc-map-extended.md +52 -7
  20. package/comate-engine/assets/skills/comate-docs-comate/references/models-and-billing.md +45 -26
  21. package/comate-engine/assets/skills/comate-docs-comate/references/product-overview.md +60 -14
  22. package/comate-engine/assets/skills/create-image-comate/SKILL.md +7 -7
  23. package/comate-engine/assets/skills/get-ugate-token-comate/SKILL.md +2 -0
  24. package/comate-engine/node_modules/@comate/plugin-engine/dist/index.js +1 -1
  25. package/comate-engine/node_modules/@comate/plugin-host/dist/index-7HvPXRep.js +1 -0
  26. package/comate-engine/node_modules/@comate/plugin-host/dist/index.js +1 -1
  27. package/comate-engine/node_modules/@comate/plugin-host/dist/main.js +1 -1
  28. package/comate-engine/node_modules/@comate/plugin-host/dist/user-DVjAG3wH.js +44 -0
  29. package/comate-engine/node_modules/@comate/plugin-shared-internals/dist/index.js +8 -8
  30. package/comate-engine/server.js +378 -266
  31. package/dist/bundle/index.js +3 -3
  32. package/package.json +1 -1
@@ -87,7 +87,7 @@ $users = User::findBySql($sql, [':status' => $status])->all();
87
87
  ### 4. 无法预编译场景(Order By / Table Name)
88
88
  ```php
89
89
  $sort = $_GET['sort'];
90
- $regex = "/^[\w\s\[\]" . "`" . "$.,*]+$/";
90
+ $regex = '/^[\w\s\[\]`\$.,*]+$/';
91
91
 
92
92
  if (!preg_match($regex, $sort)) {
93
93
  throw new \yii\web\BadRequestHttpException("Invalid sort parameter");
@@ -99,7 +99,29 @@ $users = User::find()->orderBy($sort)->all();
99
99
  1. `execute()`** 返回值误用**:Yii2 `execute()` 返回影响行数(int),不能继续 `->queryAll()`。
100
100
  2. **提前创建 Command**:必须先拼完 SQL,再 `createCommand($sql)`。
101
101
  3. **Yii1 **`criteria->params`** 覆盖**:`$criteria->params = [...]` 会覆盖已设置参数,需合并而非覆盖。
102
+ 4. **Yii1 命名参数与位置参数混用**:Yii 1.x 的 `bindValues` 根据 `isset($values[0])` 判断参数模式,混用会导致绑定失败。
102
103
 
104
+ ### 6. Yii1 vs Yii2 差异
105
+ |差异点|Yii 1.x|Yii 2.x|
106
+ |-|-|-|
107
+ |参数绑定判断|`isset($values[0])` 判断模式|支持混合模式|
108
+ |`criteria->params`|覆盖式赋值需注意|`addParams()` 可用|
109
+ |`execute()` 返回|影响行数|影响行数|
110
+
111
+ **Yii1 混用错误示例**:
112
+
113
+ ```php
114
+ // 错误:命名参数 :uid 与位置参数数组混用
115
+ $params = array_merge([':uid' => $uid], $requestIdList); // $requestIdList 是 0-indexed
116
+ $model->findAllBySql($sql, $params);
117
+ // bindValues 检测到 isset($params[0]) 为 true,进入位置模式,忽略 :uid
118
+
119
+ // 正确:全部使用命名参数
120
+ $params = [':uid' => $uid];
121
+ foreach ($requestIdList as $i => $id) {
122
+ $params[":id$i"] = $id;
123
+ }
124
+ ```
103
125
  ---
104
126
 
105
127
  ## III. ODP 框架修复方案
@@ -148,14 +170,24 @@ $result = $stmt->execute();
148
170
 
149
171
  ### 2. 使用规则
150
172
  1. **必须使用临时变量承接过滤结果**,后续 SQL 只能使用安全变量。
151
- 2. 禁止“过滤后继续使用原变量”。
173
+ 2. 禁止"过滤后继续使用原变量"。
152
174
  3. 非法输入必须返回错误/中断,不允许静默放行。
153
- 4. 对对象参数,优先提供“对象级过滤函数”,返回安全对象后再下传。
175
+ 4. 对对象参数,优先提供"对象级过滤函数",返回安全对象后再下传。
176
+ 5. **定义过滤函数后必须在 sink 前调用**,禁止只定义不调用。
177
+
178
+ ```php
179
+ // 错误:定义了但未调用
180
+ function safeFilterTablePrefix($input) { ... }
181
+ $sql = "SELECT * FROM {$tableprefix}_data"; // 仍直接使用污点变量
154
182
 
183
+ // 正确:定义并调用
184
+ $safePrefix = safeFilterTablePrefix($tableprefix);
185
+ $sql = "SELECT * FROM {$safePrefix}_data";
186
+ ```
155
187
  ### 3. 示例(标量)
156
188
  ```php
157
189
  function safeFilterOrderBy($input) {
158
- $regex = '/^[\w\s\[\]"`$.,*]+$/';
190
+ $regex = '/^[\w\s\[\]"`\$.,*]+$/';
159
191
  if (!is_string($input) || !preg_match($regex, $input)) {
160
192
  throw new InvalidArgumentException('Invalid order by');
161
193
  }
@@ -168,7 +200,7 @@ $sql = "SELECT * FROM t ORDER BY {$safeOrderBy}";
168
200
  ### 4. 示例(对象)
169
201
  ```php
170
202
  function safeValidateQuery($query) {
171
- $regex = '/^[\w\s\[\]"`$.,*]+$/';
203
+ $regex = '/^[\w\s\[\]"`\$.,*]+$/';
172
204
  if (isset($query['sort']) && !preg_match($regex, $query['sort'])) {
173
205
  throw new InvalidArgumentException('Invalid sort');
174
206
  }
@@ -232,11 +264,37 @@ $rows = $stmt->fetchAll();
232
264
  1. `execute()` 返回 bool,不能当 Statement 使用。
233
265
  2. `prepare + bindValue` 后必须 `execute()` 再 `fetch/fetchAll`。
234
266
  3. 禁止同一 SQL 中混用 `?` 与 `:name`。
267
+ 4. **PDO 位置参数 0-based 键问题**:`array_merge` 产生 0-based 索引数组,PDO `?` 占位符期望连续数组。
268
+
269
+ ```php
270
+ // 错误:array_merge 产生 0-based 索引数组
271
+ $params = array_merge($arr1, $arr2); // [0 => 'a', 1 => 'b', ...]
272
+
273
+ // 正确:使用 array_values 确保连续数组
274
+ $params = array_values(array_merge($arr1, $arr2));
275
+ $stmt->execute($params);
276
+ ```
277
+ ### 5. mysqli 修复方案
278
+ `mysqli_stmt::execute()` 返回 `bool`,不能直接调用 `fetch_array()`。
235
279
 
280
+ ```php
281
+ // 错误
282
+ $stmt = $mysqli->prepare($sql);
283
+ $stmt->bind_param('s', $param);
284
+ $result = $stmt->execute(); // $result 是 bool
285
+ $row = $result->fetch_array(); // Fatal Error
286
+
287
+ // 正确
288
+ $stmt = $mysqli->prepare($sql);
289
+ $stmt->bind_param('s', $param);
290
+ $stmt->execute();
291
+ $result = $stmt->get_result(); // 获取 mysqli_result
292
+ $row = $result->fetch_array();
293
+ ```
236
294
  ---
237
295
 
238
296
  ## V. 必须使用正则过滤的场景
239
- 以下场景不支持预编译,必须使用正则 `^[\w\s\[\]\"\x60$.,*]+$` 过滤:
297
+ 以下场景不支持预编译,必须使用正则过滤:
240
298
 
241
299
  1. 动态表名/库名:`FROM $tableName`
242
300
  2. 动态列名:`SELECT $columnName`
@@ -247,6 +305,507 @@ $rows = $stmt->fetchAll();
247
305
 
248
306
  * 若文件头/类常量已定义类似正则(如 `SAFE_SQL_PATTERN`),直接复用。
249
307
 
308
+ ##### SQLi 正则过滤规范
309
+ ###### 核心决策树
310
+ ```
311
+ 用户输入需要过滤?
312
+
313
+ ├─ 是 SQL 标识符(表名/列名/数据库名/索引名)?
314
+ │ └─ → 正则验证格式 + 白名单验证(双重保障)
315
+
316
+ ├─ 是 SELECT 列表?
317
+ │ └─ → 正则验证格式 + 提取列名后白名单验证
318
+
319
+ ├─ 是 WHERE 条件/IN 子句?
320
+ │ └─ → 使用参数化查询(强烈推荐)
321
+
322
+ ├─ 是 ORDER BY/GROUP BY?
323
+ │ └─ → 正则验证格式 + 提取列名后白名单验证
324
+
325
+ ├─ 是 LIMIT/OFFSET?
326
+ │ └─ → 正则验证纯数字
327
+
328
+ └─ 其他场景
329
+ └─ → 根据具体需求选择白名单或精确正则
330
+ ```
331
+ ---
332
+
333
+ ###### 白名单数据来源与处理策略
334
+ ###### 白名单数据从哪里来?
335
+ |来源|说明|示例|
336
+ |-|-|-|
337
+ |**数据库元数据查询**|从数据库系统表查询实际存在的表/列名|`SELECT column_name FROM information_schema.columns WHERE table_name = 'users'`|
338
+ |**代码静态分析**|扫描代码中定义的实体类、ORM 模型|JPA `@Entity`、MyBatis mapper、SQLAlchemy Model|
339
+ |**配置文件/注解**|从配置文件或注解中提取允许的字段列表|`@AllowedFields({"id", "name", "email"})`|
340
+ |**API 文档/接口定义**|从 Swagger/OpenAPI 等文档中提取字段定义||
341
+
342
+ ###### 白名单不完整时的处理策略
343
+ ```
344
+ 白名单验证失败?
345
+
346
+ ├─ 白名单数据来源明确(如元数据查询、ORM 模型)?
347
+ │ └─ → 拒绝输入,记录日志,提示用户
348
+
349
+ ├─ 白名单数据来源不明确(如硬编码、不完整)?
350
+ │ └─ → 降级为仅正则验证,记录警告日志
351
+
352
+ └─ 无法获取白名单(如动态表名)?
353
+ └─ → 仅正则验证格式,加强输入长度限制
354
+ ```
355
+ **AI 修复决策原则**:
356
+
357
+ 1. **优先获取白名单**:如果代码中有 ORM 模型或实体类定义,优先从中提取字段列表
358
+ 2. **正则兜底**:当白名单获取困难或不完整时,使用严格正则作为安全底线
359
+ 3. **记录决策原因**:在代码注释中说明为什么选择正则而非白名单
360
+
361
+ ---
362
+
363
+ ###### 不同数据库的差异说明
364
+ |数据库|标识符引用符|参数占位符|特殊注意事项|
365
+ |-|-|-|-|
366
+ |**MySQL**|反引号 ```|`?` 或 `:name`|支持反引号包裹标识符,如 `user_name`|
367
+ |**PostgreSQL**|双引号 `"`|`$1, $2, ...` 或 `?`|支持双引号包裹标识符,如 `"user_name"`|
368
+ |**SQL Server**|方括号 `[` `]`|`@param` 或 `?`|支持方括号包裹标识符,如 `[user_name]`|
369
+ |**Oracle**|双引号 `"`|`:name` 或 `?`|标识符默认大写,双引号区分大小写|
370
+ |**SQLite**|双引号 `"` 或方括号 `[` `]`|`?` 或 `:name`|兼容多种引用符|
371
+
372
+ **AI 修复原则**:根据当前使用的数据库类型,选择对应的引用符和参数占位符。
373
+
374
+ ---
375
+
376
+ ###### 选择策略对比
377
+ |场景|推荐策略|原因|
378
+ |-|-|-|
379
+ |SQL 标识符(表名/列名/数据库名/索引名)|正则 + 白名单|格式验证 + 值验证,双重保障|
380
+ |SELECT 列表|正则 + 白名单|允许通配符 `*` 和多列,列名需白名单验证|
381
+ |ORDER BY/GROUP BY|正则 + 白名单|格式验证 + 列名白名单 + 方向关键字(ASC/DESC)|
382
+ |LIMIT/OFFSET|严格正则|只允许数字,不需要白名单|
383
+ |WHERE 条件|**参数化查询**|正则过滤仍有风险,参数化最安全|
384
+ |IN 子句|参数化查询优先|优先参数化,其次严格正则|
385
+
386
+ ---
387
+
388
+ ###### 各场景正则与实现
389
+ ###### 1. SELECT 列名列表 `__SELECT_FILTER__`
390
+ **适用场景**:SELECT 后面的列名列表(如 `SELECT id,name,email FROM users` 或 `SELECT users.id,users.name FROM users`)
391
+
392
+ **正则(通用版,支持引用符)**:
393
+
394
+ ```
395
+ ^(\*|((`[^`]+`|"[^"]+"|\w+\.\w+|\w+)(,\s*(`[^`]+`|"[^"]+"|\w+\.\w+|\w+))*))$
396
+ ```
397
+ **支持格式**:
398
+
399
+ * `*` (通配符)
400
+ * `id` (单列)
401
+ * `users.id` (表名.列名)
402
+ * `user_name` (MySQL 反引号)
403
+ * `"user_name"` (PostgreSQL/Oracle 双引号)
404
+ * `id,name,email` (多列,逗号分隔)
405
+ * `users.id, users.name, users.email` (带表名前缀的多列)
406
+ * `id, name, email` (允许逗号前后空格)
407
+
408
+ **AI 修复指引**:
409
+
410
+ ```php
411
+ function safeFilterSelect($select, $allowedColumns = []) {
412
+ // 1. 先 trim() 去除首尾空白
413
+ $select = trim($select);
414
+ // 2. 正则验证格式(支持引用符)
415
+ $regex = '/^(\*|((`[^`]+`|"[^"]+"|\w+\.\w+|\w+)(,\s*(`[^`]+`|"[^"]+"|\w+\.\w+|\w+))*))$/';
416
+ if (!preg_match($regex, $select)) {
417
+ return false;
418
+ }
419
+ // 3. 如果是通配符 *,直接允许
420
+ if ($select === '*') {
421
+ return $select;
422
+ }
423
+ // 4. 提取列名并验证白名单
424
+ $parts = preg_split('/\s*,\s*/', $select);
425
+ foreach ($parts as $part) {
426
+ // 去除引用符
427
+ $cleanPart = preg_replace('/^[`"]|[`"]$/', '', $part);
428
+ // 处理表名.列名格式,提取列名部分
429
+ $column = strpos($cleanPart, '.') !== false
430
+ ? substr($cleanPart, strrpos($cleanPart, '.') + 1)
431
+ : $cleanPart;
432
+ // 白名单验证(如果白名单不完整,跳过此步仅使用正则)
433
+ if (!empty($allowedColumns)) {
434
+ if (!in_array(strtolower(trim($column)), $allowedColumns, true)) {
435
+ return false;
436
+ }
437
+ }
438
+ }
439
+ return $select;
440
+ }
441
+ ```
442
+ ---
443
+
444
+ ###### 2. SQL 标识符(表名/列名/数据库名/索引名)`__IDENTIFIER_FILTER__`
445
+ **适用场景**:表名、列名、数据库名、索引名、视图名等 SQL 标识符
446
+
447
+ **正则(通用版,支持引用符)**:
448
+
449
+ ```
450
+ ^[A-Za-z_][A-Za-z0-9_]{0,127}$|^`[^`]{1,128}`$|^"[^"]{1,128}"$
451
+ ```
452
+ **支持格式**:
453
+
454
+ * `user` (小写字母开头)
455
+ * `User123` (字母数字)
456
+ * `user_name` (下划线)
457
+ * `_private` (下划线开头,部分数据库支持)
458
+ * `user_name` (MySQL 反引号)
459
+ * `"user_name"` (PostgreSQL/Oracle 双引号)
460
+ * `[user_name]` (SQL Server 方括号)
461
+ * 长度限制:1-128 字符
462
+
463
+ **AI 修复指引**:
464
+
465
+ ```php
466
+ function safeFilterIdentifier($identifier, $allowedIdentifiers = []) {
467
+ // 1. 先 trim() 去除首尾空白
468
+ $identifier = trim($identifier);
469
+ // 2. 正则验证格式(支持引用符)
470
+ $regex = '/^[A-Za-z_][A-Za-z0-9_]{0,127}$|^`[^`]{1,128}`$|^"[^"]{1,128}"$|^\[[^\]]{1,128}\]$/';
471
+ if (!preg_match($regex, $identifier)) {
472
+ return false;
473
+ }
474
+ // 3. 去除引用符
475
+ $cleanIdentifier = preg_replace('/^[`"\[]|[`"\]]$/', '', $identifier);
476
+ // 4. 白名单验证(如果白名单不完整,跳过此步仅使用正则)
477
+ if (!empty($allowedIdentifiers)) {
478
+ if (!in_array(strtolower($cleanIdentifier), $allowedIdentifiers, true)) {
479
+ return false;
480
+ }
481
+ }
482
+ return $identifier;
483
+ }
484
+ ```
485
+ **说明**:
486
+
487
+ * 表名、列名、数据库名、索引名等标识符都遵循相同的命名规范
488
+ * 正则验证格式 + 白名单验证允许的值,双重保障
489
+ * 如果白名单不完整或无法获取,可省略步骤 4,仅使用正则验证
490
+
491
+ ---
492
+
493
+ ###### 3. ORDER BY `__ORDER_FILTER__`
494
+ **适用场景**:ORDER BY 后的字段(如 `ORDER BY name ASC, id DESC` 或 `ORDER BY users.name ASC, users.id DESC`)
495
+
496
+ **正则(通用版,支持引用符)**:
497
+
498
+ ```
499
+ ^((`[^`]+`|"[^"]+"|\w+\.\w+|\w+)(\s+(ASC|DESC))?(,\s*(`[^`]+`|"[^"]+"|\w+\.\w+|\w+)(\s+(ASC|DESC))?)*)$
500
+ ```
501
+ **支持格式**:
502
+
503
+ * `name` (单列)
504
+ * `users.name` (表名.列名)
505
+ * `user_name` (MySQL 反引号)
506
+ * `"user_name"` (PostgreSQL/Oracle 双引号)
507
+ * `name ASC` / `name DESC` (带方向)
508
+ * `users.name ASC` / `users.name DESC` (带表名前缀和方向)
509
+ * `name ASC, id DESC` (多列)
510
+ * `users.name ASC, users.id DESC` (带表名前缀的多列)
511
+
512
+ **AI 修复指引**:
513
+
514
+ ```php
515
+ function safeFilterOrderBy($order, $allowedColumns = []) {
516
+ // 1. 正则验证格式(不区分大小写)
517
+ $regex = '/^((`[^`]+`|"[^"]+"|\w+\.\w+|\w+)(\s+(ASC|DESC))?(,\s*(`[^`]+`|"[^"]+"|\w+\.\w+|\w+)(\s+(ASC|DESC))?)*)$/i';
518
+ if (!preg_match($regex, $order)) {
519
+ return false;
520
+ }
521
+ // 2. 提取列名并验证白名单
522
+ $parts = preg_split('/\s*,\s*/', $order);
523
+ foreach ($parts as $part) {
524
+ // 提取列名部分(处理表名.列名格式)
525
+ $columnPart = preg_split('/\s+/', $part)[0];
526
+ // 去除引用符
527
+ $cleanPart = preg_replace('/^[`"]|[`"]$/', '', $columnPart);
528
+ $column = strpos($cleanPart, '.') !== false
529
+ ? substr($cleanPart, strrpos($cleanPart, '.') + 1)
530
+ : $cleanPart;
531
+ // 白名单验证(如果白名单不完整,跳过此步仅使用正则)
532
+ if (!empty($allowedColumns)) {
533
+ if (!in_array(strtolower($column), $allowedColumns, true)) {
534
+ return false;
535
+ }
536
+ }
537
+ }
538
+ return $order;
539
+ }
540
+ ```
541
+ ---
542
+
543
+ ###### 4. GROUP BY `__GROUP_FILTER__`
544
+ **适用场景**:GROUP BY 后的字段(如 `GROUP BY name, age` 或 `GROUP BY users.name, users.age`)
545
+
546
+ **正则(通用版,支持引用符)**:
547
+
548
+ ```
549
+ ^((`[^`]+`|"[^"]+"|\w+\.\w+|\w+)(,\s*(`[^`]+`|"[^"]+"|\w+\.\w+|\w+))*)$
550
+ ```
551
+ **支持格式**:
552
+
553
+ * `name` (单列)
554
+ * `users.name` (表名.列名)
555
+ * `user_name` (MySQL 反引号)
556
+ * `"user_name"` (PostgreSQL/Oracle 双引号)
557
+ * `name, age` (多列)
558
+ * `users.name, users.age` (带表名前缀的多列)
559
+
560
+ **AI 修复指引**:
561
+
562
+ ```php
563
+ function safeFilterGroupBy($group, $allowedColumns = []) {
564
+ // 1. 正则验证格式
565
+ $regex = '/^((`[^`]+`|"[^"]+"|\w+\.\w+|\w+)(,\s*(`[^`]+`|"[^"]+"|\w+\.\w+|\w+))*)$/';
566
+ if (!preg_match($regex, $group)) {
567
+ return false;
568
+ }
569
+ // 2. 提取列名并验证白名单
570
+ $parts = preg_split('/\s*,\s*/', $group);
571
+ foreach ($parts as $part) {
572
+ // 去除引用符
573
+ $cleanPart = preg_replace('/^[`"]|[`"]$/', '', $part);
574
+ // 提取列名部分(处理表名.列名格式)
575
+ $column = strpos($cleanPart, '.') !== false
576
+ ? substr($cleanPart, strrpos($cleanPart, '.') + 1)
577
+ : $cleanPart;
578
+ // 白名单验证(如果白名单不完整,跳过此步仅使用正则)
579
+ if (!empty($allowedColumns)) {
580
+ if (!in_array(strtolower(trim($column)), $allowedColumns, true)) {
581
+ return false;
582
+ }
583
+ }
584
+ }
585
+ return $group;
586
+ }
587
+ ```
588
+ ---
589
+
590
+ ###### 5. LIMIT/OFFSET `__LIMIT_FILTER__`
591
+ **适用场景**:LIMIT、OFFSET 后的数值(如 `LIMIT 10`、`LIMIT 1,10`)
592
+
593
+ **正则**:`^\d+\s*,\s*\d+$|^\d+$`
594
+
595
+ **支持格式**:
596
+
597
+ * `10` (单数字)
598
+ * `1,10` (带偏移量)
599
+ * `1, 10` (允许逗号前后空格)
600
+
601
+ **AI 修复指引**:
602
+
603
+ ```php
604
+ function safeFilterLimit($limit) {
605
+ // 1. 先 trim() 去除首尾空白
606
+ $limit = trim($limit);
607
+ // 2. 正则验证纯数字
608
+ if (preg_match('/^\d+\s*,\s*\d+$|^\d+$/', $limit)) {
609
+ return $limit;
610
+ }
611
+ return false;
612
+ }
613
+ ```
614
+ ---
615
+
616
+ ###### 6. WHERE 条件 `__WHERE_FILTER__`
617
+ **适用场景**:WHERE 子句、条件判断
618
+
619
+ **策略**:**放弃正则,使用参数化查询**
620
+
621
+ **AI 修复指引**:
622
+
623
+ ```php
624
+ // ❌ 错误:字符串拼接
625
+ $sql = "SELECT * FROM users WHERE name = '" . $name . "'";
626
+
627
+ // ✅ 正确:参数化查询(PDO 使用 ?)
628
+ $sql = "SELECT * FROM users WHERE name = ?";
629
+ $stmt = $pdo->prepare($sql);
630
+ $stmt->execute([$name]);
631
+
632
+ // ✅ 正确:参数化查询(PDO 使用命名参数)
633
+ $sql = "SELECT * FROM users WHERE name = :name";
634
+ $stmt = $pdo->prepare($sql);
635
+ $stmt->bindValue(':name', $name);
636
+ $stmt->execute();
637
+
638
+ // ✅ 正确:参数化查询(mysqli)
639
+ $stmt = $mysqli->prepare("SELECT * FROM users WHERE name = ?");
640
+ $stmt->bind_param('s', $name);
641
+ $stmt->execute();
642
+ $result = $stmt->get_result();
643
+ ```
644
+ ---
645
+
646
+ ###### 7. IN 子句 `__IN_FILTER__`
647
+ **适用场景**:IN 后面的值列表(如 `IN (1,2,3)`)
648
+
649
+ **策略**:优先参数化查询,其次严格正则
650
+
651
+ **正则**:
652
+
653
+ * 数字:`^\(\s*\d+(\s*,\s*\d+)*\s*\)$`
654
+ * 字符串:`^\(\s*'[^']*'(\s*,\s*'[^']*')*\s*\)$`
655
+
656
+ **支持格式**:
657
+
658
+ * `(1,2,3)` (数字列表)
659
+ * `('a','b','c')` (字符串列表)
660
+ * `(1, 2, 3)` (允许逗号前后空格)
661
+
662
+ **AI 修复指引**:
663
+
664
+ ```php
665
+ // ✅ 优先:参数化查询(PDO 使用 ?)
666
+ $ids = [1, 2, 3];
667
+ $placeholders = implode(',', array_fill(0, count($ids), '?'));
668
+ $sql = "SELECT * FROM users WHERE id IN ($placeholders)";
669
+ $stmt = $pdo->prepare($sql);
670
+ $stmt->execute($ids);
671
+
672
+ // ✅ 优先:参数化查询(PDO 使用命名参数)
673
+ $ids = [1, 2, 3];
674
+ $params = [];
675
+ $placeholders = [];
676
+ foreach ($ids as $i => $id) {
677
+ $key = ":id$i";
678
+ $placeholders[] = $key;
679
+ $params[$key] = $id;
680
+ }
681
+ $sql = "SELECT * FROM users WHERE id IN (" . implode(',', $placeholders) . ")";
682
+ $stmt = $pdo->prepare($sql);
683
+ $stmt->execute($params);
684
+
685
+ // ⚠️ 备选:严格正则验证(仅当无法参数化时)
686
+ function safeFilterInClause($inClause) {
687
+ $inClause = trim($inClause);
688
+ if (preg_match('/^\(\s*\d+(\s*,\s*\d+)*\s*\)$/', $inClause)) {
689
+ return $inClause;
690
+ }
691
+ return false;
692
+ }
693
+ ```
694
+ ---
695
+
696
+ ###### 8. 方向关键字 `__DIRECTION_FILTER__`
697
+ **适用场景**:ORDER BY 后的 ASC/DESC
698
+
699
+ **正则**:`^(asc|desc)$`
700
+
701
+ **支持格式**:
702
+
703
+ * `ASC` / `DESC` / `asc` / `desc` (不区分大小写)
704
+
705
+ **AI 修复指引**:
706
+
707
+ ```php
708
+ function safeFilterDirection($direction) {
709
+ // 正则验证(不区分大小写)
710
+ if (preg_match('/^(asc|desc)$/i', $direction)) {
711
+ return strtoupper($direction);
712
+ }
713
+ return false;
714
+ }
715
+ ```
716
+ ---
717
+
718
+ ###### 9. 纯字母数字标识符 `__W_FILTER__`
719
+ **适用场景**:简单的表名、列名(无特殊字符)
720
+
721
+ **正则**:`^[\w-]+$`
722
+
723
+ **支持格式**:
724
+
725
+ * `user_name` (下划线)
726
+ * `User123` (字母数字)
727
+ * `table-1` (连字符)
728
+
729
+ **AI 修复指引**:
730
+
731
+ ```php
732
+ function safeFilterSimpleIdentifier($identifier) {
733
+ if (preg_match('/^[\w-]+$/', $identifier)) {
734
+ return $identifier;
735
+ }
736
+ return false;
737
+ }
738
+ ```
739
+ ---
740
+
741
+ ###### 10. 路径过滤 `__PATH_FILTER__`
742
+ **适用场景**:文件路径、URL 路径
743
+
744
+ **正则**:`^[\w/-]+$`
745
+
746
+ **支持格式**:
747
+
748
+ * `/path/to/file` (绝对路径)
749
+ * `path/to/file` (相对路径)
750
+
751
+ **AI 修复指引**:
752
+
753
+ ```php
754
+ function safeFilterPath($path) {
755
+ if (preg_match('/^[\w\/-]+$/', $path)) {
756
+ return $path;
757
+ }
758
+ return false;
759
+ }
760
+ ```
761
+ ---
762
+
763
+ ###### 11. 运算符 `__OP_FILTER__`
764
+ **适用场景**:算术运算符(如 `score+1`)
765
+
766
+ **正则**:`[+\-*/%]`
767
+
768
+ **支持格式**:
769
+
770
+ * `+` `-` `*` `/` `%` (基本算术运算符)
771
+
772
+ **AI 修复指引**:
773
+
774
+ ```php
775
+ function safeFilterOperator($operator) {
776
+ if (preg_match('/^[+\-*\/%]$/', $operator)) {
777
+ return $operator;
778
+ }
779
+ return false;
780
+ }
781
+ ```
782
+ ---
783
+
784
+ ###### 正则速查表
785
+ |过滤器|正则|说明|
786
+ |-|-|-|
787
+ |`__IDENTIFIER_FILTER__`|`/^[A-Za-z_][A-Za-z0-9_]{0,127}$\|^`[^`]{1,128}`$\|^"[^"]{1,128}"$/`|SQL 标识符,支持反引号/双引号,1-128字符|
788
+ |`__SELECT_FILTER__`|`/^(\*\|((`[^`]+`\|"[^"]+"\|\w+\.\w+\|\w+)(,\s*(`[^`]+`\|"[^"]+"\|\w+\.\w+\|\w+))*))$/`|允许通配符 `*` 和逗号分隔的列名|
789
+ |`__ORDER_FILTER__`|`/^((`[^`]+`\|"[^"]+"\|\w+\.\w+\|\w+)(\s+(ASC\|DESC))?(,\s*(`[^`]+`\|"[^"]+"\|\w+\.\w+\|\w+)(\s+(ASC\|DESC))?)*)$/i`|列名 + 方向关键字|
790
+ |`__GROUP_FILTER__`|`/^((`[^`]+`\|"[^"]+"\|\w+\.\w+\|\w+)(,\s*(`[^`]+`\|"[^"]+"\|\w+\.\w+\|\w+))*)$/`|多列,逗号分隔|
791
+ |`__LIMIT_FILTER__`|`/^\d+\s*,\s*\d+$\|^\d+$/`|纯数字,允许逗号|
792
+ |`__WHERE_FILTER__`|**参数化查询**|不要用正则|
793
+ |`__IN_FILTER__`|`/^\(\s*\d+(\s*,\s*\d+)*\s*\)$/`|数字列表|
794
+ |`__DIRECTION_FILTER__`|`/^(asc\|desc)$/i`|ASC 或 DESC|
795
+ |`__W_FILTER__`|`/^[\w-]+$/`|字母数字+下划线+连字符|
796
+ |`__PATH_FILTER__`|`/^[\w\/-]+$/`|路径字符|
797
+ |`__OP_FILTER__`|`/^[+\-*\/%]$/`|算术运算符|
798
+
799
+ ---
800
+
801
+ ###### 重要提示
802
+ 1. **所有正则验证前,务必先 **`trim()`** 去除首尾空白字符**
803
+ 2. **白名单优先**:列名/表名等固定值优先用白名单
804
+ 3. **白名单不完整时降级**:当白名单获取困难或不完整时,使用严格正则作为安全底线
805
+ 4. **参数化优先**:WHERE 条件、IN 子句优先用参数化查询
806
+ 5. **数据库适配**:根据使用的数据库类型选择对应的引用符和参数占位符
807
+ 6. **最小化改动**:只修复漏洞点,不重构其他代码
808
+
250
809
  ### 补充:操作符/排序方向建议枚举校验(不替换现有正则)
251
810
  在保留上述正则前提下,建议叠加枚举校验:
252
811
 
@@ -289,23 +848,47 @@ if (!in_array($op, $validOps, true)) {
289
848
  8. 修复位置是否与真实数据流文件/函数一致(避免改错位置)。
290
849
  9. 是否引入语法错误、死代码或不可达代码。
291
850
  10. 正则与枚举校验是否按预期拦截非法输入。
851
+ 11. **新增函数是否插入在正确位置**。
852
+ 12. **正则表达式中的特殊符号是否正确转义或置于边界位置**。
853
+ 13. **IN 子句的占位符数量是否与参数数组长度一致**。
854
+ 14. **扫描报告指出的所有 sink 是否都已修复**(仅修复报告中的 sink,不主动扩展修复 AI 发现的其他分支)。
855
+ 15. **新定义的过滤函数是否在 sink 前被调用**。
856
+ 16. **PDO 位置参数绑定的数组索引是否正确**(注意 `array_merge` 产生 0-based 索引)。
857
+ 17. **修复前后代码是否确实有差异**(避免无效修复提交)。
858
+ 18. **框架的 **`query()`** 参数数组维度是否正确**(如 CodeIgniter 期望一维数组)。
859
+ 19. **自定义白名单正则是否意外允许了单引号等 SQL 元字符**。
860
+ 20. **占位符名称是否与 SQL 中一致**(`:name` 对应 `bindValue(':name', $val)`)。
292
861
 
293
862
  ---
294
863
 
295
- ## VIII. 高频修复失败反模式(来自 php_review_20260416)
864
+ ## VIII. 高频修复失败反模式
296
865
  以下为高频失败原因,修复时必须逐项自检:
297
866
 
298
- 1. **调用不存在 API**:如盲目使用 `prepare/queryf/execute($sql,$params)`。
867
+ 1. **调用不存在 API**:如盲目使用 `prepare/queryf/execute($sql,$params)`。修复前必须在项目中**全局搜索**确认函数存在或框架支持该函数。
299
868
  2. **修错文件或修错函数**:与 SARIF 数据流路径不一致。
300
- 3. **部分修复**:只修一个分支或一部分参数,其他 sink 仍可注入。
869
+ 3. **部分修复**:只修一个分支或一部分参数,扫描报告指出的 sink 仍可注入。**注意:仅修复漏洞扫描报告中明确指出的 sink,不主动修复 AI 自行发现的其他分支问题**。
301
870
  4. **新旧 SQL 并存**:参数化查询后仍保留旧拼接查询。
302
871
  5. **变量作用域错误**:分支变量在另一分支未定义即绑定。
303
872
  6. **过滤顺序错误**:先拼接 SQL 后过滤/转义。
304
873
  7. **仅加引号不转义**:`'$input'` 不是安全修复。
305
- 8. **IN 数组绑定错误**:单占位符绑定数组或空数组未兜底。
306
- 9. **过度依赖“看起来安全”**:未验证框架/DAO 真实能力。
307
- 10. **未做回归检查**:代码改了但未验证是否仍被扫描命中。
874
+ 8. **IN 数组绑定错误**:单占位符绑定数组或空数组未兜底。PDO/Yii 的 `bindValue` **不支持**将单个参数展开为 IN 子句多值。
875
+ 9. **过度依赖"看起来安全"**:未验证框架/DAO 真实能力。
876
+ 10. **占位符与变量不匹配**:`bindValue()` 的占位符名称与 SQL 中不一致,或绑定的变量未定义。
877
+ 11. **PHP 语法错误**:在函数声明与函数体之间插入代码,破坏类结构。
878
+ 12. **数组直接字符串插值**:PHP 数组在字符串插值中输出 "Array",导致 SQL 语法错误。
879
+ 13. **定义了过滤函数但未调用**:业务代码中未调用新定义的 `safeFilter*` 函数。
880
+ 14. **无效修复提交**:修复的 `newContent` 与 `originalContent` 完全相同。
881
+
882
+ ### 占位符不匹配典型错误
883
+ ```php
884
+ // 错误示例:$param 未定义,SQL 中无 :param 占位符
885
+ $sql = "SELECT * FROM table WHERE id = {$id}"; // 仍为直接拼接
886
+ $stmt = $db->createCommand($sql)->bindValue('param:', $param)->queryAll();
308
887
 
888
+ // 正确做法
889
+ $sql = "SELECT * FROM table WHERE id = :id";
890
+ $stmt = $db->createCommand($sql)->bindValue(':id', $id)->queryAll();
891
+ ```
309
892
  ---
310
893
 
311
894
  ## IX. 最小修复策略(执行建议)
@@ -315,4 +898,4 @@ if (!in_array($op, $validOps, true)) {
315
898
  2. 可参数化则优先参数化;不可参数化则走正则过滤。
316
899
  3. 对结构化参数叠加枚举校验(方向、操作符等)。
317
900
  4. 使用 `safeFilter/safeValidate` + 临时变量截断数据流。
318
- 5. 做 VII 节 10 条强检查后再提交。
901
+ 5. 做 VII 节 20 条强检查后再提交。