mirror of
https://github.com/lexmount/moli.git
synced 2026-10-03 16:00:45 +00:00
fix(csp): reject nonnonceable script elements
This commit is contained in:
@@ -30,7 +30,6 @@ content-security-policy/resource-hints/prefetch-generate-directives.html
|
||||
content-security-policy/resource-hints/prefetch-no-csp.html
|
||||
content-security-policy/sandbox/autoplay-disabled-by-csp.html
|
||||
content-security-policy/sandbox/window-reuse-sandboxed.html
|
||||
content-security-policy/script-src/non-nonceable-elements.html
|
||||
content-security-policy/script-src/nonce-enforce-blocked.html
|
||||
content-security-policy/script-src/script-src-trusted_types_eval_DedicatedWorker.html
|
||||
content-security-policy/securitypolicyviolation/blockeduri-inline.html
|
||||
|
||||
@@ -211,6 +211,7 @@ content-security-policy/script-src/hash-always-converted-to-utf-8/iso-8859-9.htm
|
||||
content-security-policy/script-src/hash-always-converted-to-utf-8/utf-8-lone-surrogate.html
|
||||
content-security-policy/script-src/hash-always-converted-to-utf-8/utf-8.html
|
||||
content-security-policy/script-src/javascript-window-open-blocked.html
|
||||
content-security-policy/script-src/non-nonceable-elements.html
|
||||
content-security-policy/script-src/script-src-1_1.html
|
||||
content-security-policy/script-src/script-src-1_10_1.html
|
||||
content-security-policy/script-src/script-src-1_2.html
|
||||
|
||||
+12
-1
@@ -373,6 +373,7 @@ pub(super) struct DocumentSink {
|
||||
// lines from the original document tail, so location fidelity only
|
||||
// degrades and never recovers for this parser session.
|
||||
source_positions_known: Cell<bool>,
|
||||
current_script_nonceable: Cell<Option<bool>>,
|
||||
html4_empty_system_id_quirks_override_pending: Cell<bool>,
|
||||
}
|
||||
|
||||
@@ -1457,10 +1458,15 @@ impl DocumentSink {
|
||||
Self {
|
||||
target: RefCell::new(target),
|
||||
source_positions_known: Cell::new(true),
|
||||
current_script_nonceable: Cell::new(None),
|
||||
html4_empty_system_id_quirks_override_pending: Cell::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn replace_current_script_nonceable(&self, nonceable: Option<bool>) -> Option<bool> {
|
||||
self.current_script_nonceable.replace(nonceable)
|
||||
}
|
||||
|
||||
pub(super) fn snapshot_parser_stream_document(&self) -> NativeDom {
|
||||
self.target.borrow().snapshot_parser_stream_document()
|
||||
}
|
||||
@@ -1681,7 +1687,12 @@ impl TreeSink for DocumentSink {
|
||||
attrs: Vec<Attribute>,
|
||||
flags: ElementFlags,
|
||||
) -> Self::Handle {
|
||||
self.target.borrow_mut().create_element(name, attrs, flags)
|
||||
self.target.borrow_mut().create_element(
|
||||
name,
|
||||
attrs,
|
||||
flags,
|
||||
self.current_script_nonceable.get(),
|
||||
)
|
||||
}
|
||||
|
||||
fn create_comment(&self, text: StrTendril) -> Self::Handle {
|
||||
|
||||
@@ -79,6 +79,7 @@ struct HtmlTreeSinkState {
|
||||
captured_blocking_stylesheet_nodes: HashSet<NativeNodeId>,
|
||||
captured_blocking_stylesheet_signatures: HashSet<DocumentBlockingStylesheetSignature>,
|
||||
finishing_tree_builder: bool,
|
||||
non_nonceable_parser_scripts: HashSet<NativeNodeId>,
|
||||
current_position: ParserSourcePosition,
|
||||
script_start_positions: HashMap<NativeNodeId, ParserSourcePosition>,
|
||||
}
|
||||
@@ -99,6 +100,7 @@ impl Default for HtmlTreeSinkState {
|
||||
captured_blocking_stylesheet_nodes: HashSet::new(),
|
||||
captured_blocking_stylesheet_signatures: HashSet::new(),
|
||||
finishing_tree_builder: false,
|
||||
non_nonceable_parser_scripts: HashSet::new(),
|
||||
current_position: ParserSourcePosition::default(),
|
||||
script_start_positions: HashMap::new(),
|
||||
}
|
||||
|
||||
@@ -3210,6 +3210,7 @@ impl ParserStreamHtmlTreeSinkTarget {
|
||||
name: QualName,
|
||||
attrs: Vec<Attribute>,
|
||||
flags: ElementFlags,
|
||||
script_nonceable: Option<bool>,
|
||||
) -> ParseHandle {
|
||||
let parser_flags = ParserElementFlags::from_html5ever(&flags);
|
||||
let template_contents_insertion = self.take_template_contents_insertion_hint();
|
||||
@@ -3287,6 +3288,7 @@ impl ParserStreamHtmlTreeSinkTarget {
|
||||
&attributes,
|
||||
has_null_custom_element_registry_attribute,
|
||||
parser_flags,
|
||||
script_nonceable,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3312,6 +3314,7 @@ impl ParserStreamHtmlTreeSinkTarget {
|
||||
&token_attributes,
|
||||
has_null_custom_element_registry_attribute,
|
||||
parser_flags,
|
||||
script_nonceable,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3326,12 +3329,16 @@ impl ParserStreamHtmlTreeSinkTarget {
|
||||
token_attributes: &[NativeAttribute],
|
||||
has_null_custom_element_registry_attribute: bool,
|
||||
parser_flags: ParserElementFlags,
|
||||
script_nonceable: Option<bool>,
|
||||
) -> ParseHandle {
|
||||
if has_null_custom_element_registry_attribute {
|
||||
self.pending_null_custom_element_registry_elements
|
||||
.push(node_id);
|
||||
}
|
||||
if is_script {
|
||||
if script_nonceable == Some(false) {
|
||||
self.state.non_nonceable_parser_scripts.insert(node_id);
|
||||
}
|
||||
self.state
|
||||
.script_start_positions
|
||||
.insert(node_id, self.state.current_position);
|
||||
@@ -3711,7 +3718,11 @@ pub(super) fn new_live_fragment_root_html_tree_sink_stream(
|
||||
|
||||
impl ParserPlanningReadView for ParserStreamHtmlTreeSinkTarget {
|
||||
fn parser_script_read(&self, node_id: NativeNodeId) -> Option<ParserScriptRead> {
|
||||
self.read_parser_script(node_id)
|
||||
let mut script = self.read_parser_script(node_id)?;
|
||||
if self.state.non_nonceable_parser_scripts.contains(&node_id) {
|
||||
script.fetch_metadata.nonce = None;
|
||||
}
|
||||
Some(script)
|
||||
}
|
||||
|
||||
fn script_handles(&self) -> Vec<NativeNodeId> {
|
||||
|
||||
@@ -9,7 +9,7 @@ use moli_fetch::FetchPriorityHint;
|
||||
use moli_page_types::{ScriptKind, ScriptMode, ScriptSourceKind};
|
||||
use moli_script::{
|
||||
ScriptElementClassificationInput, ScriptPreparationClassificationInput,
|
||||
ScriptPreparationDisposition, classify_script_preparation,
|
||||
ScriptPreparationDisposition, classify_script_preparation, script_element_nonce_is_nonceable,
|
||||
};
|
||||
|
||||
pub struct ParserScriptRead {
|
||||
@@ -45,6 +45,19 @@ fn parser_script_read_from_node(
|
||||
// make a harmless `nomodule` suppress execution or make `defer` change the
|
||||
// parser lane.
|
||||
let is_html_script = element.is_html_script();
|
||||
let nonce = element
|
||||
.cryptographic_nonce()
|
||||
.or_else(|| element.attribute("nonce"));
|
||||
let nonce = script_element_nonce_is_nonceable(
|
||||
nonce,
|
||||
false,
|
||||
element
|
||||
.attributes()
|
||||
.iter()
|
||||
.map(|attribute| (attribute.local_name(), attribute.value())),
|
||||
)
|
||||
.then_some(nonce)
|
||||
.flatten();
|
||||
Some(ParserScriptRead {
|
||||
parser_inserted: node.flags().parser_created(),
|
||||
parser_inserted_for_prepare: element.script_parser_inserted_for_prepare(),
|
||||
@@ -76,9 +89,7 @@ fn parser_script_read_from_node(
|
||||
element.attribute("referrerpolicy"),
|
||||
element.attribute("charset"),
|
||||
element.attribute("integrity"),
|
||||
element
|
||||
.cryptographic_nonce()
|
||||
.or_else(|| element.attribute("nonce")),
|
||||
nonce,
|
||||
element.attribute("fetchpriority"),
|
||||
)
|
||||
.with_parser_inserted(node.flags().parser_created()),
|
||||
|
||||
@@ -14,6 +14,7 @@ use html5ever::{
|
||||
};
|
||||
use markup5ever::TokenizerResult;
|
||||
use moli_dom::native::NativeNodeId;
|
||||
use moli_script::script_element_nonce_is_nonceable;
|
||||
|
||||
use super::{
|
||||
html::{DocumentSink, ParseHandle, ParserFinishDiscoverySignals, ParserInputQueue},
|
||||
@@ -82,6 +83,27 @@ impl EmbedderPausingTreeBuilder {
|
||||
token: Token,
|
||||
line_number: u64,
|
||||
) -> TokenSinkResult<ParseHandle> {
|
||||
let current_script_nonceable = match &token {
|
||||
Token::TagToken(tag)
|
||||
if tag.kind == TagKind::StartTag && tag.name.as_ref() == "script" =>
|
||||
{
|
||||
let nonce = tag
|
||||
.attrs
|
||||
.iter()
|
||||
.find(|attribute| {
|
||||
attribute.name.ns.is_empty() && attribute.name.local.as_ref() == "nonce"
|
||||
})
|
||||
.map(|attribute| attribute.value.as_ref());
|
||||
Some(script_element_nonce_is_nonceable(
|
||||
nonce,
|
||||
tag.had_duplicate_attributes,
|
||||
tag.attrs
|
||||
.iter()
|
||||
.map(|attribute| (attribute.name.local.as_ref(), attribute.value.as_ref())),
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let in_foreign_content = self
|
||||
.inner
|
||||
.adjusted_current_node_present_but_not_in_html_namespace();
|
||||
@@ -99,7 +121,14 @@ impl EmbedderPausingTreeBuilder {
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let previous_script_nonceable = self
|
||||
.inner
|
||||
.sink
|
||||
.replace_current_script_nonceable(current_script_nonceable);
|
||||
let result = self.inner.process_token(token, line_number);
|
||||
self.inner
|
||||
.sink
|
||||
.replace_current_script_nonceable(previous_script_nonceable);
|
||||
// Encoding notices are advisory for this already-decoded input. A tag
|
||||
// such as <link rel=stylesheet charset=utf-8> can also require an
|
||||
// embedder pause, which must be returned at this tag boundary. Leaving
|
||||
|
||||
@@ -1746,6 +1746,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_script_handoff_only_exposes_nonceable_nonces() {
|
||||
fn handoff_nonce(markup: &str) -> Option<String> {
|
||||
let mut stream = DocumentStream::new_parser_stream_for_testing(
|
||||
Url::parse("https://example.test/page.html").expect("test url"),
|
||||
);
|
||||
let outcome = stream.pump_parser_step(markup);
|
||||
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = outcome.result else {
|
||||
panic!("expected parser script handoff for {markup:?}");
|
||||
};
|
||||
let ParserScriptHandoff::BlockingClassic { script, .. } = *handoff else {
|
||||
panic!("expected blocking classic script for {markup:?}");
|
||||
};
|
||||
script.fetch_metadata.nonce
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
handoff_nonce("<script nonce=abc>safe()</script>"),
|
||||
Some("abc".to_owned()),
|
||||
"ordinary parser script nonce should remain usable"
|
||||
);
|
||||
for markup in [
|
||||
"<script attribute<script nonce=abc>blocked()</script>",
|
||||
"<script data-marker='value<StYlE' nonce=abc>blocked()</script>",
|
||||
"<script data-marker='value<LiNk' nonce=abc>blocked()</script>",
|
||||
"<script duplicate duplicate nonce=abc>blocked()</script>",
|
||||
"<svg><script duplicate duplicate nonce=abc>blocked()</script></svg>",
|
||||
] {
|
||||
assert_eq!(
|
||||
handoff_nonce(markup),
|
||||
None,
|
||||
"nonnonceable parser script must not expose its nonce: {markup:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_script_handoff_uses_html5ever_line_with_unknown_column_across_input_chunks() {
|
||||
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
||||
|
||||
@@ -630,7 +630,7 @@ impl TreeSink for XmlStreamDocumentSink {
|
||||
XmlStreamParseHandle::element(
|
||||
target
|
||||
.common
|
||||
.create_element(html_name, html_attrs, html_flags),
|
||||
.create_element(html_name, html_attrs, html_flags, None),
|
||||
element_name,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2192,16 +2192,8 @@ fn execute_committed_inline_classic_script(
|
||||
committed: crate::host::CommittedInlineClassicScript,
|
||||
) {
|
||||
let (node, host_script_handle, source) = committed.into_parts();
|
||||
let nonce = runtime
|
||||
.dom_host
|
||||
.node(node)
|
||||
.and_then(Node::as_element)
|
||||
.and_then(|element| {
|
||||
element
|
||||
.cryptographic_nonce()
|
||||
.or_else(|| element.attribute("nonce"))
|
||||
})
|
||||
.map(str::to_owned);
|
||||
let nonce =
|
||||
crate::host::script_element_nonce_for_csp(&runtime.dom_host, node).map(str::to_owned);
|
||||
let request = crate::content_security_policy::ContentSecurityPolicyScriptElementRequest {
|
||||
nonce: nonce.as_deref(),
|
||||
integrity: None,
|
||||
|
||||
@@ -47,6 +47,7 @@ pub(super) use self::scripts::{
|
||||
begin_prepared_document_write_script_start, build_runtime_prepared_script,
|
||||
cancel_runtime_script_start_admission, dispatch_script_event,
|
||||
finish_runtime_script_start_admission, plan_script_start, prepare_runtime_script_start_commit,
|
||||
script_element_nonce_for_csp,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(super) use self::scripts::{
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
},
|
||||
},
|
||||
};
|
||||
use moli_script::ScriptPreparationDisposition;
|
||||
use moli_script::{ScriptPreparationDisposition, script_element_nonce_is_nonceable};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -27,10 +27,7 @@ impl RuntimeScriptPreparationContext {
|
||||
node: NativeNodeId,
|
||||
) -> RuntimeScriptPreparationContext {
|
||||
let script_element = dom_host.node(node).and_then(Node::as_element);
|
||||
let nonce = script_element
|
||||
.and_then(|element| element.cryptographic_nonce())
|
||||
.map(str::to_owned)
|
||||
.or_else(|| dom_host.get_attribute(node, "nonce"));
|
||||
let nonce = script_element_nonce_for_csp(dom_host, node).map(str::to_owned);
|
||||
let parser_inserted =
|
||||
script_element.is_some_and(|element| element.script_parser_inserted_for_prepare());
|
||||
let owner_document = dom_host.owner_document_handle(node);
|
||||
@@ -56,6 +53,23 @@ impl RuntimeScriptPreparationContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn script_element_nonce_for_csp(dom_host: &DomHost, node: NativeNodeId) -> Option<&str> {
|
||||
let element = dom_host.node(node).and_then(Node::as_element)?;
|
||||
let nonce = element
|
||||
.cryptographic_nonce()
|
||||
.or_else(|| element.attribute("nonce"));
|
||||
script_element_nonce_is_nonceable(
|
||||
nonce,
|
||||
false,
|
||||
element
|
||||
.attributes()
|
||||
.iter()
|
||||
.map(|attribute| (attribute.local_name(), attribute.value())),
|
||||
)
|
||||
.then_some(nonce)
|
||||
.flatten()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum RuntimeScriptStartDecision {
|
||||
Skip {
|
||||
|
||||
@@ -886,6 +886,38 @@ fn runtime_preparation_capture_uses_live_document_base_url() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_preparation_only_captures_nonceable_script_nonces() {
|
||||
let url = Url::parse("https://example.test/").expect("test url should parse");
|
||||
let document = HtmlParser.parse(
|
||||
url.clone(),
|
||||
"<!doctype html><html><head></head><body></body></html>".to_owned(),
|
||||
);
|
||||
let mut dom_host = DomHost::from_dom(document);
|
||||
let document_state = HostDocumentState::new(url);
|
||||
|
||||
let safe_script = dom_host.create_element("script");
|
||||
assert!(dom_host.set_attribute(safe_script, "nonce", "abc"));
|
||||
assert_eq!(
|
||||
RuntimeScriptPreparationContext::capture(&dom_host, &document_state, safe_script)
|
||||
.fetch_metadata
|
||||
.nonce
|
||||
.as_deref(),
|
||||
Some("abc")
|
||||
);
|
||||
|
||||
let nonnonceable_script = dom_host.create_element("script");
|
||||
assert!(dom_host.set_attribute(nonnonceable_script, "nonce", "abc"));
|
||||
assert!(dom_host.set_attribute(nonnonceable_script, "data-marker", "value<ScRiPt"));
|
||||
assert_eq!(
|
||||
RuntimeScriptPreparationContext::capture(&dom_host, &document_state, nonnonceable_script,)
|
||||
.fetch_metadata
|
||||
.nonce,
|
||||
None,
|
||||
"markup-shaped attributes must make a dynamic script nonce nonnonceable"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connected_datablock_type_mutation_alone_does_not_prepare() {
|
||||
let url = Url::parse("https://example.test/").expect("test url should parse");
|
||||
|
||||
@@ -113,15 +113,7 @@ impl JsContextHost {
|
||||
source,
|
||||
} => {
|
||||
debug_assert!(load_delay_binding.is_none());
|
||||
let nonce = runtime
|
||||
.dom_host()
|
||||
.node(node)
|
||||
.and_then(crate::dom::native::Node::as_element)
|
||||
.and_then(|element| {
|
||||
element
|
||||
.cryptographic_nonce()
|
||||
.or_else(|| element.attribute("nonce"))
|
||||
})
|
||||
let nonce = crate::host::script_element_nonce_for_csp(runtime.dom_host(), node)
|
||||
.map(str::to_owned);
|
||||
let request =
|
||||
crate::content_security_policy::ContentSecurityPolicyScriptElementRequest {
|
||||
|
||||
@@ -1950,3 +1950,28 @@ fn parser_driver_finish_parser_blocking_pause_resets_insertion_scanner_state() {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffered_preload_scanner_clears_nonnonceable_script_nonces() {
|
||||
let final_url = Url::parse("https://example.test/docs/page.html").expect("test url");
|
||||
let requests = collect_preloadable_external_script_requests_from_html(
|
||||
&final_url,
|
||||
r#"
|
||||
<script src="/safe.js" nonce="abc"></script>
|
||||
<script src="/script.js" nonce="abc" data="value<script"></script>
|
||||
<script src="/style.js" nonce="abc" data="value<style"></script>
|
||||
<script src="/link.js" nonce="abc" data="value<link"></script>
|
||||
<script src="/duplicate.js" nonce="abc" duplicate duplicate></script>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_eq!(requests.len(), 5);
|
||||
assert_eq!(
|
||||
requests
|
||||
.iter()
|
||||
.map(|request| request.fetch_metadata.nonce.as_deref())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![Some("abc"), None, None, None, None],
|
||||
"speculative requests must use the same nonceability gate as parser execution"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use html5ever::{
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use moli_script::script_element_nonce_is_nonceable;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::network::ResourceRequestClient;
|
||||
@@ -1356,13 +1357,23 @@ impl HtmlPreloadScannerSink {
|
||||
if url.scheme() == "data" {
|
||||
return;
|
||||
}
|
||||
let nonce = html_attr_value(&tag.attrs, "nonce");
|
||||
let nonce = script_element_nonce_is_nonceable(
|
||||
nonce.as_deref(),
|
||||
tag.had_duplicate_attributes,
|
||||
tag.attrs
|
||||
.iter()
|
||||
.map(|attribute| (attribute.name.local.as_ref(), attribute.value.as_ref())),
|
||||
)
|
||||
.then_some(nonce.as_deref())
|
||||
.flatten();
|
||||
let mut requests = self.requests.borrow_mut();
|
||||
let fetch_metadata = crate::planning::ScriptFetchMetadata::from_script_attributes(
|
||||
html_attr_value(&tag.attrs, "crossorigin").as_deref(),
|
||||
html_attr_value(&tag.attrs, "referrerpolicy").as_deref(),
|
||||
html_attr_value(&tag.attrs, "charset").as_deref(),
|
||||
html_attr_value(&tag.attrs, "integrity").as_deref(),
|
||||
html_attr_value(&tag.attrs, "nonce").as_deref(),
|
||||
nonce,
|
||||
html_attr_value(&tag.attrs, "fetchpriority").as_deref(),
|
||||
);
|
||||
let Some(key) = BufferedScriptPreloadKey::new(url.clone(), kind_hint, &fetch_metadata)
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
//! 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,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/// Return whether a script element's nonce is safe to use for CSP matching.
|
||||
///
|
||||
/// CSP rejects nonces on parser elements with duplicate attributes or with an
|
||||
/// attribute name/value containing `<script`, `<style`, or `<link`, preventing
|
||||
/// dangling markup from borrowing a trusted nonce.
|
||||
pub fn script_element_nonce_is_nonceable<'a>(
|
||||
nonce: Option<&str>,
|
||||
had_duplicate_attributes: bool,
|
||||
attributes: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> bool {
|
||||
nonce.is_some()
|
||||
&& !had_duplicate_attributes
|
||||
&& attributes.into_iter().all(|(name, value)| {
|
||||
!contains_nonce_breaking_markup(name) && !contains_nonce_breaking_markup(value)
|
||||
})
|
||||
}
|
||||
|
||||
fn contains_nonce_breaking_markup(value: &str) -> bool {
|
||||
[
|
||||
b"<script".as_slice(),
|
||||
b"<style".as_slice(),
|
||||
b"<link".as_slice(),
|
||||
]
|
||||
.into_iter()
|
||||
.any(|needle| {
|
||||
value
|
||||
.as_bytes()
|
||||
.windows(needle.len())
|
||||
.any(|window| window.eq_ignore_ascii_case(needle))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::script_element_nonce_is_nonceable;
|
||||
|
||||
#[test]
|
||||
fn script_nonce_rejects_duplicate_or_markup_shaped_attributes() {
|
||||
assert!(script_element_nonce_is_nonceable(
|
||||
Some("abc"),
|
||||
false,
|
||||
[("nonce", "abc"), ("data-value", "safe")],
|
||||
));
|
||||
assert!(!script_element_nonce_is_nonceable(
|
||||
None,
|
||||
false,
|
||||
[("data-value", "safe")],
|
||||
));
|
||||
assert!(!script_element_nonce_is_nonceable(
|
||||
Some("abc"),
|
||||
true,
|
||||
[("nonce", "abc")],
|
||||
));
|
||||
|
||||
for attributes in [
|
||||
[("attribute<script", "safe"), ("nonce", "abc")],
|
||||
[("attribute<style", "safe"), ("nonce", "abc")],
|
||||
[("attribute", "value<ScRiPt"), ("nonce", "abc")],
|
||||
[("attribute", "value<StYlE"), ("nonce", "abc")],
|
||||
[("attribute", "value<LiNk"), ("nonce", "abc")],
|
||||
] {
|
||||
assert!(!script_element_nonce_is_nonceable(
|
||||
Some("abc"),
|
||||
false,
|
||||
attributes,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user