Skip to main content

wowlab_tidy/languages/rust/rules/safety/
sensitive_debug.rs

1use ra_ap_syntax::{
2    AstNode,
3    ast::{self, HasAttrs, HasName},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10    Example {
11        label: "Debug on struct with password field",
12        code: "#[derive(Debug)]\nstruct Creds { password: String }",
13        pass: false,
14    },
15    Example {
16        label: "Debug on struct without sensitive fields",
17        code: "#[derive(Debug)]\nstruct User { name: String }",
18        pass: true,
19    },
20    Example {
21        label: "no Debug with password field",
22        code: "struct Creds { password: String }",
23        pass: true,
24    },
25    Example {
26        label: "Debug on struct with token field",
27        code: "#[derive(Debug)]\nstruct Auth { token: String }",
28        pass: false,
29    },
30    Example {
31        label: "Debug on struct with api_key field",
32        code: "#[derive(Debug)]\nstruct Config { api_key: String }",
33        pass: false,
34    },
35    Example {
36        label: "sensitive struct in test module",
37        code: "#[cfg(test)]\nmod tests {\n    #[derive(Debug)]\n    struct Creds { password: String }\n}",
38        pass: true,
39    },
40];
41
42crate::ast_rule!(
43    sensitive_debug,
44    "Flag `#[derive(Debug)]` on structs with sensitive fields like `password`.",
45    "Deriving Debug on types with passwords or tokens risks leaking secrets in logs and error messages.",
46    High,
47);
48
49const SENSITIVE_FIELDS: &[&str] = &[
50    "password",
51    "secret",
52    "token",
53    "api_key",
54    "private_key",
55    "secret_key",
56    "auth_token",
57    "credential",
58    "credentials",
59    "access_token",
60    "refresh_token",
61];
62
63fn check_sensitive_debug(ctx: &AstCtx<'_>) -> Vec<Violation> {
64    ctx.nodes::<ast::Struct>()
65        .filter(|item| !ctx.is_in_test(item) && has_debug_derive(item))
66        .filter_map(|item| {
67            let sensitive = find_sensitive_fields(&item);
68            let name = item.name()?;
69
70            (!sensitive.is_empty()).then(|| {
71                ctx.violation(
72                    &name,
73                    format!(
74                        "#[derive(Debug)] on struct with sensitive field(s): {} — implement Debug manually to redact",
75                        sensitive.join(", ")
76                    ),
77                )
78            })
79        })
80        .collect()
81}
82
83fn is_sensitive_field(name: &str) -> bool {
84    SENSITIVE_FIELDS.contains(&name)
85}
86
87fn has_debug_derive(item: &ast::Struct) -> bool {
88    item.attrs().any(|attr| {
89        attr.as_simple_call().is_some_and(|(name, tokens)| {
90            name == "derive"
91                && tokens
92                    .syntax()
93                    .text()
94                    .to_string()
95                    .strip_prefix('(')
96                    .and_then(|text| text.strip_suffix(')'))
97                    .is_some_and(|entries| entries.split(',').any(|entry| entry.trim() == "Debug"))
98        })
99    })
100}
101
102fn find_sensitive_fields(item: &ast::Struct) -> Vec<String> {
103    let Some(ast::FieldList::RecordFieldList(fields)) = item.field_list() else {
104        return Vec::new();
105    };
106
107    let names = fields
108        .fields()
109        .filter_map(|field| field.name())
110        .map(|name| name.text().to_string());
111
112    names.filter(|name| is_sensitive_field(name)).collect()
113}
114
115crate::tidy_ast_test!(check_sensitive_debug, {
116    crate::example_tests!(EXAMPLES, check_sensitive_debug);
117});