mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-24 09:02:19 +02:00
108 lines
4.2 KiB
Python
108 lines
4.2 KiB
Python
r"""DOM/CSS-injection regression for calendar background-image URL escaping.
|
|
|
|
CodeQL `js/incomplete-sanitization` (#463 calendar.js:416, #464 calendar.js:1263)
|
|
flagged event-background CSS that escaped `'` -> `\'` without first escaping
|
|
backslashes. A `bg:`-color value (settable per event, and CalDAV-syncable, so
|
|
untrusted) ending in or containing a backslash can then consume the closing
|
|
quote of `url('...')` and break out of the CSS string.
|
|
|
|
The fix is a single canonical escaper, `_cssUrlEscape`, in calendar/utils.js,
|
|
used by both inline sinks and by `_calBgCss` (which had the same incomplete
|
|
escaping). These tests pin the escaper: backslashes are doubled FIRST, then
|
|
quotes, so no input can terminate the `url('...')` string early.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_REPO = Path(__file__).resolve().parent.parent
|
|
_UTILS = (_REPO / "static" / "js" / "calendar" / "utils.js").as_posix()
|
|
_CALENDAR_JS = _REPO / "static" / "js" / "calendar.js"
|
|
_HAS_NODE = shutil.which("node") is not None
|
|
|
|
pytestmark = pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
|
|
|
|
|
|
def _run(js: str) -> str:
|
|
proc = subprocess.run(
|
|
["node", "--input-type=module"],
|
|
input=js, capture_output=True, text=True, cwd=str(_REPO), timeout=30,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return proc.stdout.strip()
|
|
|
|
|
|
def test_cssurlescape_doubles_backslashes_before_quotes():
|
|
js = textwrap.dedent(
|
|
f"""
|
|
const {{ _cssUrlEscape }} = await import('{_UTILS}');
|
|
console.log(JSON.stringify({{
|
|
backslash: _cssUrlEscape('a\\\\b'),
|
|
trailing: _cssUrlEscape('img\\\\'),
|
|
quote: _cssUrlEscape("a'b"),
|
|
dquote: _cssUrlEscape('a"b'),
|
|
}}));
|
|
"""
|
|
)
|
|
out = json.loads(_run(js))
|
|
# one backslash -> two; the escape for "'" is not itself re-escaped
|
|
assert out["backslash"] == r"a\\b"
|
|
assert out["trailing"] == "img\\\\" # 'img\' -> 'img\\'
|
|
assert out["quote"] == r"a\'b"
|
|
assert out["dquote"] == "a%22b"
|
|
|
|
|
|
def test_backslash_breakout_payload_cannot_close_the_url_string():
|
|
# Without the backslash-first escape, "x\" would render url('x\') and the
|
|
# trailing backslash escapes the closing quote -> breakout. After the fix the
|
|
# backslash is doubled, so the quote we add still terminates the string.
|
|
js = textwrap.dedent(
|
|
f"""
|
|
const {{ _cssUrlEscape, _calBgCss }} = await import('{_UTILS}');
|
|
const payload = 'x\\\\'; // a string ending in one backslash
|
|
console.log(JSON.stringify({{
|
|
esc: _cssUrlEscape(payload),
|
|
css: _calBgCss('bg:' + payload, 'var(--accent)'),
|
|
}}));
|
|
"""
|
|
)
|
|
out = json.loads(_run(js))
|
|
assert out["esc"] == "x\\\\" # doubled backslash
|
|
# The rendered declaration keeps the backslash doubled inside url('...').
|
|
assert "url('x\\\\')" in out["css"]
|
|
|
|
|
|
def test_calbgcss_escapes_quote_breakout():
|
|
js = textwrap.dedent(
|
|
f"""
|
|
const {{ _calBgCss }} = await import('{_UTILS}');
|
|
console.log(JSON.stringify(_calBgCss("bg:a'); X{{}}//", 'var(--accent)')));
|
|
"""
|
|
)
|
|
css = json.loads(_run(js))
|
|
# the injected single quote is escaped, so the url() string is not closed early
|
|
assert r"\'" in css
|
|
assert "url('a\\'); X{}//')" in css
|
|
|
|
|
|
def test_every_calendar_url_interpolation_is_escaped():
|
|
# Whole-file invariant: every CSS `url('${...}')` built in calendar.js must
|
|
# route its (CalDAV-syncable, untrusted) value through `_cssUrlEscape`. This
|
|
# is the guard that catches a *newly added* bg-image sink the centralization
|
|
# forgot - the failure mode that left calendar.js:2856 (edit-form color
|
|
# swatch) and :2953 (custom-dot preview) raw before this change.
|
|
src = _CALENDAR_JS.read_text(encoding="utf-8")
|
|
interps = re.findall(r"url\('\$\{([^}]*)\}'\)", src)
|
|
assert interps, "expected at least one url('${...}') interpolation in calendar.js"
|
|
unescaped = [expr for expr in interps if "_cssUrlEscape(" not in expr]
|
|
assert not unescaped, (
|
|
"bg-image url() interpolation(s) not routed through _cssUrlEscape: "
|
|
+ ", ".join(repr(e) for e in unescaped)
|
|
)
|