///|
fn attr_name_is_dangerous(name : StringView) -> Bool {
let normalized = @syn.lower_ascii(name)
normalized.has_prefix("on") ||
normalized == "srcdoc" ||
@ser.string_contains_char(normalized, ':')
}
///|
fn policy_allows_attribute(
policy : SanitizationPolicy,
tag_name : String,
attr_name : String,
) -> Bool {
if attr_name_is_dangerous(attr_name) {
return false
}
if tag_name == "a" && attr_name == "rel" && !policy.force_link_rel.is_empty() {
return true
}
match policy.allowed_attributes.get(tag_name) {
Some(attrs) if attrs.contains(attr_name) => true
_ =>
match policy.allowed_attributes.get("*") {
Some(attrs) => attrs.contains(attr_name)
None => false
}
}
}
///|
fn sanitize_attribute_value(
policy : SanitizationPolicy,
tag_name : String,
attr_name : String,
value : String,
effectively_foreign? : Bool = false,
) -> String? {
if effectively_foreign &&
is_foreign_url_function_like_attr(attr_name) &&
css_value_may_load_external_resource(value) {
sanitize_foreign_url_function_attribute_value(
policy, tag_name, attr_name, value,
)
} else if is_srcset_like_attr(attr_name) {
sanitize_srcset_attribute_value(policy, tag_name, attr_name, value)
} else if is_space_separated_url_list_attr(attr_name) {
sanitize_space_separated_url_list_attribute_value(
policy, tag_name, attr_name, value,
)
} else if is_single_url_like_attr(attr_name) {
sanitize_single_url_attribute_value(policy, tag_name, attr_name, value)
} else if attr_name == "style" {
sanitize_inline_style_value(policy, tag_name, value)
} else {
Some(value)
}
}
///|
/// Sanitize a single attribute value with this policy.
///
/// `tag_name` and `attr_name` should already be normalized to lowercase. URL
/// attributes, URL lists, foreign SVG-like URL function attributes, and inline
/// style values are validated through the policy. Returns `None` when the
/// attribute value should be dropped.
pub fn SanitizationPolicy::sanitize_attribute_value(
self : SanitizationPolicy,
tag_name : String,
attr_name : String,
value : String,
effectively_foreign? : Bool = false,
) -> String? {
sanitize_attribute_value(
self,
tag_name,
attr_name,
value,
effectively_foreign~,
)
}
///|
/// Sanitize the value of an inline `style` attribute.
///
/// Only declarations whose property names are in `allowed_css_properties` are
/// kept. Declarations that may load external resources are kept only when their
/// `url(...)` values pass the policy's URL rules. Returns `None` when no safe
/// declaration remains.
pub fn SanitizationPolicy::sanitize_inline_style_value(
self : SanitizationPolicy,
tag_name : String,
value : String,
) -> String? {
sanitize_inline_style_value(self, tag_name, value)
}
///|
/// Split HTML whitespace-separated tokens into a unique normalized list.
///
/// Tokens are trimmed, ASCII-lowercased, and appended to `tokens` only when
/// non-empty and not already present. Existing token order is preserved.
pub fn push_html_whitespace_tokens(
tokens : Array[String],
value : StringView,
) -> Unit {
let mut token_start = 0
let mut pos = 0
while pos < value.length() {
let ch = value.get_char(pos).unwrap()
if is_html_whitespace_char(ch) {
if token_start < pos {
let token = value[token_start:pos]
push_unique_string(tokens, @syn.lower_ascii(token.trim()))
}
pos += ch.utf16_len()
token_start = pos
} else {
pos += ch.utf16_len()
}
}
if token_start < value.length() {
let token = value[token_start:value.length()]
push_unique_string(tokens, @syn.lower_ascii(token.trim()))
}
}
///|
fn merge_forced_link_rel(
node : @dom.Node,
policy : SanitizationPolicy,
observer : SanitizeTransformObserver?,
) -> Unit {
if policy.force_link_rel.is_empty() {
return
}
let tokens : Array[String] = []
match node.attrs.get("rel") {
Some(Some(value)) => push_html_whitespace_tokens(tokens, value)
_ => ()
}
for token in policy.force_link_rel {
push_unique_string(tokens, token)
}
let normalized = tokens.join(" ")
if node.attrs.get("rel") != Some(Some(normalized)) {
sanitize_observer_event(
observer,
"Merged tokens into attribute 'rel' on ",
Some(node),
)
}
node.attrs["rel"] = Some(normalized)
}
///|
fn sanitize_meta_refresh_content(
node : @dom.Node,
policy : SanitizationPolicy,
observer : SanitizeTransformObserver?,
) -> Unit raise @core.HtmlError {
match node.attrs.get("http-equiv") {
Some(Some(value)) if @syn.lower_ascii(value[:].trim()) == "refresh" &&
node.attrs.contains("content") => {
sanitize_report_unsafe(
policy,
observer,
"Unsafe URL in attribute 'content' (meta refresh)",
Some(node),
)
node.attrs.remove("content")
}
_ => ()
}
}
///|
fn sanitize_forbidden_attr_pattern(attr_name : String) -> String? {
if attr_name == "srcdoc" {
Some("srcdoc")
} else if @ser.string_contains_char(attr_name, ':') {
Some("*:*")
} else if attr_name.has_prefix("on") {
Some("on*")
} else {
None
}
}
///|
fn sanitize_url_like_attr_for_report(
attr_name : String,
raw_value : String?,
effectively_foreign : Bool,
) -> Bool {
let foreign_url_function_loads = match raw_value {
Some(value) => css_value_may_load_external_resource(value)
None => false
}
is_single_url_like_attr(attr_name) ||
is_srcset_like_attr(attr_name) ||
is_space_separated_url_list_attr(attr_name) ||
(
effectively_foreign &&
is_foreign_url_function_like_attr(attr_name) &&
foreign_url_function_loads
)
}
///|
fn sanitize_dropped_attr_report_message(
policy : SanitizationPolicy,
tag_name : String,
attr_name : String,
raw_value : String?,
effectively_foreign : Bool,
) -> String {
if attr_name == "style" {
"Unsafe inline style in attribute 'style'"
} else if tag_name == "base" && attr_name == "href" {
"Unsafe URL in attribute 'href' (base tag)"
} else if sanitize_url_like_attr_for_report(
attr_name, raw_value, effectively_foreign,
) {
let suffix = match raw_value {
Some(_) =>
match policy.url_policy.lookup_rule(tag_name, attr_name) {
None => " (no rule)"
Some(_) => ""
}
None => ""
}
"Unsafe URL in attribute '" + attr_name + "'" + suffix
} else {
"Unsafe attribute '" + attr_name + "' on <" + tag_name + "> was dropped"
}
}
///|
fn sanitize_element_attributes(
node : @dom.Node,
policy : SanitizationPolicy,
observer : SanitizeTransformObserver?,
) -> Unit raise @core.HtmlError {
let tag_name = sanitize_node_name(node)
let effectively_foreign = node_is_effectively_foreign(node)
if policy.strip_invisible_unicode {
let stripped_attr_names : Array[String] = []
for key in node.attrs.keys() {
match node.attrs.get(key).unwrap() {
Some(raw_value) => {
let stripped = strip_invisible_unicode(raw_value)
if stripped != raw_value {
node.attrs[key] = Some(stripped)
stripped_attr_names.push(key)
}
}
_ => ()
}
}
if !stripped_attr_names.is_empty() {
sanitize_report_unsafe(
policy,
observer,
"Stripped invisible Unicode from attribute(s): " +
stripped_attr_names.join(", "),
Some(node),
)
}
}
let keys = node.attrs.keys().to_array()
for key in keys {
let attr_name = @syn.lower_ascii(key)
match sanitize_forbidden_attr_pattern(attr_name) {
Some(pattern) => {
sanitize_report_unsafe(
policy,
observer,
"Unsafe attribute '" +
attr_name +
"' (matched forbidden pattern '" +
pattern +
"')",
Some(node),
)
node.attrs.remove(key)
continue
}
None => ()
}
if !policy_allows_attribute(policy, tag_name, attr_name) {
sanitize_report_unsafe(
policy,
observer,
"Unsafe attribute '" + attr_name + "' (not allowed)",
Some(node),
)
node.attrs.remove(key)
} else {
match node.attrs.get(key).unwrap() {
Some(raw_value) => {
let value = raw_value
match
sanitize_attribute_value(
policy,
tag_name,
attr_name,
value,
effectively_foreign~,
) {
Some(cleaned) =>
if cleaned != raw_value || attr_name != key {
node.attrs.remove(key)
node.attrs[attr_name] = Some(cleaned)
}
None => {
sanitize_report_unsafe(
policy,
observer,
sanitize_dropped_attr_report_message(
policy,
tag_name,
attr_name,
Some(raw_value),
effectively_foreign,
),
Some(node),
)
node.attrs.remove(key)
}
}
}
None if attr_name == "href" ||
attr_name == "src" ||
attr_name == "style" ||
is_single_url_like_attr(attr_name) ||
is_srcset_like_attr(attr_name) ||
is_space_separated_url_list_attr(attr_name) => {
sanitize_report_unsafe(
policy,
observer,
sanitize_dropped_attr_report_message(
policy,
tag_name,
attr_name,
None,
effectively_foreign,
),
Some(node),
)
node.attrs.remove(key)
}
value =>
if attr_name != key {
node.attrs.remove(key)
node.attrs[attr_name] = value
}
}
}
}
if tag_name == "meta" {
sanitize_meta_refresh_content(node, policy, observer)
}
if tag_name == "a" {
merge_forced_link_rel(node, policy, observer)
}
}