scripts/csp-inline-style-check.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
#
# CSP inline-style guard.
#
# The SPA is served under a strict `style-src 'self'` Content-Security-Policy, which
# blocks inline STYLE ATTRIBUTES: the browser rejects `element.style.cssText = ...`,
# `element.style = "..."`, `element.style['cssText'] = ...`, `setAttribute('style', ...)`,
# and `style="..."` attributes parsed from innerHTML/SVG markup. Set styles PER PROPERTY
# instead — `element.style.left = '4px'` — which the CSP allows. This guard fails if a
# forbidden form (including common whitespace/bracket evasions) reappears in the
# frontend JS, so the fixes in commit history cannot silently regress.
#
# A deliberate, reviewed exception may carry the marker `csp-allow-inline-style` on the
# SAME line to be skipped.
#
# Run locally: bash scripts/csp-inline-style-check.sh
# Self-test only: bash scripts/csp-inline-style-check.sh --self-test
set -uo pipefail
# Banned inline-style forms (POSIX ERE). Whitespace variants are tolerated so trivial
# reformatting cannot slip past the guard.
PATTERNS=(
'\.style[[:space:]]*\.[[:space:]]*cssText[[:space:]]*=' # .style.cssText = (incl. spaced dots)
"\.style[[:space:]]*\[[[:space:]]*['\"]cssText['\"]" # .style['cssText'] (bracket access)
"\.style[[:space:]]*=[[:space:]]*['\"]" # el.style = "..." (cssText-equivalent)
"setAttribute[[:space:]]*\([[:space:]]*['\"][[:space:]]*style" # setAttribute('style', ...) (spaced)
'style=["'\'']' # style="..." attribute in markup
)
# The markup check stays whitespace-free (`style=`) on purpose: a lenient
# `style[[:space:]]*=` would also flag JS variable assignments named `style`
# (e.g. `const style = "..."`), a false positive. The `.style =` pattern above already
# covers the real cssText-equivalent assignment via its required leading dot.
# grep_hits <path> -> "file:line:text" for every banned form, excluding marked exceptions.
grep_hits() {
local target="$1" pat
for pat in "${PATTERNS[@]}"; do
grep -rnE "$pat" "$target" 2>/dev/null
done | grep -v 'csp-allow-inline-style' | sort -u
}
run_scan() {
local dir="${1:-web/static/js}" hits
hits="$(grep_hits "$dir" || true)"
if [ -n "$hits" ]; then
printf '%s\n' "$hits"
echo " -> inline style attribute / cssText is CSP-blocked; set styles per property"
echo " via the CSSOM (or mark a reviewed exception with csp-allow-inline-style)."
echo "CSP inline-style guard: FAILED"
exit 1
fi
echo "CSP inline-style guard: clean"
}
self_test() {
local tmp rc=0; tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' RETURN
# Forms that MUST be caught (regression fixtures for each supported evasion).
cat > "$tmp/evasions.js" <<'JS'
a.style.cssText = 'x';
a.style . cssText = 'x';
a.style['cssText'] = 'x';
a.style[ "cssText" ] = 'x';
a.style = 'color:red';
a.style = "color:red";
el.setAttribute('style','x');
el.setAttribute ( "style", "x");
var s = '<div style="x">';
JS
# Forms that must NOT be caught (safe per-property + look-alikes + a marked exception).
cat > "$tmp/safe.js" <<'JS'
a.style.left = '1px';
a.style.animationDelay = '-1s';
const style = 'solid';
let borderStyle = "dashed";
a.setAttribute('data-style','x');
w.write('<div style="x">'); // csp-allow-inline-style: reviewed
JS
local want got safe_hits
want=$(grep -cvE '^[[:space:]]*$' "$tmp/evasions.js")
got=$(grep_hits "$tmp/evasions.js" | grep -c ':' || true)
if [ "$got" -lt "$want" ]; then
echo "SELF-TEST FAIL: caught $got/$want evasion forms"; grep_hits "$tmp/evasions.js"; rc=1
else
echo "self-test: all $want evasion forms caught"
fi
safe_hits="$(grep_hits "$tmp/safe.js" || true)"
if [ -n "$safe_hits" ]; then
echo "SELF-TEST FAIL: false positive on safe forms:"; printf '%s\n' "$safe_hits"; rc=1
else
echo "self-test: no false positives on safe forms"
fi
return "$rc"
}
if [ "${1:-}" = "--self-test" ]; then
self_test; exit $?
fi
# CI runs the self-test first (fast, no deps) so the guard's own patterns are verified,
# then scans the real tree.
self_test || exit 1
echo "---"
run_scan "${1:-web/static/js}"