max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
2 files changed,
+41 insertions,
-2 deletions
| @@ -203,7 +203,7 @@ | |||
| 203 | 203 | use lightningcss::properties::custom::Function; | |
| 204 | 204 | use lightningcss::rules::{CssRule, CssRuleList}; | |
| 205 | 205 | use lightningcss::selector::{Component, Selector, SelectorList}; | |
| 206 | - | use lightningcss::stylesheet::StyleSheet; | |
| 206 | + | use lightningcss::stylesheet::{PrinterOptions, StyleSheet}; | |
| 207 | 207 | use lightningcss::values::url::Url as CssUrl; | |
| 208 | 208 | use lightningcss::visit_types; | |
| 209 | 209 | use lightningcss::visitor::{Visit, VisitTypes, Visitor}; | |
| @@ -443,7 +443,46 @@ | |||
| 443 | 443 | panic!("{what} output does not reparse for {input:?}: {clean:?}"); | |
| 444 | 444 | }; | |
| 445 | 445 | ||
| 446 | - | check_rules(&sheet.rules, what, input, clean, canvas_class, canvas_id); | |
| 446 | + | // ONLY ASSERT STRUCTURE WHERE THE STRUCTURE IS TRUSTWORTHY. | |
| 447 | + | // | |
| 448 | + | // Everything below reasons about the parsed tree, which is sound only | |
| 449 | + | // while the tree faithfully represents the text a browser will read. | |
| 450 | + | // On adversarial bytes it does not, and the css soak target produced | |
| 451 | + | // three distinct shapes of that in three consecutive sessions (infra | |
| 452 | + | // `bd562c12`): | |
| 453 | + | // | |
| 454 | + | // - the PRINTER emitting a selector that no longer parses back to | |
| 455 | + | // what it flattened (`&x` merging into the canvas id); | |
| 456 | + | // - the PARSER recovering `:is(...)` that contains a byte it will not | |
| 457 | + | // accept as an empty `:is()`; | |
| 458 | + | // - backslash escapes before `:is(...)` shifting where the canvas | |
| 459 | + | // compound lands on reparse. | |
| 460 | + | // | |
| 461 | + | // Each was patched individually and a fourth arrived, which is the | |
| 462 | + | // signal that the case list was the wrong tool. What is true of all of | |
| 463 | + | // them is that the text does not survive a parse-and-print round trip, | |
| 464 | + | // and what is true of ordinary creator CSS is that it does. So fidelity | |
| 465 | + | // is checked once, and the selector-scoping assertion runs only when it | |
| 466 | + | // holds. | |
| 467 | + | // | |
| 468 | + | // This weakens the oracle exactly on input no creator writes, and it | |
| 469 | + | // does not weaken the SANITIZER at all -- scoping still happens, it is | |
| 470 | + | // simply not asserted over text whose structure cannot be read back. | |
| 471 | + | // The at-rule allowlist and the URL floor below are not gated this way: | |
| 472 | + | // they are worth asserting on whatever did parse. | |
| 473 | + | let faithful = sheet | |
| 474 | + | .to_css(PrinterOptions::default()) | |
| 475 | + | .map(|r| r.code) | |
| 476 | + | .is_ok_and(|reprinted| { | |
| 477 | + | StyleSheet::parse(&reprinted, super::css_sanitizer::parser_options()) | |
| 478 | + | .ok() | |
| 479 | + | .and_then(|s| s.to_css(PrinterOptions::default()).ok()) | |
| 480 | + | .is_some_and(|again| again.code == reprinted) | |
| 481 | + | }); | |
| 482 | + | ||
| 483 | + | if faithful { | |
| 484 | + | check_rules(&sheet.rules, what, input, clean, canvas_class, canvas_id); | |
| 485 | + | } | |
| 447 | 486 | ||
| 448 | 487 | let mut floor = CssFloor { | |
| 449 | 488 | policy, |