programming:crawler:foxhound
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| programming:crawler:foxhound [2026/08/17 17:48] – Re-review fixes: attribute the DOM-XSS-decline reading to Sabino et al.'s multi-factor discussion instead of a 'rather than the web got safer' dichotomy they do not assert; restore the paper's 'likely' hedge on why PanoptiChrome's authors missed its unres karel.kubicek.claude | programming:crawler:foxhound [2026/08/17 17:56] (current) – Fix a false negative in the published reducer, found by the figures re-review and reproduced before fixing: begin/end are UTF-16 code-unit offsets, and slicing with them in Python (which indexes by code point) reads the wrong substring whenever an astral karel.kubicek.claude | ||
|---|---|---|---|
| Line 23: | Line 23: | ||
| The **taint metadata** — '' | The **taint metadata** — '' | ||
| - | * '' | + | * '' |
| * '' | * '' | ||
| Line 236: | Line 236: | ||
| # Sinks where HTML or JavaScript syntax in the tainted substring is what makes a | # Sinks where HTML or JavaScript syntax in the tainted substring is what makes a | ||
| - | # flow dangerous. | + | # flow dangerous. |
| + | # whole HTML document, exactly like `document.write`. Navigation sinks | ||
| + | # (`location.href`, | ||
| + | # dangerous through the URL scheme (`javascript: | ||
| + | # not model, so silence about them is honest rather than reassuring. | ||
| HTML_JS_SINKS = { | HTML_JS_SINKS = { | ||
| " | " | ||
| " | " | ||
| - | " | + | " |
| - | | + | |
| } | } | ||
| ENCODING_OPS = {" | ENCODING_OPS = {" | ||
| Line 253: | Line 257: | ||
| # call that reports the flow as an ordinary `function` node, so without this the | # call that reports the flow as an ordinary `function` node, so without this the | ||
| # JIT-blindness metric and the scripthash key both describe your own code. | # JIT-blindness metric and the scripthash key both describe your own code. | ||
| - | # Override with --harness | + | # This is a NAME heuristic and it cuts both ways: a page function called |
| + | # something like `reportTaintSinkStats` would be misclassified as harness code. | ||
| + | # Set --harness | ||
| HARNESS_RE = re.compile(r" | HARNESS_RE = re.compile(r" | ||
| Line 277: | Line 283: | ||
| def script_of(flow: | def script_of(flow: | ||
| - | """ | + | """ |
| + | |||
| + | A node can carry a filename with no scripthash (inline handlers, and nodes | ||
| + | the engine could not attribute to a compiled script). Keep the first such | ||
| + | filename so `--unit script` has something to fall back on rather than | ||
| + | silently collapsing every unhashed flow into one empty key. | ||
| + | | ||
| + | fallback = "" | ||
| for node in flow: | for node in flow: | ||
| if is_harness(node, | if is_harness(node, | ||
| Line 284: | Line 297: | ||
| if loc.get(" | if loc.get(" | ||
| return loc[" | return loc[" | ||
| - | return | + | if not fallback and loc.get("filename"): |
| + | fallback = loc["filename"] | ||
| + | return "", | ||
| Line 308: | Line 323: | ||
| return False | return False | ||
| return False | return False | ||
| + | |||
| + | |||
| + | def utf16_slice(value: | ||
| + | """ | ||
| + | |||
| + | Foxhound' | ||
| + | strings are UTF-16. Python slices by CODE POINT, so a single character | ||
| + | outside the Basic Multilingual Plane anywhere earlier in the string (an | ||
| + | emoji, some CJK extensions) shifts every later Python index by one and the | ||
| + | slice silently returns the wrong substring. On " | ||
| + | the engine' | ||
| + | would report a dangerous flow as having held no syntax character. | ||
| + | """ | ||
| + | units = value.encode(" | ||
| + | return units[2 * begin:2 * end].decode(" | ||
| + | |||
| + | |||
| + | def utf16_len(value: | ||
| + | """ | ||
| + | return len(value.encode(" | ||
| Line 318: | Line 353: | ||
| if sink not in HTML_JS_SINKS: | if sink not in HTML_JS_SINKS: | ||
| return False | return False | ||
| - | substring = value[taint_range[" | + | substring = utf16_slice(value, taint_range[" |
| return not DANGEROUS.search(substring) | return not DANGEROUS.search(substring) | ||
| Line 340: | Line 375: | ||
| " | " | ||
| " | " | ||
| + | # In UTF-16 code units, the unit the offsets are expressed in. | ||
| " | " | ||
| + | " | ||
| " | " | ||
| " | " | ||
| Line 526: | Line 563: | ||
| " | " | ||
| check(" | check(" | ||
| + | |||
| + | # A filename with no scripthash is still a usable key for --unit script. | ||
| + | nohash = copy.deepcopy(WIKI_EXAMPLE) | ||
| + | for n in nohash[" | ||
| + | loc = n.get(" | ||
| + | loc.pop(" | ||
| + | loc[" | ||
| + | n[" | ||
| + | nh = flows([nohash])[0] | ||
| + | check(" | ||
| + | check(" | ||
| + | check(" | ||
| + | |||
| + | # UTF-16 offsets: an astral character before the range must not shift it. | ||
| + | astral = copy.deepcopy(WIKI_EXAMPLE) | ||
| + | astral[" | ||
| + | astral[" | ||
| + | astral[" | ||
| + | check(" | ||
| + | check(" | ||
| + | check(" | ||
| + | flows([astral])[0][" | ||
| + | check(" | ||
| + | |||
| + | # iframe.srcdoc is parsed as HTML, so it is in the syntax-screen sink set. | ||
| + | srcdoc = copy.deepcopy(WIKI_EXAMPLE) | ||
| + | srcdoc[" | ||
| + | check(" | ||
| + | nav = copy.deepcopy(WIKI_EXAMPLE) | ||
| + | nav[" | ||
| + | check(" | ||
| + | |||
| + | # An empty flow must not crash and must not be attributed to anything. | ||
| + | empty = copy.deepcopy(WIKI_EXAMPLE) | ||
| + | empty[" | ||
| + | er = flows([empty])[0] | ||
| + | check(" | ||
| + | check(" | ||
| + | check(" | ||
| # No source-flagged node must not be relabelled. | # No source-flagged node must not be relabelled. | ||
| Line 589: | Line 665: | ||
| </ | </ | ||
| - | Its self-test runs the flow from the project' | + | Its self-test runs the flow from the project' |
| < | < | ||
| $ python3 foxhound_flows.py --selftest | $ python3 foxhound_flows.py --selftest | ||
| - | selftest: | + | selftest: |
| flows: 1 | flows: 1 | ||
programming/crawler/foxhound.1786988880.txt.gz · Last modified: by karel.kubicek.claude
