1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! A structural guard against reintroducing findings 10a and 10c.
//!
//! Those findings were fixed by routing every interpolated value through
//! `safety::ps_quote` or `safety::applescript_quote`. Nothing stopped the *next*
//! contributor — or the next refactor — from writing `format!("Start-Service
//! '{}'", name)` again, which is how the defect got in twice: escaping was
//! inconsistent rather than absent, so the pattern looked handled on review.
//!
//! This scans the source for the shape of the bug rather than relying on anyone
//! noticing it. It is a lint, so it is heuristic by nature: it looks for a
//! quoted `{}` placeholder on a line that also looks like a PowerShell or
//! AppleScript command. False positives are fixed by using the helper (which is
//! what you wanted anyway) or, if genuinely not a shell string, by adding the
//! line's distinguishing text to `ALLOWED`.
//!
//! Why a source scan and not a type: the correct fix is a newtype that only the
//! quoting helpers can construct, so a raw `String` cannot reach a command
//! builder at all. That is a larger refactor across ~117 PowerShell sites. This
//! holds the line until then, and fails loudly if it slips.
use std::path::PathBuf;
/// Substrings that mark a line as *not* shell-command construction, even though
/// it contains a quoted placeholder. These are overwhelmingly error messages.
const ALLOWED: &[&str] = &[
"anyhow!",
"arg_err",
"bad_arg",
"E_",
"unsupported operator",
"unknown ",
"invalid ",
"no such ",
"does not exist",
"not provided",
"requires ",
"Cannot convert",
"failed to",
"outside the workspace",
"no backend found",
"cannot be",
"expected ",
"[SECURITY]",
];
/// Tokens that indicate the line is building a PowerShell or AppleScript
/// command, where an unquoted interpolation is a command-injection vector.
const SHELL_MARKERS: &[&str] = &[
"Get-",
"Set-",
"New-",
"Remove-",
"Start-",
"Stop-",
"Restart-",
"Add-",
"Invoke-",
"Compress-",
"Expand-",
"Where-Object",
"Select-Object",
"ConvertTo-",
"ConvertFrom-",
"Read-Host",
"Write-Output",
"FindWindow",
"System.",
"display notification",
"display dialog",
"do shell script",
"$_.",
"$env:",
];
fn source(rel: &str) -> String {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(rel);
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()))
}
/// A line is suspect if it interpolates directly inside quotes *and* looks like
/// a shell command.
fn is_suspect(line: &str) -> bool {
let trimmed = line.trim();
if trimmed.starts_with("//") || trimmed.starts_with("///") || trimmed.starts_with("*") {
return false;
}
// The two injectable shapes: '{}' for PowerShell single-quoted, "{}" (as
// \"{}\" in a Rust literal, or bare inside a raw string) for double-quoted
// PowerShell and AppleScript.
let has_quoted_placeholder =
trimmed.contains("'{}'") || trimmed.contains("\\\"{}\\\"") || trimmed.contains("\"{}\"");
if !has_quoted_placeholder {
return false;
}
if ALLOWED.iter().any(|a| trimmed.contains(a)) {
return false;
}
SHELL_MARKERS.iter().any(|m| trimmed.contains(m))
}
#[test]
fn no_shell_command_interpolates_a_value_without_quoting_it() {
let mut offenders = Vec::new();
for file in ["src/builtins.rs", "src/safety.rs", "src/os_tools.rs"] {
for (i, line) in source(file).lines().enumerate() {
if is_suspect(line) {
offenders.push(format!(" {}:{}\n {}", file, i + 1, line.trim()));
}
}
}
assert!(
offenders.is_empty(),
"shell command(s) interpolate a value directly into a quoted literal — a value \
containing a quote (PowerShell/AppleScript) or `$(…)` (double-quoted PowerShell) \
executes.\n\nUse `safety::ps_quote(&v)` or `safety::applescript_quote(&v)` and \
interpolate with `{{}}` rather than `'{{}}'`, so the helper supplies the quotes.\n\n\
If the line is genuinely not a shell string, add a distinguishing substring to \
ALLOWED in this test.\n\n{}",
offenders.join("\n")
);
}
/// The guard must actually fire — a lint that cannot fail is worse than none,
/// because it reads as coverage.
#[test]
fn the_guard_detects_the_shape_it_is_meant_to_catch() {
assert!(
is_suspect(r#" .args(["-Command", &format!("Start-Service '{}'", name)])"#),
"the exact pre-fix shape of finding 10a must be flagged"
);
assert!(
is_suspect(r#"$bytes = [System.Text.Encoding]::UTF8.GetBytes("{}")"#),
"the pre-fix shape of finding 10c must be flagged"
);
assert!(
is_suspect(r#""display notification \"{}\" with title \"{}\"","#),
"the AppleScript shape must be flagged"
);
// And must not fire on the fixed form, or it would block the correct fix.
assert!(!is_suspect(
r#" .args(["-Command", &format!("Start-Service {}", crate::safety::ps_quote(&name))])"#
));
assert!(!is_suspect(
r#"$bytes = [System.Text.Encoding]::UTF8.GetBytes({})"#
));
// Nor on ordinary error messages.
assert!(!is_suspect(
r#"return Err(anyhow!("sess_eval: no such session '{}'", id));"#
));
}