fix(security): root-cause guards against blank tool guard rule rows

This commit is contained in:
matevip 2026-05-10 08:27:42 +08:00
parent 4567bd6019
commit ae4d432ee2
4 changed files with 94 additions and 5 deletions

View File

@ -138,6 +138,17 @@ public class SecurityController {
}
}
@Operation(summary = "按主键 ID 删除自定义规则兜底rule_id 异常时使用)")
@DeleteMapping("/guard/rules/by-id/{id}")
public R<String> deleteRuleByPk(@PathVariable Long id) {
try {
ruleService.deleteRuleByPk(id);
return R.ok("删除成功");
} catch (IllegalArgumentException e) {
return R.fail(e.getMessage());
}
}
// ==================== Audit ====================
@Operation(summary = "审计日志")

View File

@ -64,10 +64,15 @@ public class ToolGuardRuleService {
* 新增自定义规则
*/
public ToolGuardRuleEntity createRule(ToolGuardRuleEntity rule) {
if (rule == null || rule.getRuleId() == null || rule.getRuleId().isBlank()) {
throw new IllegalArgumentException("Rule ID is required");
if (rule == null) {
throw new IllegalArgumentException("Rule body is required");
}
requireNonBlank(rule.getRuleId(), "Rule ID");
requireNonBlank(rule.getName(), "Rule name");
requireNonBlank(rule.getPattern(), "Rule pattern");
rule.setRuleId(rule.getRuleId().trim());
rule.setName(rule.getName().trim());
rule.setPattern(rule.getPattern().trim());
rule.setBuiltin(false);
ruleMapper.insert(rule);
ruleRegistry.reload();
@ -75,7 +80,8 @@ public class ToolGuardRuleService {
}
/**
* 更新规则
* 更新规则仅覆盖请求里显式提供的字段显式传入的关键字段name / pattern
* 不允许置为空白避免回写出无意义的"空名空模式"
*/
public ToolGuardRuleEntity updateRule(String ruleId, ToolGuardRuleEntity update) {
ToolGuardRuleEntity existing = getByRuleId(ruleId);
@ -83,14 +89,20 @@ public class ToolGuardRuleService {
throw new IllegalArgumentException("Rule not found: " + ruleId);
}
if (update.getName() != null) existing.setName(update.getName());
if (update.getName() != null) {
requireNonBlank(update.getName(), "Rule name");
existing.setName(update.getName().trim());
}
if (update.getDescription() != null) existing.setDescription(update.getDescription());
if (update.getToolName() != null) existing.setToolName(update.getToolName());
if (update.getParamName() != null) existing.setParamName(update.getParamName());
if (update.getCategory() != null) existing.setCategory(update.getCategory());
if (update.getSeverity() != null) existing.setSeverity(update.getSeverity());
if (update.getDecision() != null) existing.setDecision(update.getDecision());
if (update.getPattern() != null) existing.setPattern(update.getPattern());
if (update.getPattern() != null) {
requireNonBlank(update.getPattern(), "Rule pattern");
existing.setPattern(update.getPattern().trim());
}
if (update.getExcludePattern() != null) existing.setExcludePattern(update.getExcludePattern());
if (update.getRemediation() != null) existing.setRemediation(update.getRemediation());
if (update.getEnabled() != null) existing.setEnabled(update.getEnabled());
@ -101,6 +113,12 @@ public class ToolGuardRuleService {
return existing;
}
private static void requireNonBlank(String value, String fieldLabel) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(fieldLabel + " is required");
}
}
/**
* 启用/禁用规则
*/
@ -128,4 +146,24 @@ public class ToolGuardRuleService {
ruleMapper.deleteById(existing.getId());
ruleRegistry.reload();
}
/**
* 按主键 ID 删除自定义规则兜底通道 rule_id 因历史脏数据为空或无法走
* /guard/rules/{ruleId} 路径变量时UI 仍可通过主键删除
*/
public void deleteRuleByPk(Long id) {
if (id == null) {
throw new IllegalArgumentException("Rule primary key is required");
}
ToolGuardRuleEntity existing = ruleMapper.selectById(id);
if (existing == null) {
throw new IllegalArgumentException("Rule not found: id=" + id);
}
if (Boolean.TRUE.equals(existing.getBuiltin())) {
throw new IllegalArgumentException(
"Cannot delete builtin rule: " + existing.getRuleId());
}
ruleMapper.deleteById(id);
ruleRegistry.reload();
}
}

View File

@ -0,0 +1,26 @@
-- Earlier releases let the rule-create API persist a custom guard rule
-- with a blank rule_id because the service skipped the not-blank check.
-- The resulting row was undeletable from the UI: the delete endpoint is
-- /guard/rules/{ruleId}, and a blank path variable produces a 404 instead
-- of resolving to the row. This migration does two things:
--
-- 1. Purge any orphan rows already persisted on existing installations
-- so users who hit the bug on v1.2.0 can recover without direct DB
-- surgery. NULL, empty, and whitespace-only rule_id are all swept;
-- built-in rules are excluded defensively because they are seeded
-- with stable IDs and should never appear here.
--
-- 2. Add a CHECK constraint so the database itself rejects blank
-- rule_id going forward. The service-layer guard already prevents
-- this from the UI, but the DB constraint defends against any
-- future code path that bypasses the service (batch import, direct
-- SQL, future endpoints) and makes the invariant explicit at the
-- schema level.
DELETE FROM mate_tool_guard_rule
WHERE (rule_id IS NULL OR LENGTH(TRIM(rule_id)) = 0)
AND (builtin IS NULL OR builtin = FALSE);
ALTER TABLE mate_tool_guard_rule
ADD CONSTRAINT ck_tool_guard_rule_id_nonblank
CHECK (rule_id IS NOT NULL AND LENGTH(TRIM(rule_id)) > 0);

View File

@ -0,0 +1,14 @@
-- See the matching H2 file for context. This migration purges any
-- orphan rows that earlier releases persisted with a blank rule_id and
-- then installs a CHECK constraint so the schema itself rejects blank
-- rule_id, defending against any future code path that bypasses the
-- service-layer guard. CHECK constraints are enforced from MySQL 8.0.16
-- onward; this project targets MySQL 8.0+ so the constraint is live.
DELETE FROM mate_tool_guard_rule
WHERE (rule_id IS NULL OR LENGTH(TRIM(rule_id)) = 0)
AND (builtin IS NULL OR builtin = FALSE);
ALTER TABLE mate_tool_guard_rule
ADD CONSTRAINT ck_tool_guard_rule_id_nonblank
CHECK (rule_id IS NOT NULL AND LENGTH(TRIM(rule_id)) > 0);