fix(sql_safety): 修复 CTE 兄弟片段边界误判

- CTE 内 DELETE/UPDATE 离开当前片段后停止查找 WHERE/LIMIT

- 补充 sibling CTE WHERE/LIMIT 不应降低风险等级的回归测试
This commit is contained in:
Illuminated2020 2026-05-10 17:30:34 +08:00
parent 99e7997620
commit 81a6dce5ac
1 changed files with 34 additions and 3 deletions

View File

@ -163,13 +163,24 @@ fn has_unfiltered_destructive_write(sql: &str) -> bool {
return false;
}
!statement[index + 1..]
.iter()
.any(|boundary| boundary.depth == token.depth && matches!(boundary.text.as_str(), "WHERE" | "LIMIT"))
!has_same_fragment_boundary(&statement, index, token.depth)
})
})
}
fn has_same_fragment_boundary(statement: &[SqlToken], destructive_write_index: usize, depth: usize) -> bool {
for boundary in &statement[destructive_write_index + 1..] {
if boundary.depth < depth {
break;
}
if boundary.depth == depth && matches!(boundary.text.as_str(), "WHERE" | "LIMIT") {
return true;
}
}
false
}
fn executable_tokens(sql: &str) -> Vec<String> {
executable_statements(sql).into_iter().flatten().collect()
}
@ -443,6 +454,26 @@ mod tests {
);
}
#[test]
fn cte_destructive_writes_do_not_use_sibling_cte_boundaries() {
assert_eq!(
risk_for(
"WITH deleted AS (DELETE FROM users RETURNING id), scoped AS (SELECT id FROM audit WHERE id = 1) SELECT * FROM scoped",
RiskContext::new("dev")
)
.risk_level,
RiskLevel::Critical
);
assert_eq!(
risk_for(
"WITH updated AS (UPDATE users SET active = false RETURNING id), scoped AS (SELECT id FROM audit LIMIT 1) SELECT * FROM scoped",
RiskContext::new("dev")
)
.risk_level,
RiskLevel::Critical
);
}
#[test]
fn postgresql_dollar_quotes_do_not_contribute_tokens() {
assert_eq!(classify_sql("SELECT $$ DELETE FROM users $$"), OperationClass::Read);