Files
moli/moli-script/src/lib.rs
ldm0 a9830ab24b fix(csp): reject nonnonceable script elements
Apply the shared script nonceability check to parser scripts, buffered preloads and runtime script preparation. Suspicious script attribute names and values must not authorize execution merely because a nonce matches. Includes parser, preload and dynamic-preparation regressions.

The attribute checks follow the [CSP3 nonceability algorithm](https://www.w3.org/TR/CSP3/#is-element-nonceable), including case-insensitive `<link`, `<script` and `<style` matches and parser duplicate attributes.

Validation: all branch checks passed. Summary [ 115.690s] 18925 tests run: 18925 passed (9 slow), 16 skipped
2026-09-29 16:27:50 +08:00

301 lines
10 KiB
Rust

//! Shared script classification policy for parser and renderer code.
//!
//! This crate owns the renderer-neutral rules for script kind/mode decisions
//! so parser planning and runtime scheduling agree without copying
//! classification logic.
mod classify;
mod nonce;
mod scheduling;
pub use classify::{
classify_script_element, classify_script_kind, classify_script_mode,
classify_script_preparation, html_script_element_supports_type,
};
pub use nonce::script_element_nonce_is_nonceable;
pub use scheduling::{
ScriptElementClassification, ScriptElementClassificationInput, ScriptPreparationClassification,
ScriptPreparationClassificationInput, ScriptPreparationDisposition, ScriptSchedulingInput,
};
#[cfg(test)]
mod tests {
use moli_page_types::{ScriptKind, ScriptMode, ScriptSourceKind};
use crate::{
ScriptElementClassificationInput, ScriptPreparationClassificationInput,
ScriptPreparationDisposition, ScriptSchedulingInput, classify_script_element,
classify_script_kind, classify_script_mode, classify_script_preparation,
html_script_element_supports_type,
};
#[test]
fn classifies_module_and_dynamic_external_classic_modes() {
assert_eq!(
classify_script_mode(ScriptSchedulingInput {
parser_inserted: true,
allow_parser_blocking_modes: true,
force_async: false,
async_attribute_present: false,
defer_attribute_present: false,
kind: ScriptKind::Module,
source_kind: ScriptSourceKind::External,
}),
Some(ScriptMode::ModuleDefer)
);
assert_eq!(
classify_script_mode(ScriptSchedulingInput {
parser_inserted: false,
allow_parser_blocking_modes: false,
force_async: false,
async_attribute_present: false,
defer_attribute_present: false,
kind: ScriptKind::Classic,
source_kind: ScriptSourceKind::External,
}),
Some(ScriptMode::InOrder)
);
}
#[test]
fn classifies_script_type_attribute_values() {
assert_eq!(classify_script_kind(None), ScriptKind::Classic);
assert_eq!(classify_script_kind(Some("")), ScriptKind::Classic);
assert_eq!(classify_script_kind(Some(" ")), ScriptKind::DataBlock);
assert_eq!(
classify_script_kind(Some("text/javascript")),
ScriptKind::Classic
);
assert_eq!(
classify_script_kind(Some(" text/javascript\t")),
ScriptKind::Classic
);
assert_eq!(
classify_script_kind(Some("text/javascript; charset=utf-8")),
ScriptKind::DataBlock
);
assert_eq!(
classify_script_kind(Some("application/x-javascript")),
ScriptKind::Classic
);
assert_eq!(
classify_script_kind(Some("TEXT/JAVASCRIPT1.5")),
ScriptKind::Classic
);
assert_eq!(
classify_script_kind(Some("javascript")),
ScriptKind::DataBlock
);
assert_eq!(
classify_script_kind(Some("text/javascript\u{000B}")),
ScriptKind::DataBlock
);
assert_eq!(
classify_script_kind(Some("text/javascript\u{00A0}")),
ScriptKind::DataBlock
);
assert_eq!(
classify_script_kind(Some("text/javascript1.6")),
ScriptKind::DataBlock
);
assert_eq!(classify_script_kind(Some("module")), ScriptKind::Module);
assert_eq!(
classify_script_kind(Some("importmap")),
ScriptKind::ImportMap
);
assert_eq!(
classify_script_kind(Some("application/json")),
ScriptKind::DataBlock
);
}
#[test]
fn html_script_element_supports_uses_exact_supported_type_tokens() {
assert!(html_script_element_supports_type("classic"));
assert!(html_script_element_supports_type("module"));
assert!(html_script_element_supports_type("importmap"));
for unsupported in [
"",
" ",
" classic ",
"module ",
"Classic",
"Module",
"text/javascript",
"application/javascript",
"speculationrules",
"unsupported",
] {
assert!(
!html_script_element_supports_type(unsupported),
"{unsupported:?} should not be supported"
);
}
}
#[test]
fn classifies_language_only_script_attribute_values() {
assert_eq!(
classify_script_element(ScriptElementClassificationInput {
script_type: None,
language: Some("javascript"),
event: None,
for_attribute: None,
})
.kind,
ScriptKind::Classic
);
assert_eq!(
classify_script_element(ScriptElementClassificationInput {
script_type: None,
language: Some("JAVASCRIPT1.5"),
event: None,
for_attribute: None,
})
.kind,
ScriptKind::Classic
);
assert_eq!(
classify_script_element(ScriptElementClassificationInput {
script_type: None,
language: Some(" javascript "),
event: None,
for_attribute: None,
})
.kind,
ScriptKind::DataBlock
);
assert_eq!(
classify_script_element(ScriptElementClassificationInput {
script_type: None,
language: Some("javascript "),
event: None,
for_attribute: None,
})
.kind,
ScriptKind::DataBlock
);
assert_eq!(
classify_script_element(ScriptElementClassificationInput {
script_type: None,
language: Some("javascript\t"),
event: None,
for_attribute: None,
})
.kind,
ScriptKind::DataBlock
);
assert_eq!(
classify_script_element(ScriptElementClassificationInput {
script_type: None,
language: Some("javascript1.6"),
event: None,
for_attribute: None,
})
.kind,
ScriptKind::DataBlock
);
assert_eq!(
classify_script_element(ScriptElementClassificationInput {
script_type: Some("application/javascript"),
language: Some("unknown"),
event: None,
for_attribute: None,
})
.kind,
ScriptKind::Classic
);
}
#[test]
fn classifies_legacy_for_event_script_execution_gate() {
assert!(
!classify_script_element(ScriptElementClassificationInput {
script_type: None,
language: None,
event: Some(" onload() "),
for_attribute: Some(" window "),
})
.legacy_event_for_mismatch
);
assert!(
classify_script_element(ScriptElementClassificationInput {
script_type: None,
language: None,
event: Some("onclick"),
for_attribute: Some("window"),
})
.legacy_event_for_mismatch
);
assert!(
classify_script_element(ScriptElementClassificationInput {
script_type: None,
language: None,
event: Some("\u{a0}onload"),
for_attribute: Some("window"),
})
.legacy_event_for_mismatch
);
assert!(
!classify_script_element(ScriptElementClassificationInput {
script_type: None,
language: None,
event: Some("handler"),
for_attribute: None,
})
.legacy_event_for_mismatch
);
}
#[test]
fn preparation_classification_combines_kind_and_scheduling_policy() {
let classification = classify_script_preparation(ScriptPreparationClassificationInput {
element: ScriptElementClassificationInput {
script_type: Some("module"),
language: None,
event: None,
for_attribute: None,
},
parser_inserted: true,
allow_parser_blocking_modes: true,
force_async: false,
async_attribute_present: false,
defer_attribute_present: false,
source_kind: ScriptSourceKind::External,
});
assert_eq!(
classification.disposition,
ScriptPreparationDisposition::Module(ScriptMode::ModuleDefer)
);
assert!(!classification.legacy_event_for_mismatch);
}
#[test]
fn non_executable_script_kinds_do_not_receive_scheduling_modes() {
for (script_type, expected) in [
("importmap", ScriptPreparationDisposition::ImportMap),
("application/json", ScriptPreparationDisposition::DataBlock),
] {
let classification =
classify_script_preparation(ScriptPreparationClassificationInput {
element: ScriptElementClassificationInput {
script_type: Some(script_type),
language: None,
event: None,
for_attribute: None,
},
parser_inserted: true,
allow_parser_blocking_modes: true,
force_async: false,
async_attribute_present: true,
defer_attribute_present: true,
source_kind: ScriptSourceKind::External,
});
assert_eq!(classification.disposition, expected);
}
}
}