{"id":35298214,"url":"https://github.com/niklak/dom_sanitizer","last_synced_at":"2026-04-06T01:32:04.388Z","repository":{"id":286796286,"uuid":"960312083","full_name":"niklak/dom_sanitizer","owner":"niklak","description":"Flexible HTML sanitization for Rust — build policies and sanitize documents easily.","archived":false,"fork":false,"pushed_at":"2026-01-27T08:06:06.000Z","size":333,"stargazers_count":4,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-27T20:45:45.709Z","etag":null,"topics":["html","html-sanitization","sanitization"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/niklak.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-04-04T08:06:38.000Z","updated_at":"2026-01-27T08:02:45.000Z","dependencies_parsed_at":"2025-04-24T10:26:39.031Z","dependency_job_id":"ed6d0677-b884-4280-8605-672d6fd18957","html_url":"https://github.com/niklak/dom_sanitizer","commit_stats":null,"previous_names":["niklak/dom_sanitizer"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/niklak/dom_sanitizer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklak%2Fdom_sanitizer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklak%2Fdom_sanitizer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklak%2Fdom_sanitizer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklak%2Fdom_sanitizer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/niklak","download_url":"https://codeload.github.com/niklak/dom_sanitizer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklak%2Fdom_sanitizer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31456637,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-05T21:22:52.476Z","status":"ssl_error","status_checked_at":"2026-04-05T21:22:51.943Z","response_time":75,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["html","html-sanitization","sanitization"],"created_at":"2025-12-30T16:40:16.910Z","updated_at":"2026-04-06T01:32:04.383Z","avatar_url":"https://github.com/niklak.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# dom_sanitizer\n\n[![Crates.io version](https://img.shields.io/crates/v/dom_sanitizer.svg?style=flat)](https://crates.io/crates/dom_sanitizer)\n[![Download](https://img.shields.io/crates/d/dom_sanitizer.svg?style=flat)](https://crates.io/crates/dom_sanitizer)\n[![docs.rs docs](https://img.shields.io/badge/docs-latest-blue.svg?style=flat)](https://docs.rs/dom_sanitizer)\n[![codecov](https://codecov.io/github/niklak/dom_sanitizer/graph/badge.svg?token=Y3EN2HE4SR)](https://codecov.io/github/niklak/dom_sanitizer)\n\n[![Rust CI](https://github.com/niklak/dom_sanitizer/actions/workflows/rust.yml/badge.svg)](https://github.com/niklak/dom_sanitizer/actions/workflows/rust.yml)\n\n\u003e Flexible HTML sanitization for Rust — build policies and sanitize documents easily.\n\n\u003e Built on top of [dom_query](https://github.com/niklak/dom_query)\n\n\n## Motivation\n\nEnsuring the safety and clarity of HTML content is essential for modern web applications. Beyond defending against threats like Cross-Site Scripting (XSS), it's often necessary to remove unwanted or irrelevant markup—such as ads, tracking elements, or redundant tags. While many sanitization libraries exist, they often fall short in *usability*, *flexibility*, or *extensibility*.\n\n- **Ease of Use**: This library offers a simple and ergonomic API, making it easy to integrate into any Rust project with minimal setup.\n- **Flexibility**: Configure sanitization using permissive or restrictive policies — choose whether to allow or deny content by default, and fine-tune what elements and attributes are excluded from the base policy.\n- **Extensibility**: A plugin-style policy system lets you implement custom checkers to exclude specific elements or attributes from the base policy. This enables advanced logic, such as applying regular expressions to control content inclusion, or skipping entire namespaces like SVG or MathML during sanitization.\n- **Thread Safety**: Sanitization policies are immutable after construction and can be safely shared across threads, making them ideal for use in concurrent environments.\n\n\n## Examples\n\nA policy may have either a `Restrictive` or a `Permissive` directive.\nThe policy directive defines the sanitization behavior.\n\nIf the directive is `Permissive`, it allows all elements and attributes by default.\nIf the directive is `Restrictive`, it denies all elements except `html`, `head`, and `body`, and denies all attributes by default.\n\nWhen you *exclude* elements in a `Restrictive` policy, it means that only those elements will be kept in the DOM. The same applies to attributes.\n\nWhen you *exclude* elements in a `Permissive` policy, it means that those elements will be removed from the DOM. The same applies to attributes.\n\nBoth policies may remove given elements and their descendants from the DOM. This is useful for removing elements like `\u003cscript\u003e` or `\u003cstyle\u003e`.\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eA Basic PermissivePolicy\u003c/b\u003e\u003c/summary\u003e\n\n```rust\nuse dom_sanitizer::PermissivePolicy;\nuse dom_query::Document;\n\n// `PermissivePolicy\u003c'a\u003e`, as well as `AllowAllPolicy`, is an alias for `Policy\u003c'a, Permissive\u003e`\nlet policy = PermissivePolicy::builder()\n    // Disallow `div` elements\n    .exclude_elements(\u0026[\"div\"])\n    // Disallow `role` attribute globally\n    .exclude_attrs(\u0026[\"role\"])\n    // Disallow `href` attribute for `a` elements\n    .exclude_element_attrs(\"a\", \u0026[\"href\"])\n    // remove `style` elements including their descendants (elements, text, comments)\n    .remove_elements(\u0026[\"style\"])\n    .build();\n\nlet contents: \u0026str = r#\"\n    \u003c!DOCTYPE html\u003e\n    \u003chtml\u003e\n        \u003chead\u003e\u003ctitle\u003eTest\u003c/title\u003e\u003c/head\u003e\n        \u003cbody\u003e\n            \u003cstyle\u003e\n                p { border-bottom: 2px solid black; }\n            \u003c/style\u003e\n            \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe first paragraph contains \u003ca href=\"/first\" role=\"link\"\u003ethe first link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe second paragraph contains \u003ca href=\"/second\" role=\"link\"\u003ethe second link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe third paragraph contains \u003ca href=\"/third\" role=\"link\"\u003ethe third link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003cp id=\"highlight\" role=\"paragraph\"\u003e\u003cmark\u003ehighlighted text\u003c/mark\u003e, \u003cb\u003ebold text\u003c/b\u003e\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003c/div\u003e\n        \u003c/body\u003e\n    \u003c/html\u003e\"#;\n\nlet doc = Document::from(contents);\npolicy.sanitize_document(\u0026doc);\n\n// After sanitization:\n\n// `style` removed from the DOM\nassert!(!doc.select(\"style\").exists());\n// - No `div` elements remain\nassert!(!doc.select(\"div\").exists());\n// - No `role` attributes remain\nassert!(!doc.select(\"[role]\").exists());\n// - `p` elements are preserved\nassert_eq!(doc.select(\"p\").length(), 4);\n// - `a` elements are preserved but without `href` attributes\nassert_eq!(doc.select(\"a\").length(), 3);\nassert_eq!(doc.select(\"a[href]\").length(), 0);\n```\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eA Basic RestrictivePolicy\u003c/b\u003e\u003c/summary\u003e\n\n```rust\nuse dom_sanitizer::RestrictivePolicy;\nuse dom_query::Document;\n\n\n// `RestrictivePolicy\u003c'a\u003e`, as well as `DenyAllPolicy`, is an alias for `Policy\u003c'a, Restrictive\u003e` \n\n// Create a new restrictive policy with builder\nlet policy = RestrictivePolicy::builder()\n    // allow only `p` and `a` elements\n    .exclude_elements(\u0026[\"p\", \"a\"])\n    // allow `href` attribute for `a` elements\n    .exclude_element_attrs(\"a\", \u0026[\"href\"])\n    // remove `style` elements including their descendants (elements, text, comments)\n    .remove_elements(\u0026[\"style\"])\n    .build();\n\nlet contents: \u0026str = r#\"\n    \u003c!DOCTYPE html\u003e\n    \u003chtml\u003e\n        \u003chead\u003e\u003ctitle\u003eTest\u003c/title\u003e\u003c/head\u003e\n        \u003cbody\u003e\n            \u003cstyle\u003e\n                p { border-bottom: 2px solid black; }\n            \u003c/style\u003e\n            \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe first paragraph contains \u003ca href=\"/first\" role=\"link\"\u003ethe first link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe second paragraph contains \u003ca href=\"/second\" role=\"link\"\u003ethe second link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe third paragraph contains \u003ca href=\"/third\" role=\"link\"\u003ethe third link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003cp id=\"highlight\" role=\"paragraph\"\u003e\u003cmark\u003ehighlighted text\u003c/mark\u003e, \u003cb\u003ebold text\u003c/b\u003e\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003c/div\u003e\n        \u003c/body\u003e\n    \u003c/html\u003e\"#;\n\nlet doc = dom_query::Document::from(contents);\npolicy.sanitize_document(\u0026doc);\n\n// After sanitization:\n\n// `style` removed from the DOM\nassert!(!doc.select(\"style\").exists());\n\n// No `div` elements in the DOM\nassert!(!doc.select(\"div\").exists());\n// No `role` attributes in the DOM\nassert!(!doc.select(\"[role]\").exists());\n// But we still have `p` elements\nassert_eq!(doc.select(\"p\").length(), 4);\n// as well as `a` elements with `href` attributes\nassert_eq!(doc.select(\"a[href]\").length(), 3);\n\n// `html`, `head`, and `body` elements are always kept\nassert!(doc.select(\"html\").exists());\nassert!(doc.select(\"head\").exists());\nassert!(doc.select(\"body\").exists());\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eUsing Presets \u0026 Combining Policies\u003c/b\u003e\u003c/summary\u003e\n\nThis example demonstrates how to combine multiple preset policies into one.\n\n```rust\n// \nuse dom_sanitizer::{preset, RestrictivePolicy};\n\n// Create a new restrictive policy using the builder\nlet _policy = RestrictivePolicy::builder()\n    // Allow global attributes from the `global_attr_policy` preset —\n    // includes `class`, `id`, `role`, `dir`, `lang`, and `title`\n    .merge(preset::global_attr_policy())\n    // Allow list elements from the `list_policy` preset —\n    // includes `ul`, `ol`, and `li`\n    .merge(preset::list_policy())\n    // Allow table-related elements from the `table_policy` preset —\n    // includes `table`, `caption`, `colgroup`, `col`, `th`, `thead`, `tbody`, `tr`, `td`, and `tfoot`\n    .merge(preset::table_policy())\n    // Allow table-related attributes from the `table_attr_policy` preset\n    .merge(preset::table_attr_policy())\n    // Allow inline formatting elements from the `highlight_policy` preset —\n    // includes `b`, `del`, `em`, `i`, `ins`, `mark`, `s`, `small`, `strong`, and `u`\n    .merge(preset::highlight_policy())\n    // You can still apply custom rules in addition to using preset policies\n    .exclude_elements(\u0026[\"h1\", \"h2\", \"h3\", \"a\", \"svg\"])\n    .exclude_elements(\u0026[\"meta\", \"link\"])\n    .exclude_element_attrs(\"meta\", \u0026[\"charset\", \"name\", \"content\"])\n    .exclude_attrs(\u0026[\"translate\"])\n    .exclude_element_attrs(\"a\", \u0026[\"href\"])\n    .remove_elements(\u0026[\"style\", \"script\"])\n    .build();\n```\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eHTML Sanitization\u003c/b\u003e\u003c/summary\u003e\n\n```rust\nuse dom_sanitizer::PermissivePolicy;\nuse dom_query::Document;\n\n\n// Create a new permissive policy with builder\nlet policy = PermissivePolicy::builder()\n    // remove `style` elements including their descendants (elements, text, comments)\n    .remove_elements(\u0026[\"style\"])\n    .build();\n\nlet contents: \u0026str = r#\"\n    \u003c!DOCTYPE html\u003e\n    \u003chtml\u003e\n        \u003chead\u003e\u003ctitle\u003eTest\u003c/title\u003e\u003c/head\u003e\n        \u003cbody\u003e\n            \u003cstyle\u003e\n                p { border-bottom: 2px solid black; }\n            \u003c/style\u003e\n            \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe first paragraph contains \u003ca href=\"/first\" role=\"link\"\u003ethe first link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003c/div\u003e\n        \u003c/body\u003e\n    \u003c/html\u003e\"#;\n\nassert!(contents.contains(\"\u003cstyle\u003e\"));\nassert!(contents.contains(r#\"p { border-bottom: 2px solid black; }\"#));\n\nlet html = policy.sanitize_html(contents);\n\nassert!(!html.contains(\"\u003cstyle\u003e\"));\nassert!(!html.contains(r#\"p { border-bottom: 2px solid black; }\"#));\n\n```\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eSharing A `Policy` Across Threads\u003c/b\u003e\u003c/summary\u003e\n\n```rust\nuse std::sync::Arc;\n\nuse dom_sanitizer::preset::table_policy;\nuse dom_sanitizer::DenyAllPolicy;\n\nlet policy = DenyAllPolicy::builder()\n    // Allow table elements\n    .merge(table_policy())\n    .remove_elements(\u0026[\"style\"])\n    // `html`, `head`, and `body` are always kept\n    .build();\n    \nlet shared_policy = Arc::new(policy);\n\nlet mut handles = Vec::new();\nfor _ in 0..4 {\n    let policy = shared_policy.clone();\n    let handle = std::thread::spawn(move || {\n        let contents: \u0026str = include_str!(\"../test-pages/table.html\");\n        let doc = dom_query::Document::from(contents);\n        policy.sanitize_document(\u0026doc);\n        assert!(doc.select(\"table tr \u003e td\").exists());\n        assert!(!doc.select(\"style\").exists());\n    });\n    handles.push(handle);\n}\n\nfor handle in handles {\n    handle.join().expect(\"worker thread panicked\");\n}\n\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e \n\u003csummary\u003e\u003cb\u003eSanitizing Only Nodes Inside a Selection\u003c/b\u003e\u003c/summary\u003e\n\n`dom_sanitizer` allows you to apply sanitization only to nodes within a selected set of nodes. \nThis is useful when you want to sanitize specific parts of a document without affecting the rest of it.\n\n```rust\nuse dom_sanitizer::RestrictivePolicy;\nuse dom_query::Document;\n\nlet contents: \u0026str = r#\"\n    \u003c!DOCTYPE html\u003e\n    \u003chtml\u003e\n        \u003chead\u003e\u003ctitle\u003eTest\u003c/title\u003e\u003c/head\u003e\n        \u003cbody\u003e\n            \u003cstyle\u003e\n                p { border-bottom: 2px solid black; }\n            \u003c/style\u003e\n            \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe first paragraph contains \u003ca href=\"/first\" role=\"link\"\u003ethe first link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe second paragraph contains \u003ca href=\"/second\" role=\"link\"\u003ethe second link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe third paragraph contains \u003ca href=\"/third\" role=\"link\"\u003ethe third link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003cp id=\"highlight\" role=\"paragraph\"\u003e\u003cmark\u003ehighlighted text\u003c/mark\u003e, \u003cb\u003ebold text\u003c/b\u003e\u003c/p\u003e\u003c/div\u003e\n            \u003cdiv\u003e\u003c/div\u003e\n        \u003c/body\u003e\n    \u003c/html\u003e\"#;\nlet policy = RestrictivePolicy::builder().build();\nlet doc = Document::from(contents);\n\n// Before sanitization, there are no paragraphs that contain only text content\nassert!(!doc.select(\"p:only-text\").exists());\n\nlet sel = doc.select(\"p\");\npolicy.sanitize_selection(\u0026sel);\n\n// After sanitization, all paragraphs contain only text content\nassert_eq!(doc.select(\"p:only-text\").length(), 4);\n\n```\n\u003c/details\u003e\n\n---\nWhen the basic `Policy` capabilities are not enough, `PluginPolicy` allows\nyou to define a fully customized sanitization policy.\nLike `Policy`, `PluginPolicy` can be either `Restrictive` or `Permissive`.\n\nTo exclude elements from sanitization or remove them completely,\nimplement the `NodeChecker` trait.\nTo exclude attributes from sanitization, implement the `AttrChecker` trait.\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eA Basic Permissive Plugin Policy\u003c/b\u003e\u003c/summary\u003e\n\n```rust\nuse dom_sanitizer::plugin_policy::{AttrChecker, NodeChecker, PluginPolicy};\nuse dom_sanitizer::Permissive;\n\nuse dom_query::NodeRef;\n\nuse html5ever::{local_name, LocalName};\n\n/// Matches nodes with a specific local name.\npub struct MatchLocalName(pub LocalName);\nimpl NodeChecker for MatchLocalName {\n    fn is_match(\u0026self, node: \u0026NodeRef) -\u003e bool {\n        node.qual_name_ref()\n            .map_or(false, |qual_name| self.0 == qual_name.local)\n    }\n}\n\n/// Matches a suspicious attributes that starts with `on` but is not `onclick`.\nstruct SuspiciousAttr;\nimpl AttrChecker for SuspiciousAttr {\n    fn is_match_attr(\u0026self, _node: \u0026NodeRef, attr: \u0026html5ever::Attribute) -\u003e bool {\n        let attr_name = attr.name.local.as_ref().to_ascii_lowercase();\n        attr_name != \"onclick\" \u0026\u0026 attr_name.starts_with(\"on\")\n    }\n}\n\n// Creates a permissive policy that allows all elements and attributes by default,\n// excluding those matched by custom checkers.\nlet policy: PluginPolicy\u003cPermissive\u003e = PluginPolicy::builder()\n    // `div` elements become disallowed and will be stripped from the DOM\n    .exclude(MatchLocalName(local_name!(\"div\")))\n    // `style` elements will be completely removed from the DOM\n    .remove(MatchLocalName(local_name!(\"style\")))\n    // Attributes that start with `on` and are not `onclick` will be removed\n    .exclude_attr(SuspiciousAttr)\n    .build();\n\nlet contents: \u0026str = r#\"\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\u003ctitle\u003eTest Ad Block\u003c/title\u003e\u003c/head\u003e\n    \u003cbody\u003e\n        \u003cstyle\u003e@keyframes x{}\u003c/style\u003e\n        \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe first paragraph contains \u003ca href=\"/first\" role=\"link\"\u003ethe first link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n        \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe second paragraph contains \u003ca href=\"/second\" role=\"link\"\u003ethe second link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n        \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe third paragraph contains \u003ca href=\"/third\" role=\"link\"\u003ethe third link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n        \u003cdiv\u003e\u003cp id=\"highlight\" role=\"paragraph\"\u003e\u003cmark\u003ehighlighted text\u003c/mark\u003e, \u003cb\u003ebold text\u003c/b\u003e\u003c/p\u003e\u003c/div\u003e\n        \u003cdiv\u003e\n            \u003ca style=\"animation-name:x\" onanimationend=\"alert(1)\"\u003e\u003c/a\u003e\n        \u003c/div\u003e\n    \u003c/body\u003e\n\u003c/html\u003e\"#;\n\nlet doc = dom_query::Document::from(contents);\n\npolicy.sanitize_document(\u0026doc);\n\n// The `style` element is removed from the DOM\nassert!(!doc.select(\"style\").exists());\n// All `div` elements are removed from the DOM\nassert!(!doc.select(\"div\").exists());\n// All 4 `\u003cp\u003e` elements remain\nassert_eq!(doc.select(\"p\").length(), 4);\n// Suspicious attributes removed (e.g., `onanimationend`)\nassert!(!doc.select(\"a[onanimationend]\").exists());\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eA Basic Restrictive Plugin Policy\u003c/b\u003e\u003c/summary\u003e\n\nThis example is using some predefined checkers from the `preset` module.\n\n```rust\nuse dom_sanitizer::plugin_policy::{NodeChecker, PluginPolicy};\nuse dom_sanitizer::plugin_policy::preset;\nuse dom_sanitizer::Restrictive;\nuse dom_query::NodeRef;\n\nuse html5ever::local_name;\n\nstruct ExcludeOnlyHttps;\nimpl NodeChecker for ExcludeOnlyHttps {\n    fn is_match(\u0026self, node: \u0026NodeRef) -\u003e bool {\n        node.has_name(\"a\")\n            \u0026\u0026 node\n                .attr(\"href\")\n                .map_or(false, |href| href.starts_with(\"https://\"))\n    }\n}\n\n// Creates a restrictive policy that allows only specific elements and attributes\n// which are explicitly excluded from sanitization with custom checkers.\nlet policy: PluginPolicy\u003cRestrictive\u003e = PluginPolicy::builder()\n    // Allow `a` elements only if their `href` starts with \"https://\"\n    .exclude(ExcludeOnlyHttps)\n    // Allow `title`, `p`, `mark`, and `b` elements\n    .exclude(preset::LocalNamesMatcher::new(\u0026[\n        \"title\", \"p\", \"mark\", \"b\",\n    ]))\n    // `html`, `head`, and `body` are always kept\n    .build();\n\nlet contents: \u0026str = r#\"\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\u003ctitle\u003eTest Ad Block\u003c/title\u003e\u003c/head\u003e\n    \u003cbody\u003e\n        \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe first paragraph contains \u003ca href=\"/first\" role=\"link\"\u003ethe first link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n        \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe second paragraph contains \u003ca href=\"/second\" role=\"link\"\u003ethe second link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n        \u003cdiv\u003e\u003cp role=\"paragraph\"\u003eThe third paragraph contains \u003ca href=\"/third\" role=\"link\"\u003ethe third link\u003c/a\u003e.\u003c/p\u003e\u003c/div\u003e\n        \u003cdiv\u003e\u003cp id=\"highlight\" role=\"paragraph\"\u003e\u003cmark\u003ehighlighted text\u003c/mark\u003e, \u003cb\u003ebold text\u003c/b\u003e\u003c/p\u003e\u003c/div\u003e\n    \u003c/body\u003e\n\u003c/html\u003e\"#;\n\nlet doc = dom_query::Document::from(contents);\n\npolicy.sanitize_document(\u0026doc);\n\n// After sanitization:\n// - there are no `div` elements in the DOM\nassert!(!doc.select(\"div\").exists());\n\n// All links are stripped, because it's not clear if they are secure. (Didn't match the policy)\nassert_eq!(doc.select(\"a\").length(), 0);\n// `link` appears only as text inside `p` elements\nassert_eq!(doc.html().matches(\"link\").count(), 3);\n\n// html, head, body are always kept\nassert!(doc.select(\"html\").exists());\nassert!(doc.select(\"head\").exists());\nassert!(doc.select(\"body\").exists());\n\n// title is preserved, because it's excluded from the Restrictive policy\nassert!(doc.select(\"head title\").exists());\nassert!(doc.select(\"p mark\").exists());\nassert!(doc.select(\"p b\").exists());\nassert!(!doc.select(\"p[role]\").exists());\n```\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eRegex-Based Content Filter\u003c/b\u003e\u003c/summary\u003e\n\nThis example demonstrates how to implement a more advanced content filtering strategy\nusing external dependencies like `regex`.\n\n```rust\nuse dom_sanitizer::plugin_policy::{NodeChecker, PluginPolicy};\nuse dom_sanitizer::Permissive;\n\nuse dom_query::NodeRef;\nuse html5ever::LocalName;\nuse regex::Regex;\n\n// `RegexContentCountMatcher` checks whether a given regex pattern appears\n// in the text content of a node a certain number of times. If the number\n// of matches is greater than or equal to the specified threshold, the node\n// is considered a match.\nstruct RegexContentCountMatcher {\n    element_scope: LocalName,\n    regex: Regex,\n    threshold: usize,\n}\n\nimpl RegexContentCountMatcher {\n    fn new(re: \u0026str, threshold: usize, element_scope: \u0026str) -\u003e Self {\n        Self {\n            element_scope: LocalName::from(element_scope),\n            regex: Regex::new(re).unwrap(),\n            threshold,\n        }\n    }\n}\n\nimpl NodeChecker for RegexContentCountMatcher {\n    fn is_match(\u0026self, node: \u0026NodeRef) -\u003e bool {\n        let Some(qual_name) = node.qual_name_ref() else {\n            return false;\n        };\n        if qual_name.local != self.element_scope {\n            return false;\n        }\n\n        let text = node.text();\n        if text.is_empty() {\n            return false;\n        }\n\n        self.regex.find_iter(\u0026text).count() \u003e= self.threshold\n    }\n}\n\nlet policy: PluginPolicy\u003cPermissive\u003e = PluginPolicy::builder()\n    .remove(RegexContentCountMatcher::new(\n        r\"(?i)shop now|amazing deals|offer\",\n        3,\n        \"div\",\n    ))\n    .build();\n\nlet contents: \u0026str = r#\"\n\u003chtml lang=\"en\"\u003e\n    \u003chead\u003e\u003ctitle\u003eTest Ad Block\u003c/title\u003e\u003c/head\u003e\n    \u003cbody\u003e\n        \u003cdiv class=\"ad-block\"\u003e\n            \u003ch3 class=\"ad-title\"\u003eLimited Time Offer!\u003c/h3\u003e\n            \u003cp class=\"ad-text\"\u003eDiscover amazing deals on our latest products. Shop now and save big!\u003c/p\u003e\n            \u003ca href=\"/deal\" target=\"_blank\"\u003eLearn More\u003c/a\u003e\n        \u003c/div\u003e\n        \u003cdiv\u003e\u003cp class=\"regular-text\"\u003eA test paragraph.\u003c/p\u003e\u003c/div\u003e\n        \u003cdiv\u003e\u003cp\u003eAnother test paragraph.\u003c/p\u003e\u003c/div\u003e\n    \u003c/body\u003e\n\u003c/html\u003e\"#;\n\nlet doc = dom_query::Document::from(contents);\n\n// Before sanitization:\nassert!(doc.select(\"div.ad-block\").exists());\nassert_eq!(doc.select(\"div\").length(), 3);\nassert_eq!(doc.select(\"p\").length(), 3);\n\npolicy.sanitize_document(\u0026doc);\n\n// After sanitization, the `div.ad-block` element is removed because\n// its text content matched the pattern 3 times, which is considered too noisy.\nassert!(!doc.select(\"div.ad-block\").exists());\nassert_eq!(doc.select(\"div\").length(), 2);\nassert_eq!(doc.select(\"p\").length(), 2);\n```\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eSharing A `PluginPolicy` across Threads (atomic)\u003c/b\u003e\u003c/summary\u003e\n\n*This example requires the `atomic` feature.*\n\nIt demonstrates how to safely share and use a `PluginPolicy` across multiple threads. \nIt utilizes the `atomic` feature, which is required to share `dom_query::Document`.\n\n```rust\n#[cfg(feature = \"atomic\")]\n{\n    use std::sync::Arc;\n    use std::sync::mpsc::channel;\n\n    use html5ever::local_name;\n\n    use dom_sanitizer::plugin_policy::preset;\n    use dom_sanitizer::plugin_policy::PluginPolicy;\n    use dom_sanitizer::Restrictive;\n\n\n    let policy: PluginPolicy\u003cRestrictive\u003e = PluginPolicy::builder()\n        // Allow table elements\n        .exclude(preset::LocalNamesMatcher::new(\u0026[\n            \"table\", \"tbody\", \"tr\", \"th\", \"td\",\n        ]))\n        .remove(preset::LocalNameMatcher::new(\"style\"))\n        // `html`, `head`, and `body` are always kept\n        .build();\n        \n    let shared_policy = Arc::new(policy);\n\n    let (tx, rx) = channel();\n\n    for _ in 0..4 {\n        let policy = shared_policy.clone();\n        let thread_tx = tx.clone();\n        std::thread::spawn(move || {\n            let contents: \u0026str = include_str!(\"../test-pages/table.html\");\n            let doc = dom_query::Document::from(contents);\n            policy.sanitize_document(\u0026doc);\n            thread_tx.send(doc).unwrap();\n            \n        });\n        \n    }\n    drop(tx);\n\n    for doc in rx {\n        assert!(!doc.select(\"style\").exists());\n        assert!(doc.select(\"table tr \u003e td\").exists());\n    }\n}\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eAllowing a Namespace in a Restrictive Plugin Policy (SVG)\u003c/b\u003e\u003c/summary\u003e\n\nWhen sanitizing elements such as `\u003csvg\u003e` or `\u003cmath\u003e`, explicitly listing all allowed elements and attributes can be overly verbose.\nInstead, you can create a `NodeChecker` that allows all elements within a specific namespace, or an `AttrChecker` that allows all attributes for certain elements.\n\n```rust\nuse dom_query::{Document, NodeRef};\nuse dom_sanitizer::plugin_policy::{preset, AttrChecker, PluginPolicy};\nuse dom_sanitizer::Restrictive;\nuse html5ever::{ns, LocalName};\n\n// HTML with a **malicious** SVG\nlet contents: \u0026str = r#\"\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n    \u003chead\u003e\u003ctitle\u003eTest\u003c/title\u003e\u003c/head\u003e\n    \u003cbody\u003e\n        \u003csvg oncontentvisibilityautostatechange=alert(1) style=display:block;content-visibility:auto\n            viewBox=\"0 0 100 100\" preserveAspectRatio=\"xMidYMid slice\" role=\"img\"\u003e\n            \u003ctitle\u003eA gradient\u003c/title\u003e\n            \u003clinearGradient id=\"gradient\"\u003e\n                \u003cstop class=\"begin\" offset=\"0%\" stop-color=\"red\" /\u003e\n                \u003cstop class=\"end\" offset=\"100%\" stop-color=\"black\" /\u003e\n            \u003c/linearGradient\u003e\n            \u003crect x=\"0\" y=\"0\" width=\"100\" height=\"100\" style=\"fill:url(#gradient)\" /\u003e\n            \u003ccircle cx=\"50\" cy=\"50\" r=\"30\" style=\"fill:url(#gradient)\" /\u003e\n        \u003c/svg\u003e\n        \u003cp\u003eSome text\u003c/p\u003e\n        \u003cdiv\u003eSome other text\u003c/div\u003e\n    \u003c/body\u003e\n\u003c/html\u003e\"#;\n\n    // Define a custom attribute checker that allows all attributes \n    // for elements in the SVG namespace, except those whose names start with \"on\".\n    struct SvgSafeAttrs;\n\n    impl AttrChecker for SvgSafeAttrs {\n        fn is_match_attr(\u0026self, node: \u0026NodeRef, attr: \u0026html5ever::Attribute) -\u003e bool {\n            if !node\n                .qual_name_ref()\n                .map_or(false, |name| name.ns == ns!(svg))\n            {\n                return false;\n            }\n            !attr.name.local.to_ascii_lowercase().starts_with(\"on\")\n        }\n    }\n    // Create a policy that strips all elements and attributes,\n    // except those explicitly excluded.\n    let policy: PluginPolicy\u003cRestrictive\u003e = PluginPolicy::builder()\n        // Allow all elements from the SVG namespace.\n        .exclude(preset::NamespaceMatcher(ns!(svg)))\n        // Also allow \u003cdiv\u003e elements.\n        .exclude(preset::LocalNameMatcher::new(\"div\"))\n        // Allow all attributes on elements in the SVG namespace, except those starting with `on`.\n        .exclude_attr(SvgSafeAttrs)\n        .build();\n\n    let doc = Document::from(contents);\n\n    policy.sanitize_document(\u0026doc);\n    \n    // The SVG no longer has the `oncontentvisibilityautostatechange` attribute.\n    assert!(!doc\n        .select(\"svg[oncontentvisibilityautostatechange]\")\n        .exists());\n    // The SVG still has the `style` attribute.\n    assert!(doc.select(\"svg[style]\").exists());\n    // Other elements in the SVG namespace still have their attributes.\n    assert!(doc.select(\"circle[r][style]\").exists());\n    assert!(doc.select(\"rect[width][height][style]\").exists());\n    // The \u003cdiv\u003e element was preserved.\n    assert!(doc.select(\"div\").exists());\n    // The \u003cp\u003e element was removed.\n    assert!(!doc.select(\"p\").exists());\n```\n\u003c/details\u003e\n\n## Crate Features\n\n- `atomic` — enables thread-safe usage of `dom_query::Document` by activating the `atomic` feature of the `dom_query` dependency.\nRequired only if the `Document` needs to be shared or transferred across threads (i.e., `Send` + `Sync` bounds).\n\n## License\n\nLicensed under MIT ([LICENSE](LICENSE) or \u003chttp://opensource.org/licenses/MIT\u003e)\n\n\n## Contribution\n\nAny contribution intentionally submitted for inclusion in the work by you, shall be\nlicensed with MIT license, without any additional terms or conditions.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fniklak%2Fdom_sanitizer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fniklak%2Fdom_sanitizer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fniklak%2Fdom_sanitizer/lists"}