Skip to main content

cha_core/plugins/
unsafe_api.rs

1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3/// Detect usage of potentially dangerous functions and constructs.
4///
5/// Uses tree-sitter AST queries (when `ctx.tree` is available) so matches
6/// inside string literals, comments, and identifiers are not falsely flagged.
7/// Falls back to nothing when there's no AST — better silence than noise.
8///
9/// ## References
10///
11/// [1] CWE-676: Use of Potentially Dangerous Function.
12///     https://cwe.mitre.org/data/definitions/676.html
13pub struct UnsafeApiAnalyzer;
14
15impl Plugin for UnsafeApiAnalyzer {
16    fn name(&self) -> &str {
17        "unsafe_api"
18    }
19
20    fn smells(&self) -> Vec<String> {
21        vec!["unsafe_api".into()]
22    }
23
24    fn description(&self) -> &str {
25        "Dangerous function calls (eval/exec/system)"
26    }
27
28    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
29        let (Some(tree), Some(lang)) = (ctx.tree, ctx.ts_language) else {
30            return vec![];
31        };
32        let patterns = queries_for(&ctx.model.language);
33        if patterns.is_empty() {
34            return vec![];
35        }
36        let source = ctx.file.content.as_bytes();
37        let mut findings = Vec::new();
38        for (pattern, label, msg) in patterns {
39            for matches in crate::query::run_query(tree, lang, source, pattern) {
40                let Some(cap) = matches.iter().find(|c| c.capture_name == "site") else {
41                    continue;
42                };
43                findings.push(Finding {
44                    smell_name: "unsafe_api".into(),
45                    category: SmellCategory::Security,
46                    severity: Severity::Warning,
47                    location: Location {
48                        path: ctx.file.path.clone(),
49                        start_line: cap.start_line as usize,
50                        start_col: cap.start_col as usize,
51                        end_line: cap.end_line as usize,
52                        end_col: cap.end_col as usize,
53                        name: None,
54                    },
55                    message: format!("Potentially dangerous: `{label}` — {msg}"),
56                    suggested_refactorings: vec!["Use a safe alternative".into()],
57                    ..Default::default()
58                });
59            }
60        }
61        findings
62    }
63}
64
65/// Tree-sitter S-expr queries per language. Each entry is
66/// `(query_pattern, display_label, message)`.
67///
68/// Queries should produce a `@site` capture for the location to report.
69// cha:ignore long_method
70fn queries_for(lang: &str) -> Vec<(&'static str, &'static str, &'static str)> {
71    match lang {
72        "rust" => vec![
73            (
74                "(unsafe_block) @site",
75                "unsafe block",
76                "unsafe block — review for memory safety",
77            ),
78            (
79                "(function_modifiers \"unsafe\") @site",
80                "unsafe fn",
81                "unsafe fn — review for memory safety",
82            ),
83        ],
84        "python" => vec![
85            (
86                r#"(call function: (identifier) @n (#eq? @n "eval")) @site"#,
87                "eval",
88                "eval() executes arbitrary code",
89            ),
90            (
91                r#"(call function: (identifier) @n (#eq? @n "exec")) @site"#,
92                "exec",
93                "exec() executes arbitrary code",
94            ),
95            (
96                r#"(call function: (attribute object: (identifier) @o attribute: (identifier) @a) (#eq? @o "os") (#eq? @a "system")) @site"#,
97                "os.system",
98                "os.system() is vulnerable to shell injection",
99            ),
100            (
101                r#"(call function: (attribute object: (identifier) @o attribute: (identifier) @a) (#eq? @o "subprocess") (#eq? @a "call")) @site"#,
102                "subprocess.call",
103                "prefer subprocess.run with shell=False",
104            ),
105            (
106                r#"(call function: (attribute object: (identifier) @o attribute: (identifier) @a) (#eq? @o "pickle") (#match? @a "^(load|loads)$")) @site"#,
107                "pickle.load",
108                "pickle deserialization can execute arbitrary code",
109            ),
110        ],
111        "typescript" => vec![
112            (
113                r#"(call_expression function: (identifier) @n (#eq? @n "eval")) @site"#,
114                "eval",
115                "eval() executes arbitrary code",
116            ),
117            (
118                r#"(member_expression property: (property_identifier) @p (#eq? @p "innerHTML")) @site"#,
119                "innerHTML",
120                "innerHTML can lead to XSS",
121            ),
122            (
123                r#"(jsx_attribute (property_identifier) @p (#eq? @p "dangerouslySetInnerHTML")) @site"#,
124                "dangerouslySetInnerHTML",
125                "React escape hatch — review for XSS",
126            ),
127            (
128                r#"(call_expression function: (member_expression object: (identifier) @o property: (property_identifier) @p) (#eq? @o "document") (#eq? @p "write")) @site"#,
129                "document.write",
130                "document.write can lead to XSS",
131            ),
132        ],
133        "c" | "cpp" => vec![
134            (
135                r#"(call_expression function: (identifier) @n (#eq? @n "gets")) @site"#,
136                "gets",
137                "gets() has no bounds checking — use fgets()",
138            ),
139            (
140                r#"(call_expression function: (identifier) @n (#eq? @n "sprintf")) @site"#,
141                "sprintf",
142                "sprintf() has no bounds checking — use snprintf()",
143            ),
144            (
145                r#"(call_expression function: (identifier) @n (#eq? @n "strcpy")) @site"#,
146                "strcpy",
147                "strcpy() has no bounds checking — use strncpy()",
148            ),
149            (
150                r#"(call_expression function: (identifier) @n (#eq? @n "strcat")) @site"#,
151                "strcat",
152                "strcat() has no bounds checking — use strncat()",
153            ),
154            (
155                r#"(call_expression function: (identifier) @n (#eq? @n "system")) @site"#,
156                "system",
157                "system() is vulnerable to shell injection",
158            ),
159        ],
160        "go" => vec![
161            (
162                r#"(call_expression function: (selector_expression operand: (identifier) @o field: (field_identifier) @f) (#eq? @o "exec") (#eq? @f "Command")) @site"#,
163                "exec.Command",
164                "review for command injection",
165            ),
166            (
167                r#"(call_expression function: (selector_expression operand: (identifier) @o field: (field_identifier) @f) (#eq? @o "template") (#eq? @f "HTML")) @site"#,
168                "template.HTML",
169                "bypasses HTML escaping — review for XSS",
170            ),
171        ],
172        _ => vec![],
173    }
174}