ammonia/lib.rs
1// Copyright (C) Michael Howell and others
2// this library is released under the same terms as Rust itself.
3
4#![deny(unsafe_code)]
5#![deny(missing_docs)]
6
7//! Ammonia is a whitelist-based HTML sanitization library. It is designed to
8//! prevent cross-site scripting, layout breaking, and clickjacking caused
9//! by untrusted user-provided HTML being mixed into a larger web page.
10//!
11//! Ammonia uses [html5ever] to parse and serialize document fragments the same way browsers do,
12//! so it is extremely resilient to syntactic obfuscation.
13//!
14//! Ammonia parses its input exactly according to the HTML5 specification;
15//! it will not linkify bare URLs, insert line or paragraph breaks, or convert `(C)` into ©.
16//! If you want that, use a markup processor before running the sanitizer, like [pulldown-cmark].
17//!
18//! # Examples
19//!
20//! ```
21//! let result = ammonia::clean(
22//! "<b><img src='' onerror=alert('hax')>I'm not trying to XSS you</b>"
23//! );
24//! assert_eq!(result, "<b><img src=\"\">I'm not trying to XSS you</b>");
25//! ```
26//!
27//! [html5ever]: https://github.com/servo/html5ever "The HTML parser in Servo"
28//! [pulldown-cmark]: https://github.com/google/pulldown-cmark "CommonMark parser"
29
30#[cfg(ammonia_unstable)]
31pub mod rcdom;
32
33#[cfg(not(ammonia_unstable))]
34mod rcdom;
35
36mod style;
37
38use html5ever::interface::Attribute;
39use html5ever::serialize::{serialize, SerializeOpts};
40use html5ever::tree_builder::{NodeOrText, TreeSink};
41use html5ever::{driver as html, local_name, ns, Namespace, QualName};
42use maplit::{hashmap, hashset};
43use std::sync::LazyLock;
44use rcdom::{Handle, NodeData, RcDom, SerializableHandle};
45use std::borrow::{Borrow, Cow};
46use std::cell::Cell;
47use std::cmp::max;
48use std::collections::{HashMap, HashSet};
49use std::fmt::{self, Display};
50use std::io;
51use std::iter::IntoIterator as IntoIter;
52use std::mem;
53use std::rc::Rc;
54use std::str::FromStr;
55use html5ever::tendril::stream::TendrilSink;
56use html5ever::tendril::StrTendril;
57use html5ever::tendril::{format_tendril, ByteTendril};
58pub use url::Url;
59
60use html5ever::buffer_queue::BufferQueue;
61use html5ever::tokenizer::{Token, TokenSink, TokenSinkResult, Tokenizer};
62pub use url;
63
64static AMMONIA: LazyLock<Builder<'static>> = LazyLock::new(Builder::default);
65
66/// Clean HTML with a conservative set of defaults.
67///
68/// * [tags](struct.Builder.html#defaults)
69/// * [`script` and `style` have their contents stripped](struct.Builder.html#defaults-1)
70/// * [attributes on specific tags](struct.Builder.html#defaults-2)
71/// * [attributes on all tags](struct.Builder.html#defaults-6)
72/// * [url schemes](struct.Builder.html#defaults-7)
73/// * [relative URLs are passed through, unchanged, by default](struct.Builder.html#defaults-8)
74/// * [links are marked `noopener noreferrer` by default](struct.Builder.html#defaults-9)
75/// * all `class=""` settings are blocked by default
76/// * comments are stripped by default
77/// * no generic attribute prefixes are turned on by default
78/// * no specific tag-attribute-value settings are configured by default
79///
80/// [opener]: https://mathiasbynens.github.io/rel-noopener/
81/// [referrer]: https://en.wikipedia.org/wiki/HTTP_referer
82///
83/// # Examples
84///
85/// assert_eq!(ammonia::clean("XSS<script>attack</script>"), "XSS")
86pub fn clean(src: &str) -> String {
87 AMMONIA.clean(src).to_string()
88}
89
90/// Turn an arbitrary string into unformatted HTML.
91///
92/// This function is roughly equivalent to PHP's `htmlspecialchars` and `htmlentities`.
93/// It is as strict as possible, encoding every character that has special meaning to the
94/// HTML parser.
95///
96/// # Warnings
97///
98/// This function cannot be used to package strings into a `<script>` or `<style>` tag;
99/// you need a JavaScript or CSS escaper to do that.
100///
101/// // DO NOT DO THIS
102/// # use ammonia::clean_text;
103/// let untrusted = "Robert\"); abuse();//";
104/// let html = format!("<script>invoke(\"{}\")</script>", clean_text(untrusted));
105///
106/// `<textarea>` tags will strip the first newline, if present, even if that newline is encoded.
107/// If you want to build an editor that works the way most folks expect them to, you should put a
108/// newline at the beginning of the tag, like this:
109///
110/// # use ammonia::{Builder, clean_text};
111/// let untrusted = "\n\nhi!";
112/// let mut b = Builder::new();
113/// b.add_tags(&["textarea"]);
114/// // This is the bad version
115/// // The user put two newlines at the beginning, but the first one was removed
116/// let sanitized = b.clean(&format!("<textarea>{}</textarea>", clean_text(untrusted))).to_string();
117/// assert_eq!("<textarea>\nhi!</textarea>", sanitized);
118/// // This is a good version
119/// // The user put two newlines at the beginning, and we add a third one,
120/// // so the result still has two
121/// let sanitized = b.clean(&format!("<textarea>\n{}</textarea>", clean_text(untrusted))).to_string();
122/// assert_eq!("<textarea>\n\nhi!</textarea>", sanitized);
123/// // This version is also often considered good
124/// // For many applications, leading and trailing whitespace is probably unwanted
125/// let sanitized = b.clean(&format!("<textarea>{}</textarea>", clean_text(untrusted.trim()))).to_string();
126/// assert_eq!("<textarea>hi!</textarea>", sanitized);
127///
128/// It also does not make user text safe for HTML attribute microsyntaxes such as `class` or `id`.
129/// Only use this function for places where HTML accepts unrestricted text such as `title` attributes
130/// and paragraph contents.
131pub fn clean_text(src: &str) -> String {
132 let mut ret_val = String::with_capacity(max(4, src.len()));
133 for c in src.chars() {
134 let replacement = match c {
135 // this character, when confronted, will start a tag
136 '<' => "<",
137 // in an unquoted attribute, will end the attribute value
138 '>' => ">",
139 // in an attribute surrounded by double quotes, this character will end the attribute value
140 '\"' => """,
141 // in an attribute surrounded by single quotes, this character will end the attribute value
142 '\'' => "'",
143 // in HTML5, returns a bogus parse error in an unquoted attribute, while in SGML/HTML, it will end an attribute value surrounded by backquotes
144 '`' => "`",
145 // in an unquoted attribute, this character will end the attribute
146 '/' => "/",
147 // starts an entity reference
148 '&' => "&",
149 // if at the beginning of an unquoted attribute, will get ignored
150 '=' => "=",
151 // will end an unquoted attribute
152 ' ' => " ",
153 '\t' => "	",
154 '\n' => " ",
155 '\x0c' => "",
156 '\r' => " ",
157 // a spec-compliant browser will perform this replacement anyway, but the middleware might not
158 '\0' => "�",
159 // ALL OTHER CHARACTERS ARE PASSED THROUGH VERBATIM
160 _ => {
161 ret_val.push(c);
162 continue;
163 }
164 };
165 ret_val.push_str(replacement);
166 }
167 ret_val
168}
169
170/// Determine if a given string contains HTML
171///
172/// This function is parses the full string into HTML and checks if the input contained any
173/// HTML syntax.
174///
175/// # Note
176/// This function will return positively for strings that contain invalid HTML syntax like
177/// `<g>` and even `Vec::<u8>::new()`.
178pub fn is_html(input: &str) -> bool {
179 let santok = SanitizationTokenizer::new();
180 let mut chunk = ByteTendril::new();
181 chunk.push_slice(input.as_bytes());
182 let mut input = BufferQueue::default();
183 input.push_back(chunk.try_reinterpret().unwrap());
184
185 let tok = Tokenizer::new(santok, Default::default());
186 let _ = tok.feed(&mut input);
187 tok.end();
188 tok.sink.was_sanitized.get()
189}
190
191#[derive(Clone)]
192struct SanitizationTokenizer {
193 was_sanitized: Cell<bool>,
194}
195
196impl SanitizationTokenizer {
197 pub fn new() -> SanitizationTokenizer {
198 SanitizationTokenizer {
199 was_sanitized: false.into(),
200 }
201 }
202}
203
204impl TokenSink for SanitizationTokenizer {
205 type Handle = ();
206 fn process_token(&self, token: Token, _line_number: u64) -> TokenSinkResult<()> {
207 match token {
208 Token::CharacterTokens(_) | Token::EOFToken | Token::ParseError(_) => {}
209 _ => {
210 self.was_sanitized.set(true);
211 }
212 }
213 TokenSinkResult::Continue
214 }
215 fn end(&self) {}
216}
217
218/// An HTML sanitizer.
219///
220/// Given a fragment of HTML, Ammonia will parse it according to the HTML5
221/// parsing algorithm and sanitize any disallowed tags or attributes. This
222/// algorithm also takes care of things like unclosed and (some) misnested
223/// tags.
224///
225/// # Examples
226///
227/// use ammonia::{Builder, UrlRelative};
228///
229/// let a = Builder::default()
230/// .link_rel(None)
231/// .url_relative(UrlRelative::PassThrough)
232/// .clean("<a href=/>test")
233/// .to_string();
234/// assert_eq!(
235/// a,
236/// "<a href=\"/\">test</a>");
237///
238/// # Panics
239///
240/// Running [`clean`] or [`clean_from_reader`] may cause a panic if the builder is
241/// configured with any of these (contradictory) settings:
242///
243/// * The `rel` attribute is added to [`generic_attributes`] or the
244/// [`tag_attributes`] for the `<a>` tag, and [`link_rel`] is not set to `None`.
245///
246/// For example, this is going to panic, since [`link_rel`] is set to
247/// `Some("noopener noreferrer")` by default,
248/// and it makes no sense to simultaneously say that the user is allowed to
249/// set their own `rel` attribute while saying that every link shall be set to
250/// a particular value:
251///
252/// ```should_panic
253/// use ammonia::Builder;
254/// use maplit::hashset;
255///
256/// # fn main() {
257/// Builder::default()
258/// .generic_attributes(hashset!["rel"])
259/// .clean("");
260/// # }
261/// ```
262///
263/// This, however, is perfectly valid:
264///
265/// ```
266/// use ammonia::Builder;
267/// use maplit::hashset;
268///
269/// # fn main() {
270/// Builder::default()
271/// .generic_attributes(hashset!["rel"])
272/// .link_rel(None)
273/// .clean("");
274/// # }
275/// ```
276///
277/// * The `class` attribute is in [`allowed_classes`] and is in the
278/// corresponding [`tag_attributes`] or in [`generic_attributes`].
279///
280/// This is done both to line up with the treatment of `rel`,
281/// and to prevent people from accidentally allowing arbitrary
282/// classes on a particular element.
283///
284/// This will panic:
285///
286/// ```should_panic
287/// use ammonia::Builder;
288/// use maplit::{hashmap, hashset};
289///
290/// # fn main() {
291/// Builder::default()
292/// .generic_attributes(hashset!["class"])
293/// .allowed_classes(hashmap!["span" => hashset!["hidden"]])
294/// .clean("");
295/// # }
296/// ```
297///
298/// This, however, is perfectly valid:
299///
300/// ```
301/// use ammonia::Builder;
302/// use maplit::{hashmap, hashset};
303///
304/// # fn main() {
305/// Builder::default()
306/// .allowed_classes(hashmap!["span" => hashset!["hidden"]])
307/// .clean("");
308/// # }
309/// ```
310///
311/// * A tag is in either [`tags`] or [`tag_attributes`] while also
312/// being in [`clean_content_tags`].
313///
314/// Both [`tags`] and [`tag_attributes`] are whitelists but
315/// [`clean_content_tags`] is a blacklist, so it doesn't make sense
316/// to have the same tag in both.
317///
318/// For example, this will panic, since the `aside` tag is in
319/// [`tags`] by default:
320///
321/// ```should_panic
322/// use ammonia::Builder;
323/// use maplit::hashset;
324///
325/// # fn main() {
326/// Builder::default()
327/// .clean_content_tags(hashset!["aside"])
328/// .clean("");
329/// # }
330/// ```
331///
332/// This, however, is valid:
333///
334/// ```
335/// use ammonia::Builder;
336/// use maplit::hashset;
337///
338/// # fn main() {
339/// Builder::default()
340/// .rm_tags(&["aside"])
341/// .clean_content_tags(hashset!["aside"])
342/// .clean("");
343/// # }
344/// ```
345///
346/// [`clean`]: #method.clean
347/// [`clean_from_reader`]: #method.clean_from_reader
348/// [`generic_attributes`]: #method.generic_attributes
349/// [`tag_attributes`]: #method.tag_attributes
350/// [`generic_attributes`]: #method.generic_attributes
351/// [`link_rel`]: #method.link_rel
352/// [`allowed_classes`]: #method.allowed_classes
353/// [`id_prefix`]: #method.id_prefix
354/// [`tags`]: #method.tags
355/// [`clean_content_tags`]: #method.clean_content_tags
356#[derive(Debug)]
357pub struct Builder<'a> {
358 tags: HashSet<&'a str>,
359 clean_content_tags: HashSet<&'a str>,
360 tag_attributes: HashMap<&'a str, HashSet<&'a str>>,
361 tag_attribute_values: HashMap<&'a str, HashMap<&'a str, HashSet<&'a str>>>,
362 set_tag_attribute_values: HashMap<&'a str, HashMap<&'a str, &'a str>>,
363 generic_attributes: HashSet<&'a str>,
364 url_schemes: HashSet<&'a str>,
365 url_relative: UrlRelative<'a>,
366 attribute_filter: Option<Box<dyn AttributeFilter>>,
367 link_rel: Option<&'a str>,
368 allowed_classes: HashMap<&'a str, HashSet<&'a str>>,
369 strip_comments: bool,
370 id_prefix: Option<&'a str>,
371 generic_attribute_prefixes: Option<HashSet<&'a str>>,
372 style_properties: Option<HashSet<&'a str>>,
373}
374
375impl<'a> Default for Builder<'a> {
376 fn default() -> Self {
377 #[rustfmt::skip]
378 let tags = hashset![
379 "a", "abbr", "acronym", "area", "article", "aside", "b", "bdi",
380 "bdo", "blockquote", "br", "caption", "center", "cite", "code",
381 "col", "colgroup", "data", "dd", "del", "details", "dfn", "div",
382 "dl", "dt", "em", "figcaption", "figure", "footer", "h1", "h2",
383 "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "i", "img",
384 "ins", "kbd", "li", "map", "mark", "nav", "ol", "p", "pre",
385 "q", "rp", "rt", "rtc", "ruby", "s", "samp", "small", "span",
386 "strike", "strong", "sub", "summary", "sup", "table", "tbody",
387 "td", "th", "thead", "time", "tr", "tt", "u", "ul", "var", "wbr"
388 ];
389 let clean_content_tags = hashset!["script", "style"];
390 let generic_attributes = hashset!["lang", "title"];
391 let tag_attributes = hashmap![
392 "a" => hashset![
393 "href", "hreflang"
394 ],
395 "bdo" => hashset![
396 "dir"
397 ],
398 "blockquote" => hashset![
399 "cite"
400 ],
401 "col" => hashset![
402 "align", "char", "charoff", "span"
403 ],
404 "colgroup" => hashset![
405 "align", "char", "charoff", "span"
406 ],
407 "del" => hashset![
408 "cite", "datetime"
409 ],
410 "hr" => hashset![
411 "align", "size", "width"
412 ],
413 "img" => hashset![
414 "align", "alt", "height", "src", "width"
415 ],
416 "ins" => hashset![
417 "cite", "datetime"
418 ],
419 "ol" => hashset![
420 "start"
421 ],
422 "q" => hashset![
423 "cite"
424 ],
425 "table" => hashset![
426 "align", "char", "charoff", "summary"
427 ],
428 "tbody" => hashset![
429 "align", "char", "charoff"
430 ],
431 "td" => hashset![
432 "align", "char", "charoff", "colspan", "headers", "rowspan"
433 ],
434 "tfoot" => hashset![
435 "align", "char", "charoff"
436 ],
437 "th" => hashset![
438 "align", "char", "charoff", "colspan", "headers", "rowspan", "scope"
439 ],
440 "thead" => hashset![
441 "align", "char", "charoff"
442 ],
443 "tr" => hashset![
444 "align", "char", "charoff"
445 ],
446 ];
447 let tag_attribute_values = hashmap![];
448 let set_tag_attribute_values = hashmap![];
449 let url_schemes = hashset![
450 "bitcoin",
451 "ftp",
452 "ftps",
453 "geo",
454 "http",
455 "https",
456 "im",
457 "irc",
458 "ircs",
459 "magnet",
460 "mailto",
461 "mms",
462 "mx",
463 "news",
464 "nntp",
465 "openpgp4fpr",
466 "sip",
467 "sms",
468 "smsto",
469 "ssh",
470 "tel",
471 "url",
472 "webcal",
473 "wtai",
474 "xmpp"
475 ];
476 let allowed_classes = hashmap![];
477
478 Builder {
479 tags,
480 clean_content_tags,
481 tag_attributes,
482 tag_attribute_values,
483 set_tag_attribute_values,
484 generic_attributes,
485 url_schemes,
486 url_relative: UrlRelative::PassThrough,
487 attribute_filter: None,
488 link_rel: Some("noopener noreferrer"),
489 allowed_classes,
490 strip_comments: true,
491 id_prefix: None,
492 generic_attribute_prefixes: None,
493 style_properties: None,
494 }
495 }
496}
497
498impl<'a> Builder<'a> {
499 /// Sets the tags that are allowed.
500 ///
501 /// Note that the document-level tags `<html>`, `<head>`, and `<body>` cannot
502 /// be allowed here. Ammonia parses its input as a fragment (as if it were
503 /// the contents of a `<div>`), so these tags are stripped by the parser
504 /// before they reach the sanitizer.
505 ///
506 /// # Examples
507 ///
508 /// use ammonia::Builder;
509 /// use maplit::hashset;
510 ///
511 /// # fn main() {
512 /// let tags = hashset!["my-tag"];
513 /// let a = Builder::new()
514 /// .tags(tags)
515 /// .clean("<my-tag>")
516 /// .to_string();
517 /// assert_eq!(a, "<my-tag></my-tag>");
518 /// # }
519 ///
520 /// # Defaults
521 ///
522 /// ```notest
523 /// a, abbr, acronym, area, article, aside, b, bdi,
524 /// bdo, blockquote, br, caption, center, cite, code,
525 /// col, colgroup, data, dd, del, details, dfn, div,
526 /// dl, dt, em, figcaption, figure, footer, h1, h2,
527 /// h3, h4, h5, h6, header, hgroup, hr, i, img,
528 /// ins, kbd, li, map, mark, nav, ol, p, pre,
529 /// q, rp, rt, rtc, ruby, s, samp, small, span,
530 /// strike, strong, sub, summary, sup, table, tbody,
531 /// td, th, thead, time, tr, tt, u, ul, var, wbr
532 /// ```
533 pub fn tags(&mut self, value: HashSet<&'a str>) -> &mut Self {
534 self.tags = value;
535 self
536 }
537
538 /// Add additonal whitelisted tags without overwriting old ones.
539 ///
540 /// Does nothing if the tag is already there.
541 ///
542 /// # Examples
543 ///
544 /// let a = ammonia::Builder::default()
545 /// .add_tags(&["my-tag"])
546 /// .clean("<my-tag>test</my-tag> <span>mess</span>").to_string();
547 /// assert_eq!("<my-tag>test</my-tag> <span>mess</span>", a);
548 pub fn add_tags<T: 'a + ?Sized + Borrow<str>, I: IntoIter<Item = &'a T>>(
549 &mut self,
550 it: I,
551 ) -> &mut Self {
552 self.tags.extend(it.into_iter().map(Borrow::borrow));
553 self
554 }
555
556 /// Remove already-whitelisted tags.
557 ///
558 /// Does nothing if the tags is already gone.
559 ///
560 /// # Examples
561 ///
562 /// let a = ammonia::Builder::default()
563 /// .rm_tags(&["span"])
564 /// .clean("<span></span>").to_string();
565 /// assert_eq!("", a);
566 pub fn rm_tags<'b, T: 'b + ?Sized + Borrow<str>, I: IntoIter<Item = &'b T>>(
567 &mut self,
568 it: I,
569 ) -> &mut Self {
570 for i in it {
571 self.tags.remove(i.borrow());
572 }
573 self
574 }
575
576 /// Returns a copy of the set of whitelisted tags.
577 ///
578 /// # Examples
579 ///
580 /// use maplit::hashset;
581 ///
582 /// let tags = hashset!["my-tag-1", "my-tag-2"];
583 ///
584 /// let mut b = ammonia::Builder::default();
585 /// b.tags(Clone::clone(&tags));
586 /// assert_eq!(tags, b.clone_tags());
587 pub fn clone_tags(&self) -> HashSet<&'a str> {
588 self.tags.clone()
589 }
590
591 /// Sets the tags whose contents will be completely removed from the output.
592 ///
593 /// Adding tags which are whitelisted in `tags` or `tag_attributes` will cause
594 /// a panic.
595 ///
596 /// # Examples
597 ///
598 /// use ammonia::Builder;
599 /// use maplit::hashset;
600 ///
601 /// # fn main() {
602 /// let tag_blacklist = hashset!["script", "style"];
603 /// let a = Builder::new()
604 /// .clean_content_tags(tag_blacklist)
605 /// .clean("<script>alert('hello')</script><style>a { background: #fff }</style>")
606 /// .to_string();
607 /// assert_eq!(a, "");
608 /// # }
609 ///
610 /// # Defaults
611 ///
612 /// ```notest
613 /// script, style
614 /// ```
615 pub fn clean_content_tags(&mut self, value: HashSet<&'a str>) -> &mut Self {
616 self.clean_content_tags = value;
617 self
618 }
619
620 /// Add additonal blacklisted clean-content tags without overwriting old ones.
621 ///
622 /// Does nothing if the tag is already there.
623 ///
624 /// Adding tags which are whitelisted in `tags` or `tag_attributes` will cause
625 /// a panic.
626 ///
627 /// # Examples
628 ///
629 /// let a = ammonia::Builder::default()
630 /// .add_clean_content_tags(&["my-tag"])
631 /// .clean("<my-tag>test</my-tag><span>mess</span>").to_string();
632 /// assert_eq!("<span>mess</span>", a);
633 pub fn add_clean_content_tags<T: 'a + ?Sized + Borrow<str>, I: IntoIter<Item = &'a T>>(
634 &mut self,
635 it: I,
636 ) -> &mut Self {
637 self.clean_content_tags
638 .extend(it.into_iter().map(Borrow::borrow));
639 self
640 }
641
642 /// Remove already-blacklisted clean-content tags.
643 ///
644 /// Does nothing if the tags aren't blacklisted.
645 ///
646 /// # Examples
647 /// use ammonia::Builder;
648 /// use maplit::hashset;
649 ///
650 /// # fn main() {
651 /// let tag_blacklist = hashset!["script"];
652 /// let a = ammonia::Builder::default()
653 /// .clean_content_tags(tag_blacklist)
654 /// .rm_clean_content_tags(&["script"])
655 /// .clean("<script>XSS</script>").to_string();
656 /// assert_eq!("XSS", a);
657 /// # }
658 pub fn rm_clean_content_tags<'b, T: 'b + ?Sized + Borrow<str>, I: IntoIter<Item = &'b T>>(
659 &mut self,
660 it: I,
661 ) -> &mut Self {
662 for i in it {
663 self.clean_content_tags.remove(i.borrow());
664 }
665 self
666 }
667
668 /// Returns a copy of the set of blacklisted clean-content tags.
669 ///
670 /// # Examples
671 /// # use maplit::hashset;
672 ///
673 /// let tags = hashset!["my-tag-1", "my-tag-2"];
674 ///
675 /// let mut b = ammonia::Builder::default();
676 /// b.clean_content_tags(Clone::clone(&tags));
677 /// assert_eq!(tags, b.clone_clean_content_tags());
678 pub fn clone_clean_content_tags(&self) -> HashSet<&'a str> {
679 self.clean_content_tags.clone()
680 }
681
682 /// Sets the HTML attributes that are allowed on specific tags.
683 ///
684 /// The value is structured as a map from tag names to a set of attribute names.
685 ///
686 /// If a tag is not itself whitelisted, adding entries to this map will do nothing.
687 ///
688 /// # Examples
689 ///
690 /// use ammonia::Builder;
691 /// use maplit::{hashmap, hashset};
692 ///
693 /// # fn main() {
694 /// let tags = hashset!["my-tag"];
695 /// let tag_attributes = hashmap![
696 /// "my-tag" => hashset!["val"]
697 /// ];
698 /// let a = Builder::new().tags(tags).tag_attributes(tag_attributes)
699 /// .clean("<my-tag val=1>")
700 /// .to_string();
701 /// assert_eq!(a, "<my-tag val=\"1\"></my-tag>");
702 /// # }
703 ///
704 /// # Defaults
705 ///
706 /// ```notest
707 /// a =>
708 /// href, hreflang
709 /// bdo =>
710 /// dir
711 /// blockquote =>
712 /// cite
713 /// col =>
714 /// align, char, charoff, span
715 /// colgroup =>
716 /// align, char, charoff, span
717 /// del =>
718 /// cite, datetime
719 /// hr =>
720 /// align, size, width
721 /// img =>
722 /// align, alt, height, src, width
723 /// ins =>
724 /// cite, datetime
725 /// ol =>
726 /// start
727 /// q =>
728 /// cite
729 /// table =>
730 /// align, char, charoff, summary
731 /// tbody =>
732 /// align, char, charoff
733 /// td =>
734 /// align, char, charoff, colspan, headers, rowspan
735 /// tfoot =>
736 /// align, char, charoff
737 /// th =>
738 /// align, char, charoff, colspan, headers, rowspan, scope
739 /// thead =>
740 /// align, char, charoff
741 /// tr =>
742 /// align, char, charoff
743 /// ```
744 pub fn tag_attributes(&mut self, value: HashMap<&'a str, HashSet<&'a str>>) -> &mut Self {
745 self.tag_attributes = value;
746 self
747 }
748
749 /// Add additonal whitelisted tag-specific attributes without overwriting old ones.
750 ///
751 /// # Examples
752 ///
753 /// let a = ammonia::Builder::default()
754 /// .add_tags(&["my-tag"])
755 /// .add_tag_attributes("my-tag", &["my-attr"])
756 /// .clean("<my-tag my-attr>test</my-tag> <span>mess</span>").to_string();
757 /// assert_eq!("<my-tag my-attr=\"\">test</my-tag> <span>mess</span>", a);
758 pub fn add_tag_attributes<
759 T: 'a + ?Sized + Borrow<str>,
760 U: 'a + ?Sized + Borrow<str>,
761 I: IntoIter<Item = &'a T>,
762 >(
763 &mut self,
764 tag: &'a U,
765 it: I,
766 ) -> &mut Self {
767 self.tag_attributes
768 .entry(tag.borrow())
769 .or_default()
770 .extend(it.into_iter().map(Borrow::borrow));
771 self
772 }
773
774 /// Remove already-whitelisted tag-specific attributes.
775 ///
776 /// Does nothing if the attribute is already gone.
777 ///
778 /// # Examples
779 ///
780 /// let a = ammonia::Builder::default()
781 /// .rm_tag_attributes("a", &["href"])
782 /// .clean("<a href=\"/\"></a>").to_string();
783 /// assert_eq!("<a rel=\"noopener noreferrer\"></a>", a);
784 pub fn rm_tag_attributes<
785 'b,
786 'c,
787 T: 'b + ?Sized + Borrow<str>,
788 U: 'c + ?Sized + Borrow<str>,
789 I: IntoIter<Item = &'b T>,
790 >(
791 &mut self,
792 tag: &'c U,
793 it: I,
794 ) -> &mut Self {
795 if let Some(tag) = self.tag_attributes.get_mut(tag.borrow()) {
796 for i in it {
797 tag.remove(i.borrow());
798 }
799 }
800 self
801 }
802
803 /// Returns a copy of the set of whitelisted tag-specific attributes.
804 ///
805 /// # Examples
806 /// use maplit::{hashmap, hashset};
807 ///
808 /// let tag_attributes = hashmap![
809 /// "my-tag" => hashset!["my-attr-1", "my-attr-2"]
810 /// ];
811 ///
812 /// let mut b = ammonia::Builder::default();
813 /// b.tag_attributes(Clone::clone(&tag_attributes));
814 /// assert_eq!(tag_attributes, b.clone_tag_attributes());
815 pub fn clone_tag_attributes(&self) -> HashMap<&'a str, HashSet<&'a str>> {
816 self.tag_attributes.clone()
817 }
818
819 /// Sets the values of HTML attributes that are allowed on specific tags.
820 ///
821 /// The value is structured as a map from tag names to a map from attribute names to a set of
822 /// attribute values.
823 ///
824 /// If a tag is not itself whitelisted, adding entries to this map will do nothing.
825 ///
826 /// # Examples
827 ///
828 /// use ammonia::Builder;
829 /// use maplit::{hashmap, hashset};
830 ///
831 /// # fn main() {
832 /// let tags = hashset!["my-tag"];
833 /// let tag_attribute_values = hashmap![
834 /// "my-tag" => hashmap![
835 /// "my-attr" => hashset!["val"],
836 /// ],
837 /// ];
838 /// let a = Builder::new().tags(tags).tag_attribute_values(tag_attribute_values)
839 /// .clean("<my-tag my-attr=val>")
840 /// .to_string();
841 /// assert_eq!(a, "<my-tag my-attr=\"val\"></my-tag>");
842 /// # }
843 ///
844 /// # Defaults
845 ///
846 /// None.
847 pub fn tag_attribute_values(
848 &mut self,
849 value: HashMap<&'a str, HashMap<&'a str, HashSet<&'a str>>>,
850 ) -> &mut Self {
851 self.tag_attribute_values = value;
852 self
853 }
854
855 /// Add additonal whitelisted tag-specific attribute values without overwriting old ones.
856 ///
857 /// # Examples
858 ///
859 /// let a = ammonia::Builder::default()
860 /// .add_tags(&["my-tag"])
861 /// .add_tag_attribute_values("my-tag", "my-attr", &[""])
862 /// .clean("<my-tag my-attr>test</my-tag> <span>mess</span>").to_string();
863 /// assert_eq!("<my-tag my-attr=\"\">test</my-tag> <span>mess</span>", a);
864 pub fn add_tag_attribute_values<
865 T: 'a + ?Sized + Borrow<str>,
866 U: 'a + ?Sized + Borrow<str>,
867 V: 'a + ?Sized + Borrow<str>,
868 I: IntoIter<Item = &'a T>,
869 >(
870 &mut self,
871 tag: &'a U,
872 attribute: &'a V,
873 it: I,
874 ) -> &mut Self {
875 self.tag_attribute_values
876 .entry(tag.borrow())
877 .or_default()
878 .entry(attribute.borrow())
879 .or_default()
880 .extend(it.into_iter().map(Borrow::borrow));
881
882 self
883 }
884
885 /// Remove already-whitelisted tag-specific attribute values.
886 ///
887 /// Does nothing if the attribute or the value is already gone.
888 ///
889 /// # Examples
890 ///
891 /// let a = ammonia::Builder::default()
892 /// .rm_tag_attributes("a", &["href"])
893 /// .add_tag_attribute_values("a", "href", &["/"])
894 /// .rm_tag_attribute_values("a", "href", &["/"])
895 /// .clean("<a href=\"/\"></a>").to_string();
896 /// assert_eq!("<a rel=\"noopener noreferrer\"></a>", a);
897 pub fn rm_tag_attribute_values<
898 'b,
899 'c,
900 T: 'b + ?Sized + Borrow<str>,
901 U: 'c + ?Sized + Borrow<str>,
902 V: 'c + ?Sized + Borrow<str>,
903 I: IntoIter<Item = &'b T>,
904 >(
905 &mut self,
906 tag: &'c U,
907 attribute: &'c V,
908 it: I,
909 ) -> &mut Self {
910 if let Some(attrs) = self
911 .tag_attribute_values
912 .get_mut(tag.borrow())
913 .and_then(|map| map.get_mut(attribute.borrow()))
914 {
915 for i in it {
916 attrs.remove(i.borrow());
917 }
918 }
919 self
920 }
921
922 /// Returns a copy of the set of whitelisted tag-specific attribute values.
923 ///
924 /// # Examples
925 ///
926 /// use maplit::{hashmap, hashset};
927 ///
928 /// let attribute_values = hashmap![
929 /// "my-attr-1" => hashset!["foo"],
930 /// "my-attr-2" => hashset!["baz", "bar"],
931 /// ];
932 /// let tag_attribute_values = hashmap![
933 /// "my-tag" => attribute_values
934 /// ];
935 ///
936 /// let mut b = ammonia::Builder::default();
937 /// b.tag_attribute_values(Clone::clone(&tag_attribute_values));
938 /// assert_eq!(tag_attribute_values, b.clone_tag_attribute_values());
939 pub fn clone_tag_attribute_values(
940 &self,
941 ) -> HashMap<&'a str, HashMap<&'a str, HashSet<&'a str>>> {
942 self.tag_attribute_values.clone()
943 }
944
945 /// Sets the values of HTML attributes that are to be set on specific tags.
946 ///
947 /// The value is structured as a map from tag names to a map from attribute names to an
948 /// attribute value.
949 ///
950 /// If a tag is not itself whitelisted, adding entries to this map will do nothing.
951 ///
952 /// # Examples
953 ///
954 /// use ammonia::Builder;
955 /// use maplit::{hashmap, hashset};
956 ///
957 /// # fn main() {
958 /// let tags = hashset!["my-tag"];
959 /// let set_tag_attribute_values = hashmap![
960 /// "my-tag" => hashmap![
961 /// "my-attr" => "val",
962 /// ],
963 /// ];
964 /// let a = Builder::new().tags(tags).set_tag_attribute_values(set_tag_attribute_values)
965 /// .clean("<my-tag>")
966 /// .to_string();
967 /// assert_eq!(a, "<my-tag my-attr=\"val\"></my-tag>");
968 /// # }
969 ///
970 /// # Defaults
971 ///
972 /// None.
973 pub fn set_tag_attribute_values(
974 &mut self,
975 value: HashMap<&'a str, HashMap<&'a str, &'a str>>,
976 ) -> &mut Self {
977 self.set_tag_attribute_values = value;
978 self
979 }
980
981 /// Add an attribute value to set on a specific element.
982 ///
983 /// # Examples
984 ///
985 /// let a = ammonia::Builder::default()
986 /// .add_tags(&["my-tag"])
987 /// .set_tag_attribute_value("my-tag", "my-attr", "val")
988 /// .clean("<my-tag>test</my-tag> <span>mess</span>").to_string();
989 /// assert_eq!("<my-tag my-attr=\"val\">test</my-tag> <span>mess</span>", a);
990 pub fn set_tag_attribute_value<
991 T: 'a + ?Sized + Borrow<str>,
992 A: 'a + ?Sized + Borrow<str>,
993 V: 'a + ?Sized + Borrow<str>,
994 >(
995 &mut self,
996 tag: &'a T,
997 attribute: &'a A,
998 value: &'a V,
999 ) -> &mut Self {
1000 self.set_tag_attribute_values
1001 .entry(tag.borrow())
1002 .or_default()
1003 .insert(attribute.borrow(), value.borrow());
1004 self
1005 }
1006
1007 /// Remove existing tag-specific attribute values to be set.
1008 ///
1009 /// Does nothing if the attribute is already gone.
1010 ///
1011 /// # Examples
1012 ///
1013 /// let a = ammonia::Builder::default()
1014 /// // this does nothing, since no value is set for this tag attribute yet
1015 /// .rm_set_tag_attribute_value("a", "target")
1016 /// .set_tag_attribute_value("a", "target", "_blank")
1017 /// .rm_set_tag_attribute_value("a", "target")
1018 /// .clean("<a href=\"/\"></a>").to_string();
1019 /// assert_eq!("<a href=\"/\" rel=\"noopener noreferrer\"></a>", a);
1020 pub fn rm_set_tag_attribute_value<
1021 T: 'a + ?Sized + Borrow<str>,
1022 A: 'a + ?Sized + Borrow<str>,
1023 >(
1024 &mut self,
1025 tag: &'a T,
1026 attribute: &'a A,
1027 ) -> &mut Self {
1028 if let Some(attributes) = self.set_tag_attribute_values.get_mut(tag.borrow()) {
1029 attributes.remove(attribute.borrow());
1030 }
1031 self
1032 }
1033
1034 /// Returns the value that will be set for the attribute on the element, if any.
1035 ///
1036 /// # Examples
1037 ///
1038 /// let mut b = ammonia::Builder::default();
1039 /// b.set_tag_attribute_value("a", "target", "_blank");
1040 /// let value = b.get_set_tag_attribute_value("a", "target");
1041 /// assert_eq!(value, Some("_blank"));
1042 pub fn get_set_tag_attribute_value<
1043 T: 'a + ?Sized + Borrow<str>,
1044 A: 'a + ?Sized + Borrow<str>,
1045 >(
1046 &self,
1047 tag: &'a T,
1048 attribute: &'a A,
1049 ) -> Option<&'a str> {
1050 self.set_tag_attribute_values
1051 .get(tag.borrow())
1052 .and_then(|map| map.get(attribute.borrow()))
1053 .copied()
1054 }
1055
1056 /// Returns a copy of the set of tag-specific attribute values to be set.
1057 ///
1058 /// # Examples
1059 ///
1060 /// use maplit::{hashmap, hashset};
1061 ///
1062 /// let attribute_values = hashmap![
1063 /// "my-attr-1" => "foo",
1064 /// "my-attr-2" => "bar",
1065 /// ];
1066 /// let set_tag_attribute_values = hashmap![
1067 /// "my-tag" => attribute_values,
1068 /// ];
1069 ///
1070 /// let mut b = ammonia::Builder::default();
1071 /// b.set_tag_attribute_values(Clone::clone(&set_tag_attribute_values));
1072 /// assert_eq!(set_tag_attribute_values, b.clone_set_tag_attribute_values());
1073 pub fn clone_set_tag_attribute_values(&self) -> HashMap<&'a str, HashMap<&'a str, &'a str>> {
1074 self.set_tag_attribute_values.clone()
1075 }
1076
1077 /// Sets the prefix of attributes that are allowed on any tag.
1078 ///
1079 /// # Examples
1080 ///
1081 /// use ammonia::Builder;
1082 /// use maplit::hashset;
1083 ///
1084 /// # fn main() {
1085 /// let prefixes = hashset!["data-"];
1086 /// let a = Builder::new()
1087 /// .generic_attribute_prefixes(prefixes)
1088 /// .clean("<b data-val=1>")
1089 /// .to_string();
1090 /// assert_eq!(a, "<b data-val=\"1\"></b>");
1091 /// # }
1092 ///
1093 /// # Defaults
1094 ///
1095 /// No attribute prefixes are allowed by default.
1096 pub fn generic_attribute_prefixes(&mut self, value: HashSet<&'a str>) -> &mut Self {
1097 self.generic_attribute_prefixes = Some(value);
1098 self
1099 }
1100
1101 /// Add additional whitelisted attribute prefix without overwriting old ones.
1102 ///
1103 /// # Examples
1104 ///
1105 /// let a = ammonia::Builder::default()
1106 /// .add_generic_attribute_prefixes(&["my-"])
1107 /// .clean("<span my-attr>mess</span>").to_string();
1108 /// assert_eq!("<span my-attr=\"\">mess</span>", a);
1109 pub fn add_generic_attribute_prefixes<
1110 T: 'a + ?Sized + Borrow<str>,
1111 I: IntoIter<Item = &'a T>,
1112 >(
1113 &mut self,
1114 it: I,
1115 ) -> &mut Self {
1116 self.generic_attribute_prefixes
1117 .get_or_insert_with(HashSet::new)
1118 .extend(it.into_iter().map(Borrow::borrow));
1119 self
1120 }
1121
1122 /// Remove already-whitelisted attribute prefixes.
1123 ///
1124 /// Does nothing if the attribute prefix is already gone.
1125 ///
1126 /// # Examples
1127 ///
1128 /// let a = ammonia::Builder::default()
1129 /// .add_generic_attribute_prefixes(&["data-", "code-"])
1130 /// .rm_generic_attribute_prefixes(&["data-"])
1131 /// .clean("<span code-test=\"foo\" data-test=\"cool\"></span>").to_string();
1132 /// assert_eq!("<span code-test=\"foo\"></span>", a);
1133 pub fn rm_generic_attribute_prefixes<
1134 'b,
1135 T: 'b + ?Sized + Borrow<str>,
1136 I: IntoIter<Item = &'b T>,
1137 >(
1138 &mut self,
1139 it: I,
1140 ) -> &mut Self {
1141 if let Some(true) = self.generic_attribute_prefixes.as_mut().map(|prefixes| {
1142 for i in it {
1143 let _ = prefixes.remove(i.borrow());
1144 }
1145 prefixes.is_empty()
1146 }) {
1147 self.generic_attribute_prefixes = None;
1148 }
1149 self
1150 }
1151
1152 /// Returns a copy of the set of whitelisted attribute prefixes.
1153 ///
1154 /// # Examples
1155 ///
1156 /// use maplit::hashset;
1157 ///
1158 /// let generic_attribute_prefixes = hashset!["my-prfx-1-", "my-prfx-2-"];
1159 ///
1160 /// let mut b = ammonia::Builder::default();
1161 /// b.generic_attribute_prefixes(Clone::clone(&generic_attribute_prefixes));
1162 /// assert_eq!(Some(generic_attribute_prefixes), b.clone_generic_attribute_prefixes());
1163 pub fn clone_generic_attribute_prefixes(&self) -> Option<HashSet<&'a str>> {
1164 self.generic_attribute_prefixes.clone()
1165 }
1166
1167 /// Sets the attributes that are allowed on any tag.
1168 ///
1169 /// # Examples
1170 ///
1171 /// use ammonia::Builder;
1172 /// use maplit::hashset;
1173 ///
1174 /// # fn main() {
1175 /// let attributes = hashset!["data-val"];
1176 /// let a = Builder::new()
1177 /// .generic_attributes(attributes)
1178 /// .clean("<b data-val=1>")
1179 /// .to_string();
1180 /// assert_eq!(a, "<b data-val=\"1\"></b>");
1181 /// # }
1182 ///
1183 /// # Defaults
1184 ///
1185 /// ```notest
1186 /// lang, title
1187 /// ```
1188 pub fn generic_attributes(&mut self, value: HashSet<&'a str>) -> &mut Self {
1189 self.generic_attributes = value;
1190 self
1191 }
1192
1193 /// Add additonal whitelisted attributes without overwriting old ones.
1194 ///
1195 /// # Examples
1196 ///
1197 /// let a = ammonia::Builder::default()
1198 /// .add_generic_attributes(&["my-attr"])
1199 /// .clean("<span my-attr>mess</span>").to_string();
1200 /// assert_eq!("<span my-attr=\"\">mess</span>", a);
1201 pub fn add_generic_attributes<T: 'a + ?Sized + Borrow<str>, I: IntoIter<Item = &'a T>>(
1202 &mut self,
1203 it: I,
1204 ) -> &mut Self {
1205 self.generic_attributes
1206 .extend(it.into_iter().map(Borrow::borrow));
1207 self
1208 }
1209
1210 /// Remove already-whitelisted attributes.
1211 ///
1212 /// Does nothing if the attribute is already gone.
1213 ///
1214 /// # Examples
1215 ///
1216 /// let a = ammonia::Builder::default()
1217 /// .rm_generic_attributes(&["title"])
1218 /// .clean("<span title=\"cool\"></span>").to_string();
1219 /// assert_eq!("<span></span>", a);
1220 pub fn rm_generic_attributes<'b, T: 'b + ?Sized + Borrow<str>, I: IntoIter<Item = &'b T>>(
1221 &mut self,
1222 it: I,
1223 ) -> &mut Self {
1224 for i in it {
1225 self.generic_attributes.remove(i.borrow());
1226 }
1227 self
1228 }
1229
1230 /// Returns a copy of the set of whitelisted attributes.
1231 ///
1232 /// # Examples
1233 ///
1234 /// use maplit::hashset;
1235 ///
1236 /// let generic_attributes = hashset!["my-attr-1", "my-attr-2"];
1237 ///
1238 /// let mut b = ammonia::Builder::default();
1239 /// b.generic_attributes(Clone::clone(&generic_attributes));
1240 /// assert_eq!(generic_attributes, b.clone_generic_attributes());
1241 pub fn clone_generic_attributes(&self) -> HashSet<&'a str> {
1242 self.generic_attributes.clone()
1243 }
1244
1245 /// Sets the URL schemes permitted on `href` and `src` attributes.
1246 ///
1247 /// # Examples
1248 ///
1249 /// use ammonia::Builder;
1250 /// use maplit::hashset;
1251 ///
1252 /// # fn main() {
1253 /// let url_schemes = hashset![
1254 /// "http", "https", "mailto", "magnet"
1255 /// ];
1256 /// let a = Builder::new().url_schemes(url_schemes)
1257 /// .clean("<a href=\"magnet:?xt=urn:ed2k:31D6CFE0D16AE931B73C59D7E0C089C0&xl=0&dn=zero_len.fil&xt=urn:bitprint:3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ.LWPNACQDBZRYXW3VHJVCJ64QBZNGHOHHHZWCLNQ&xt=urn:md5:D41D8CD98F00B204E9800998ECF8427E\">zero-length file</a>")
1258 /// .to_string();
1259 ///
1260 /// // See `link_rel` for information on the rel="noopener noreferrer" attribute
1261 /// // in the cleaned HTML.
1262 /// assert_eq!(a,
1263 /// "<a href=\"magnet:?xt=urn:ed2k:31D6CFE0D16AE931B73C59D7E0C089C0&xl=0&dn=zero_len.fil&xt=urn:bitprint:3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ.LWPNACQDBZRYXW3VHJVCJ64QBZNGHOHHHZWCLNQ&xt=urn:md5:D41D8CD98F00B204E9800998ECF8427E\" rel=\"noopener noreferrer\">zero-length file</a>");
1264 /// # }
1265 ///
1266 /// # Defaults
1267 ///
1268 /// ```notest
1269 /// bitcoin, ftp, ftps, geo, http, https, im, irc,
1270 /// ircs, magnet, mailto, mms, mx, news, nntp,
1271 /// openpgp4fpr, sip, sms, smsto, ssh, tel, url,
1272 /// webcal, wtai, xmpp
1273 /// ```
1274 pub fn url_schemes(&mut self, value: HashSet<&'a str>) -> &mut Self {
1275 self.url_schemes = value;
1276 self
1277 }
1278
1279 /// Add additonal whitelisted URL schemes without overwriting old ones.
1280 ///
1281 /// # Examples
1282 ///
1283 /// let a = ammonia::Builder::default()
1284 /// .add_url_schemes(&["my-scheme"])
1285 /// .clean("<a href=my-scheme:home>mess</span>").to_string();
1286 /// assert_eq!("<a href=\"my-scheme:home\" rel=\"noopener noreferrer\">mess</a>", a);
1287 pub fn add_url_schemes<T: 'a + ?Sized + Borrow<str>, I: IntoIter<Item = &'a T>>(
1288 &mut self,
1289 it: I,
1290 ) -> &mut Self {
1291 self.url_schemes.extend(it.into_iter().map(Borrow::borrow));
1292 self
1293 }
1294
1295 /// Remove already-whitelisted attributes.
1296 ///
1297 /// Does nothing if the attribute is already gone.
1298 ///
1299 /// # Examples
1300 ///
1301 /// let a = ammonia::Builder::default()
1302 /// .rm_url_schemes(&["ftp"])
1303 /// .clean("<a href=\"ftp://ftp.mozilla.org/\"></a>").to_string();
1304 /// assert_eq!("<a rel=\"noopener noreferrer\"></a>", a);
1305 pub fn rm_url_schemes<'b, T: 'b + ?Sized + Borrow<str>, I: IntoIter<Item = &'b T>>(
1306 &mut self,
1307 it: I,
1308 ) -> &mut Self {
1309 for i in it {
1310 self.url_schemes.remove(i.borrow());
1311 }
1312 self
1313 }
1314
1315 /// Returns a copy of the set of whitelisted URL schemes.
1316 ///
1317 /// # Examples
1318 /// use maplit::hashset;
1319 ///
1320 /// let url_schemes = hashset!["my-scheme-1", "my-scheme-2"];
1321 ///
1322 /// let mut b = ammonia::Builder::default();
1323 /// b.url_schemes(Clone::clone(&url_schemes));
1324 /// assert_eq!(url_schemes, b.clone_url_schemes());
1325 pub fn clone_url_schemes(&self) -> HashSet<&'a str> {
1326 self.url_schemes.clone()
1327 }
1328
1329 /// Configures the behavior for relative URLs: pass-through, resolve-with-base, or deny.
1330 ///
1331 /// # Examples
1332 ///
1333 /// use ammonia::{Builder, UrlRelative};
1334 ///
1335 /// let a = Builder::new().url_relative(UrlRelative::PassThrough)
1336 /// .clean("<a href=/>Home</a>")
1337 /// .to_string();
1338 ///
1339 /// // See `link_rel` for information on the rel="noopener noreferrer" attribute
1340 /// // in the cleaned HTML.
1341 /// assert_eq!(
1342 /// a,
1343 /// "<a href=\"/\" rel=\"noopener noreferrer\">Home</a>");
1344 ///
1345 /// # Defaults
1346 ///
1347 /// ```notest
1348 /// UrlRelative::PassThrough
1349 /// ```
1350 pub fn url_relative(&mut self, value: UrlRelative<'a>) -> &mut Self {
1351 self.url_relative = value;
1352 self
1353 }
1354
1355 /// Allows rewriting of all attributes using a callback.
1356 ///
1357 /// The callback takes name of the element, attribute and its value.
1358 /// Returns `None` to remove the attribute, or a value to use.
1359 ///
1360 /// Rewriting of attributes with URLs is done before `url_relative()`.
1361 ///
1362 /// # Panics
1363 ///
1364 /// If more than one callback is set.
1365 ///
1366 /// # Examples
1367 ///
1368 /// ```rust
1369 /// use ammonia::Builder;
1370 /// let a = Builder::new()
1371 /// .attribute_filter(|element, attribute, value| {
1372 /// match (element, attribute) {
1373 /// ("img", "src") => None,
1374 /// _ => Some(value.into())
1375 /// }
1376 /// })
1377 /// .link_rel(None)
1378 /// .clean("<a href=/><img alt=Home src=foo></a>")
1379 /// .to_string();
1380 /// assert_eq!(a,
1381 /// r#"<a href="/"><img alt="Home"></a>"#);
1382 /// ```
1383 pub fn attribute_filter<'cb, CallbackFn>(&mut self, callback: CallbackFn) -> &mut Self
1384 where
1385 CallbackFn: for<'u> Fn(&str, &str, &'u str) -> Option<Cow<'u, str>> + Send + Sync + 'static,
1386 {
1387 assert!(
1388 self.attribute_filter.is_none(),
1389 "attribute_filter can be set only once"
1390 );
1391 self.attribute_filter = Some(Box::new(callback));
1392 self
1393 }
1394
1395 /// Returns `true` if the relative URL resolver is set to `Deny`.
1396 ///
1397 /// # Examples
1398 ///
1399 /// use ammonia::{Builder, UrlRelative};
1400 /// let mut a = Builder::default();
1401 /// a.url_relative(UrlRelative::Deny);
1402 /// assert!(a.is_url_relative_deny());
1403 /// a.url_relative(UrlRelative::PassThrough);
1404 /// assert!(!a.is_url_relative_deny());
1405 pub fn is_url_relative_deny(&self) -> bool {
1406 matches!(self.url_relative, UrlRelative::Deny)
1407 }
1408
1409 /// Returns `true` if the relative URL resolver is set to `PassThrough`.
1410 ///
1411 /// # Examples
1412 ///
1413 /// use ammonia::{Builder, UrlRelative};
1414 /// let mut a = Builder::default();
1415 /// a.url_relative(UrlRelative::Deny);
1416 /// assert!(!a.is_url_relative_pass_through());
1417 /// a.url_relative(UrlRelative::PassThrough);
1418 /// assert!(a.is_url_relative_pass_through());
1419 pub fn is_url_relative_pass_through(&self) -> bool {
1420 matches!(self.url_relative, UrlRelative::PassThrough)
1421 }
1422
1423 /// Returns `true` if the relative URL resolver is set to `Custom`.
1424 ///
1425 /// # Examples
1426 ///
1427 /// use ammonia::{Builder, UrlRelative};
1428 /// use std::borrow::Cow;
1429 /// fn test(a: &str) -> Option<Cow<str>> { None }
1430 /// # fn main() {
1431 /// let mut a = Builder::default();
1432 /// a.url_relative(UrlRelative::Custom(Box::new(test)));
1433 /// assert!(a.is_url_relative_custom());
1434 /// a.url_relative(UrlRelative::PassThrough);
1435 /// assert!(!a.is_url_relative_custom());
1436 /// a.url_relative(UrlRelative::Deny);
1437 /// assert!(!a.is_url_relative_custom());
1438 /// # }
1439 pub fn is_url_relative_custom(&self) -> bool {
1440 matches!(self.url_relative, UrlRelative::Custom(_))
1441 }
1442
1443 /// Configures a `rel` attribute that will be added on links.
1444 ///
1445 /// If `rel` is in the generic or tag attributes, this must be set to `None`.
1446 /// Common `rel` values to include:
1447 ///
1448 /// * `noopener`: This prevents [a particular type of XSS attack],
1449 /// and should usually be turned on for untrusted HTML.
1450 /// * `noreferrer`: This prevents the browser from [sending the source URL]
1451 /// to the website that is linked to.
1452 /// * `nofollow`: This prevents search engines from [using this link for
1453 /// ranking], which disincentivizes spammers.
1454 ///
1455 /// To turn on rel-insertion, call this function with a space-separated list.
1456 /// Ammonia does not parse rel-attributes;
1457 /// it just puts the given string into the attribute directly.
1458 ///
1459 /// [a particular type of XSS attack]: https://mathiasbynens.github.io/rel-noopener/
1460 /// [sending the source URL]: https://en.wikipedia.org/wiki/HTTP_referer
1461 /// [using this link for ranking]: https://en.wikipedia.org/wiki/Nofollow
1462 ///
1463 /// # Examples
1464 ///
1465 /// use ammonia::Builder;
1466 ///
1467 /// let a = Builder::new().link_rel(None)
1468 /// .clean("<a href=https://rust-lang.org/>Rust</a>")
1469 /// .to_string();
1470 /// assert_eq!(
1471 /// a,
1472 /// "<a href=\"https://rust-lang.org/\">Rust</a>");
1473 ///
1474 /// # Defaults
1475 ///
1476 /// ```notest
1477 /// Some("noopener noreferrer")
1478 /// ```
1479 pub fn link_rel(&mut self, value: Option<&'a str>) -> &mut Self {
1480 self.link_rel = value;
1481 self
1482 }
1483
1484 /// Returns the settings for links' `rel` attribute, if one is set.
1485 ///
1486 /// # Examples
1487 ///
1488 /// use ammonia::{Builder, UrlRelative};
1489 /// let mut a = Builder::default();
1490 /// a.link_rel(Some("a b"));
1491 /// assert_eq!(a.get_link_rel(), Some("a b"));
1492 pub fn get_link_rel(&self) -> Option<&str> {
1493 self.link_rel
1494 }
1495
1496 /// Sets the CSS classes that are allowed on specific tags.
1497 ///
1498 /// The values is structured as a map from tag names to a set of class names.
1499 ///
1500 /// If the `class` attribute is itself whitelisted for a tag, then adding entries to
1501 /// this map will cause a panic.
1502 ///
1503 /// # Examples
1504 ///
1505 /// use ammonia::Builder;
1506 /// use maplit::{hashmap, hashset};
1507 ///
1508 /// # fn main() {
1509 /// let allowed_classes = hashmap![
1510 /// "code" => hashset!["rs", "ex", "c", "cxx", "js"]
1511 /// ];
1512 /// let a = Builder::new()
1513 /// .allowed_classes(allowed_classes)
1514 /// .clean("<code class=rs>fn main() {}</code>")
1515 /// .to_string();
1516 /// assert_eq!(
1517 /// a,
1518 /// "<code class=\"rs\">fn main() {}</code>");
1519 /// # }
1520 ///
1521 /// # Defaults
1522 ///
1523 /// The set of allowed classes is empty by default.
1524 pub fn allowed_classes(&mut self, value: HashMap<&'a str, HashSet<&'a str>>) -> &mut Self {
1525 self.allowed_classes = value;
1526 self
1527 }
1528
1529 /// Add additonal whitelisted classes without overwriting old ones.
1530 ///
1531 /// # Examples
1532 ///
1533 /// let a = ammonia::Builder::default()
1534 /// .add_allowed_classes("a", &["onebox"])
1535 /// .clean("<a href=/ class=onebox>mess</span>").to_string();
1536 /// assert_eq!("<a href=\"/\" class=\"onebox\" rel=\"noopener noreferrer\">mess</a>", a);
1537 pub fn add_allowed_classes<
1538 T: 'a + ?Sized + Borrow<str>,
1539 U: 'a + ?Sized + Borrow<str>,
1540 I: IntoIter<Item = &'a T>,
1541 >(
1542 &mut self,
1543 tag: &'a U,
1544 it: I,
1545 ) -> &mut Self {
1546 self.allowed_classes
1547 .entry(tag.borrow())
1548 .or_default()
1549 .extend(it.into_iter().map(Borrow::borrow));
1550 self
1551 }
1552
1553 /// Remove already-whitelisted attributes.
1554 ///
1555 /// Does nothing if the attribute is already gone.
1556 ///
1557 /// # Examples
1558 ///
1559 /// let a = ammonia::Builder::default()
1560 /// .add_allowed_classes("span", &["active"])
1561 /// .rm_allowed_classes("span", &["active"])
1562 /// .clean("<span class=active>").to_string();
1563 /// assert_eq!("<span class=\"\"></span>", a);
1564 pub fn rm_allowed_classes<
1565 'b,
1566 'c,
1567 T: 'b + ?Sized + Borrow<str>,
1568 U: 'c + ?Sized + Borrow<str>,
1569 I: IntoIter<Item = &'b T>,
1570 >(
1571 &mut self,
1572 tag: &'c U,
1573 it: I,
1574 ) -> &mut Self {
1575 if let Some(tag) = self.allowed_classes.get_mut(tag.borrow()) {
1576 for i in it {
1577 tag.remove(i.borrow());
1578 }
1579 }
1580 self
1581 }
1582
1583 /// Returns a copy of the set of whitelisted class attributes.
1584 ///
1585 /// # Examples
1586 ///
1587 /// use maplit::{hashmap, hashset};
1588 ///
1589 /// let allowed_classes = hashmap![
1590 /// "my-tag" => hashset!["my-class-1", "my-class-2"]
1591 /// ];
1592 ///
1593 /// let mut b = ammonia::Builder::default();
1594 /// b.allowed_classes(Clone::clone(&allowed_classes));
1595 /// assert_eq!(allowed_classes, b.clone_allowed_classes());
1596 pub fn clone_allowed_classes(&self) -> HashMap<&'a str, HashSet<&'a str>> {
1597 self.allowed_classes.clone()
1598 }
1599
1600 /// Configures the handling of HTML comments.
1601 ///
1602 /// If this option is false, comments will be preserved.
1603 ///
1604 /// # Examples
1605 ///
1606 /// use ammonia::Builder;
1607 ///
1608 /// let a = Builder::new().strip_comments(false)
1609 /// .clean("<!-- yes -->")
1610 /// .to_string();
1611 /// assert_eq!(
1612 /// a,
1613 /// "<!-- yes -->");
1614 ///
1615 /// # Defaults
1616 ///
1617 /// `true`
1618 pub fn strip_comments(&mut self, value: bool) -> &mut Self {
1619 self.strip_comments = value;
1620 self
1621 }
1622
1623 /// Returns `true` if comment stripping is turned on.
1624 ///
1625 /// # Examples
1626 ///
1627 /// let mut a = ammonia::Builder::new();
1628 /// a.strip_comments(true);
1629 /// assert!(a.will_strip_comments());
1630 /// a.strip_comments(false);
1631 /// assert!(!a.will_strip_comments());
1632 pub fn will_strip_comments(&self) -> bool {
1633 self.strip_comments
1634 }
1635
1636 /// Prefixes all "id" attribute values with a given string. Note that the tag and
1637 /// attribute themselves must still be whitelisted.
1638 ///
1639 /// # Examples
1640 ///
1641 /// use ammonia::Builder;
1642 /// use maplit::hashset;
1643 ///
1644 /// # fn main() {
1645 /// let attributes = hashset!["id"];
1646 /// let a = Builder::new()
1647 /// .generic_attributes(attributes)
1648 /// .id_prefix(Some("safe-"))
1649 /// .clean("<b id=42>")
1650 /// .to_string();
1651 /// assert_eq!(a, "<b id=\"safe-42\"></b>");
1652 /// # }
1653
1654 ///
1655 /// # Defaults
1656 ///
1657 /// `None`
1658 pub fn id_prefix(&mut self, value: Option<&'a str>) -> &mut Self {
1659 self.id_prefix = value;
1660 self
1661 }
1662
1663 /// Only allows the specified properties in `style` attributes.
1664 ///
1665 /// Irrelevant if `style` is not an allowed attribute.
1666 ///
1667 /// Note that if style filtering is enabled style properties will be normalised e.g.
1668 /// invalid declarations and @rules will be removed, with only syntactically valid
1669 /// declarations kept.
1670 ///
1671 /// # Examples
1672 ///
1673 /// use ammonia::Builder;
1674 /// use maplit::hashset;
1675 ///
1676 /// # fn main() {
1677 /// let attributes = hashset!["style"];
1678 /// let properties = hashset!["color"];
1679 /// let a = Builder::new()
1680 /// .generic_attributes(attributes)
1681 /// .filter_style_properties(properties)
1682 /// .clean("<p style=\"font-weight: heavy; color: red\">my html</p>")
1683 /// .to_string();
1684 /// assert_eq!(a, "<p style=\"color:red\">my html</p>");
1685 /// # }
1686 pub fn filter_style_properties(&mut self, value: HashSet<&'a str>) -> &mut Self {
1687 self.style_properties = Some(value);
1688 self
1689 }
1690
1691 /// Constructs a [`Builder`] instance configured with the [default options].
1692 ///
1693 /// # Examples
1694 ///
1695 /// use ammonia::{Builder, Url, UrlRelative};
1696 /// # use std::error::Error;
1697 ///
1698 /// # fn do_main() -> Result<(), Box<dyn Error>> {
1699 /// let input = "<!-- comments will be stripped -->This is an <a href=.>Ammonia</a> example using <a href=struct.Builder.html#method.new onclick=xss>the <code onmouseover=xss>new()</code> function</a>.";
1700 /// let output = "This is an <a href=\"https://docs.rs/ammonia/1.0/ammonia/\" rel=\"noopener noreferrer\">Ammonia</a> example using <a href=\"https://docs.rs/ammonia/1.0/ammonia/struct.Builder.html#method.new\" rel=\"noopener noreferrer\">the <code>new()</code> function</a>.";
1701 ///
1702 /// let result = Builder::new() // <--
1703 /// .url_relative(UrlRelative::RewriteWithBase(Url::parse("https://docs.rs/ammonia/1.0/ammonia/")?))
1704 /// .clean(input)
1705 /// .to_string();
1706 /// assert_eq!(result, output);
1707 /// # Ok(())
1708 /// # }
1709 /// # fn main() { do_main().unwrap() }
1710 ///
1711 /// [default options]: fn.clean.html
1712 /// [`Builder`]: struct.Builder.html
1713 pub fn new() -> Self {
1714 Self::default()
1715 }
1716
1717 /// Constructs a [`Builder`] instance configured with no allowed tags.
1718 ///
1719 /// # Examples
1720 ///
1721 /// use ammonia::{Builder, Url, UrlRelative};
1722 /// # use std::error::Error;
1723 ///
1724 /// # fn do_main() -> Result<(), Box<dyn Error>> {
1725 /// let input = "<!-- comments will be stripped -->This is an <a href=.>Ammonia</a> example using <a href=struct.Builder.html#method.new onclick=xss>the <code onmouseover=xss>empty()</code> function</a>.";
1726 /// let output = "This is an Ammonia example using the empty() function.";
1727 ///
1728 /// let result = Builder::empty() // <--
1729 /// .url_relative(UrlRelative::RewriteWithBase(Url::parse("https://docs.rs/ammonia/1.0/ammonia/")?))
1730 /// .clean(input)
1731 /// .to_string();
1732 /// assert_eq!(result, output);
1733 /// # Ok(())
1734 /// # }
1735 /// # fn main() { do_main().unwrap() }
1736 ///
1737 /// [default options]: fn.clean.html
1738 /// [`Builder`]: struct.Builder.html
1739 pub fn empty() -> Self {
1740 Self {
1741 tags: hashset![],
1742 ..Self::default()
1743 }
1744 }
1745
1746 /// Sanitizes an HTML fragment in a string according to the configured options.
1747 ///
1748 /// # Examples
1749 ///
1750 /// use ammonia::{Builder, Url, UrlRelative};
1751 /// # use std::error::Error;
1752 ///
1753 /// # fn do_main() -> Result<(), Box<dyn Error>> {
1754 /// let input = "<!-- comments will be stripped -->This is an <a href=.>Ammonia</a> example using <a href=struct.Builder.html#method.new onclick=xss>the <code onmouseover=xss>new()</code> function</a>.";
1755 /// let output = "This is an <a href=\"https://docs.rs/ammonia/1.0/ammonia/\" rel=\"noopener noreferrer\">Ammonia</a> example using <a href=\"https://docs.rs/ammonia/1.0/ammonia/struct.Builder.html#method.new\" rel=\"noopener noreferrer\">the <code>new()</code> function</a>.";
1756 ///
1757 /// let result = Builder::new()
1758 /// .url_relative(UrlRelative::RewriteWithBase(Url::parse("https://docs.rs/ammonia/1.0/ammonia/")?))
1759 /// .clean(input)
1760 /// .to_string(); // <--
1761 /// assert_eq!(result, output);
1762 /// # Ok(())
1763 /// # }
1764 /// # fn main() { do_main().unwrap() }
1765 pub fn clean(&self, src: &str) -> Document {
1766 let parser = Self::make_parser();
1767 let dom = parser.one(src);
1768 self.clean_dom(dom)
1769 }
1770
1771 /// Sanitizes an HTML fragment from a reader according to the configured options.
1772 ///
1773 /// The input should be in UTF-8 encoding, otherwise the decoding is lossy, just
1774 /// like when using [`String::from_utf8_lossy`].
1775 ///
1776 /// To avoid consuming the reader, a mutable reference can be passed to this method.
1777 ///
1778 /// # Examples
1779 ///
1780 /// use ammonia::Builder;
1781 /// # use std::error::Error;
1782 ///
1783 /// # fn do_main() -> Result<(), Box<dyn Error>> {
1784 /// let a = Builder::new()
1785 /// .clean_from_reader(&b"<!-- no -->"[..])? // notice the `b`
1786 /// .to_string();
1787 /// assert_eq!(a, "");
1788 /// # Ok(()) }
1789 /// # fn main() { do_main().unwrap() }
1790 ///
1791 /// [`String::from_utf8_lossy`]: https://doc.rust-lang.org/std/string/struct.String.html#method.from_utf8_lossy
1792 pub fn clean_from_reader<R>(&self, mut src: R) -> io::Result<Document>
1793 where
1794 R: io::Read,
1795 {
1796 let parser = Self::make_parser().from_utf8();
1797 let dom = parser.read_from(&mut src)?;
1798 Ok(self.clean_dom(dom))
1799 }
1800
1801 /// Clean a post-parsing DOM.
1802 ///
1803 /// This is not a public API because RcDom isn't really stable.
1804 /// We want to be able to take breaking changes to html5ever itself
1805 /// without having to break Ammonia's API.
1806 fn clean_dom(&self, dom: RcDom) -> Document {
1807 let mut id_to_tag_name_map = HashMap::new();
1808 let mut id_to_tag_name_stack = vec![{
1809 let children = dom.document.children.borrow();
1810 children[0].clone()
1811 }];
1812 while let Some(tag) = id_to_tag_name_stack.pop() {
1813 if let NodeData::Element { name, attrs, .. } = &tag.data {
1814 let attrs = attrs.borrow();
1815 for attr in &attrs[..] {
1816 if &*attr.name.local == "id" {
1817 id_to_tag_name_map.entry(attr.value.to_string()).and_modify(|ent| *ent = None).or_insert_with(|| Some(name.local.to_string()));
1818 }
1819 }
1820 }
1821 id_to_tag_name_stack.extend(tag.children.borrow().iter().map(|x| x.clone()));
1822 }
1823
1824 let mut stack = Vec::new();
1825 let mut removed = Vec::new();
1826 let link_rel = self
1827 .link_rel
1828 .map(|link_rel| format_tendril!("{}", link_rel));
1829 if link_rel.is_some() {
1830 assert!(self.generic_attributes.get("rel").is_none());
1831 assert!(self
1832 .tag_attributes
1833 .get("a")
1834 .and_then(|a| a.get("rel"))
1835 .is_none());
1836 }
1837 assert!(self.allowed_classes.is_empty() || !self.generic_attributes.contains("class"));
1838 for tag_name in self.allowed_classes.keys() {
1839 assert!(self
1840 .tag_attributes
1841 .get(tag_name)
1842 .and_then(|a| a.get("class"))
1843 .is_none());
1844 }
1845 for tag_name in &self.clean_content_tags {
1846 assert!(!self.tags.contains(tag_name), "`{tag_name}` appears in `clean_content_tags` and in `tags` at the same time");
1847 assert!(!self.tag_attributes.contains_key(tag_name), "`{tag_name}` appears in `clean_content_tags` and in `tag_attributes` at the same time");
1848 }
1849 let body = {
1850 let children = dom.document.children.borrow();
1851 children[0].clone()
1852 };
1853 stack.extend(
1854 mem::take(&mut *body.children.borrow_mut())
1855 .into_iter()
1856 .rev(),
1857 );
1858 // This design approach is used to prevent pathological content from producing
1859 // a stack overflow. The `stack` contains to-be-cleaned nodes, while `remove`,
1860 // of course, contains nodes that need to be dropped (we can't just drop them,
1861 // because they could have a very deep child tree).
1862 while let Some(mut node) = stack.pop() {
1863 if matches!(node.data, NodeData::Element { ref name, .. } if &*name.local == "selectedcontent" && name.ns == ns!(html)) &&
1864 self.is_within(node.clone(), ns!(html), "select")
1865 {
1866 for sub in node.children.borrow_mut().iter_mut() {
1867 sub.parent.replace(None);
1868 }
1869 *node.children.borrow_mut() = Vec::new();
1870 }
1871 let parent = node.parent
1872 .replace(None).expect("a node in the DOM will have a parent, except the root, which is not processed")
1873 .upgrade().expect("a node's parent will be pointed to by its parent (or the root pointer), and will not be dropped");
1874 let pass = self.clean_child(&mut node, &parent, &id_to_tag_name_map);
1875 self.adjust_node_attributes(&mut node, &link_rel, self.id_prefix, &parent, &id_to_tag_name_map);
1876 if self.clean_node_content(&node) || !self.check_expected_namespace(&parent, &node) {
1877 removed.push(node);
1878 continue;
1879 }
1880 if pass {
1881 dom.append(&parent.clone(), NodeOrText::AppendNode(node.clone()));
1882 } else {
1883 for sub in node.children.borrow_mut().iter_mut() {
1884 sub.parent.replace(Some(Rc::downgrade(&parent)));
1885 }
1886 }
1887 stack.extend(
1888 mem::take(&mut *node.children.borrow_mut())
1889 .into_iter()
1890 .rev(),
1891 );
1892 if !pass {
1893 removed.push(node);
1894 }
1895 }
1896 // Now, imperatively clean up all of the child nodes.
1897 // Otherwise, we could wind up with a DoS, either caused by a memory leak,
1898 // or caused by a stack overflow.
1899 while let Some(node) = removed.pop() {
1900 removed.extend_from_slice(&mem::take(&mut *node.children.borrow_mut())[..]);
1901 }
1902 Document(dom)
1903 }
1904
1905 fn is_within(&self, mut child: Handle, ns: Namespace, tag: &str) -> bool {
1906 while let Some(parent) = child.parent.take() {
1907 child.parent.set(Some(parent.clone()));
1908 match child.data {
1909 NodeData::Element { ref name, .. } if name.ns == ns && &*name.local == tag => return true,
1910 _ => {
1911 if let Some(parent) = parent.upgrade() {
1912 child = parent;
1913 } else {
1914 return false;
1915 }
1916 }
1917 }
1918 }
1919 false
1920 }
1921
1922 /// Returns `true` if a node and all its content should be removed.
1923 fn clean_node_content(&self, node: &Handle) -> bool {
1924 match node.data {
1925 NodeData::Text { .. }
1926 | NodeData::Comment { .. }
1927 | NodeData::Doctype { .. }
1928 | NodeData::Document
1929 | NodeData::ProcessingInstruction { .. } => false,
1930 NodeData::Element { ref name, .. } => self.clean_content_tags.contains(&*name.local),
1931 }
1932 }
1933
1934 /// Remove unwanted attributes, and check if the node should be kept or not.
1935 ///
1936 /// The root node doesn't need cleaning because we create the root node ourselves,
1937 /// and it doesn't get serialized, and ... it just exists to give the parser
1938 /// a context (in this case, a div-like block context).
1939 fn clean_child(&self, child: &mut Handle, parent: &Handle, id_to_tag_name_map: &HashMap<String, Option<String>>) -> bool {
1940 match child.data {
1941 NodeData::Text { .. } => true,
1942 NodeData::Comment { .. } => !self.strip_comments,
1943 NodeData::Doctype { .. }
1944 | NodeData::Document
1945 | NodeData::ProcessingInstruction { .. } => false,
1946 NodeData::Element {
1947 ref name,
1948 ref attrs,
1949 ..
1950 } => {
1951 if self.tags.contains(&*name.local) {
1952 let whitelisted = |tag_name: &str, attr_name: &str, attr_val: &str|
1953 self.generic_attributes.contains(attr_name)
1954 || self.generic_attribute_prefixes.as_ref().map(|prefixes| {
1955 prefixes.iter().any(|&p| attr_name.starts_with(p))
1956 }) == Some(true)
1957 || self
1958 .tag_attributes
1959 .get(tag_name)
1960 .map(|ta| ta.contains(attr_name))
1961 == Some(true)
1962 || self
1963 .tag_attribute_values
1964 .get(tag_name)
1965 .and_then(|tav| tav.get(attr_name))
1966 .map(|vs| {
1967 vs.iter().any(|v| v.to_lowercase() == attr_val.to_lowercase())
1968 })
1969 == Some(true);
1970 let attr_filter = |tag_name: &str, attr_name: &str, attr_val: &str| {
1971 if !whitelisted(tag_name, attr_name, attr_val) {
1972 // If the class attribute is not whitelisted,
1973 // but there is a whitelisted set of allowed_classes,
1974 // do not strip out the class attribute.
1975 // Banned classes will be filtered later.
1976 attr_name == "class" && self.allowed_classes.contains_key(tag_name)
1977 } else if is_url_attr(tag_name, attr_name) {
1978 let url = Url::parse(attr_val);
1979 if let Ok(url) = url {
1980 self.url_schemes.contains(url.scheme())
1981 } else if url == Err(url::ParseError::RelativeUrlWithoutBase) {
1982 !matches!(self.url_relative, UrlRelative::Deny)
1983 } else {
1984 false
1985 }
1986 } else {
1987 true
1988 }
1989 };
1990 attrs.borrow_mut().retain(|attr| attr_filter(&*name.local, &*attr.name.local, &*attr.value));
1991 if
1992 // https://svgwg.org/specs/animations/#AnimateElement
1993 name.ns == ns!(svg) &&
1994 (&*name.local == "animate" || &*name.local == "set")
1995 {
1996 let animate_name = attrs.borrow()
1997 .iter()
1998 .find(|attr| &*attr.name.local == "attributeName")
1999 .map(|attr| attr.value.clone());
2000 let animate_values = attrs.borrow()
2001 .iter()
2002 .find(|attr| &*attr.name.local == "values")
2003 .map(|attr| attr.value.clone());
2004 let animate_from = attrs.borrow()
2005 .iter()
2006 .find(|attr| &*attr.name.local == "from")
2007 .map(|attr| attr.value.clone());
2008 let animate_to = attrs.borrow()
2009 .iter()
2010 .find(|attr| &*attr.name.local == "to")
2011 .map(|attr| attr.value.clone());
2012 let animate_href = attrs.borrow()
2013 .iter()
2014 .find(|attr| &*attr.name.local == "href")
2015 .map(|attr| attr.value.clone());
2016 let animate_tag_name = animate_href
2017 .map(|href| {
2018 if href.starts_with("#") {
2019 id_to_tag_name_map.get(&href[1..]).and_then(|inner| Some(&inner.as_ref()?[..]))
2020 } else {
2021 None
2022 }
2023 })
2024 .unwrap_or_else(|| {
2025 if let &NodeData::Element { name: ref parent_name, .. } = &parent.data {
2026 Some(&*parent_name.local)
2027 } else {
2028 None
2029 }
2030 });
2031 match (animate_name, animate_values, animate_from, animate_to, animate_tag_name) {
2032 (Some(animate_name), _, _, _, Some(animate_tag_name)) if self.set_tag_attribute_values.get(animate_tag_name).map_or(false, |attribute_values| attribute_values.contains_key(&*animate_name)) => false,
2033 (Some(animate_name), Some(animate_values), None, None, Some(animate_tag_name)) => {
2034 // https://svgwg.org/specs/animations/#ValuesAttribute
2035 animate_values.split(';').all(|attr_val| attr_filter(animate_tag_name, &*animate_name, attr_val))
2036 }
2037 (Some(animate_name), None, Some(animate_from), Some(animate_to), Some(animate_tag_name)) => {
2038 // https://svgwg.org/specs/animations/#FromAttribute
2039 attr_filter(animate_tag_name, &*animate_name, &*animate_from) &&
2040 attr_filter(animate_tag_name, &*animate_name, &*animate_to)
2041 }
2042 (Some(animate_name), None, Some(animate_from), None, Some(animate_tag_name)) => {
2043 // https://svgwg.org/specs/animations/#FromAttribute
2044 attr_filter(animate_tag_name, &*animate_name, &*animate_from)
2045 }
2046 (Some(animate_name), None, None, Some(animate_to), Some(animate_tag_name)) => {
2047 // https://svgwg.org/specs/animations/#FromAttribute
2048 attr_filter(animate_tag_name, &*animate_name, &*animate_to)
2049 }
2050 _ => false,
2051 }
2052 } else {
2053 true
2054 }
2055 } else {
2056 false
2057 }
2058 }
2059 }
2060 }
2061
2062 // Check for unexpected namespace changes.
2063 //
2064 // The issue happens if developers added to the list of allowed tags any
2065 // tag which is parsed in RCDATA state, PLAINTEXT state or RAWTEXT state,
2066 // that is:
2067 //
2068 // * title
2069 // * textarea
2070 // * xmp
2071 // * iframe
2072 // * noembed
2073 // * noframes
2074 // * plaintext
2075 // * noscript
2076 // * style
2077 // * script
2078 //
2079 // An example in the wild is Plume, that allows iframe [1]. So in next
2080 // examples I'll assume the following policy:
2081 //
2082 // Builder::new()
2083 // .add_tags(&["iframe"])
2084 //
2085 // In HTML namespace `<iframe>` is parsed specially; that is, its content is
2086 // treated as text. For instance, the following html:
2087 //
2088 // <iframe><a>test
2089 //
2090 // Is parsed into the following DOM tree:
2091 //
2092 // iframe
2093 // └─ #text: <a>test
2094 //
2095 // So iframe cannot have any children other than a text node.
2096 //
2097 // The same is not true, though, in "foreign content"; that is, within
2098 // <svg> or <math> tags. The following html:
2099 //
2100 // <svg><iframe><a>test
2101 //
2102 // is parsed differently:
2103 //
2104 // svg
2105 // └─ iframe
2106 // └─ a
2107 // └─ #text: test
2108 //
2109 // So in SVG namespace iframe can have children.
2110 //
2111 // Ammonia disallows <svg> but it keeps its content after deleting it. And
2112 // the parser internally keeps track of the namespace of the element. So
2113 // assume we have the following snippet:
2114 //
2115 // <svg><iframe><a title="</iframe><img src onerror=alert(1)>">test
2116 //
2117 // It is parsed into:
2118 //
2119 // svg
2120 // └─ iframe
2121 // └─ a title="</iframe><img src onerror=alert(1)>"
2122 // └─ #text: test
2123 //
2124 // This DOM tree is harmless from ammonia point of view because the piece
2125 // of code that looks like XSS is in a title attribute. Hence, the
2126 // resulting "safe" HTML from ammonia would be:
2127 //
2128 // <iframe><a title="</iframe><img src onerror=alert(1)>" rel="noopener
2129 // noreferrer">test</a></iframe>
2130 //
2131 // However, at this point, the information about namespace is lost, which
2132 // means that the browser will parse this snippet into:
2133 //
2134 // ├─ iframe
2135 // │ └─ #text: <a title="
2136 // ├─ img src="" onerror="alert(1)"
2137 // └─ #text: " rel="noopener noreferrer">test
2138 //
2139 // Leading to XSS.
2140 //
2141 // To solve this issue, check for unexpected namespace switches after cleanup.
2142 // Elements which change namespace at an unexpected point are removed.
2143 // This function returns `true` if `child` should be kept, and `false` if it
2144 // should be removed.
2145 //
2146 // [1]: https://github.com/Plume-org/Plume/blob/main/plume-models/src/safe_string.rs#L21
2147 fn check_expected_namespace(&self, parent: &Handle, child: &Handle) -> bool {
2148 let (parent, parent_attr, child) = match (&parent.data, &child.data) {
2149 (NodeData::Element { name: pn, attrs, .. }, NodeData::Element { name: cn, .. }) => (pn, attrs, cn),
2150 _ => return true,
2151 };
2152 // The only way to switch from html to svg is with the <svg> tag
2153 if parent.ns == ns!(html) && child.ns == ns!(svg) {
2154 child.local == local_name!("svg")
2155 // The only way to switch from html to mathml is with the <math> tag
2156 } else if parent.ns == ns!(html) && child.ns == ns!(mathml) {
2157 child.local == local_name!("math")
2158 // The only way to switch from mathml to svg/html is with a text integration point
2159 } else if parent.ns == ns!(mathml) && child.ns != ns!(mathml) {
2160 // https://html.spec.whatwg.org/#mathml
2161 if &*parent.local == "annotation-xml" {
2162 let parent_attr = parent_attr.borrow();
2163 // https://html.spec.whatwg.org/#tree-construction
2164 if child.ns == ns!(html)
2165 && parent_attr
2166 .iter()
2167 .filter(|attr| attr.name.local == local_name!("encoding"))
2168 .all(|attr| {
2169 &*attr.value == "text/html" || &*attr.value == "application/xhtml+xml"
2170 })
2171 {
2172 is_html_tag(&child.local)
2173 && parent_attr
2174 .iter()
2175 .filter(|attr| attr.name.local == local_name!("encoding"))
2176 .count()
2177 == 1
2178 } else {
2179 child.local == local_name!("svg") && child.ns == ns!(svg)
2180 }
2181 } else {
2182 matches!(&*parent.local, "mi" | "mo" | "mn" | "ms" | "mtext")
2183 && if child.ns == ns!(html) {
2184 is_html_tag(&child.local)
2185 } else {
2186 true
2187 }
2188 }
2189
2190 // The only way to switch from svg to mathml/html is with an html integration point
2191 } else if parent.ns == ns!(svg) && child.ns != ns!(svg) {
2192 // https://html.spec.whatwg.org/#svg-0
2193 matches!(&*parent.local, "foreignObject")
2194 && if child.ns == ns!(html) { is_html_tag(&child.local) } else { true }
2195 } else if child.ns == ns!(svg) {
2196 is_svg_tag(&child.local)
2197 } else if child.ns == ns!(mathml) {
2198 is_mathml_tag(&child.local)
2199 } else if child.ns == ns!(html) {
2200 is_html_tag(&child.local)
2201 } else {
2202 // There are no other supported ways to switch namespace
2203 parent.ns == child.ns
2204 }
2205 }
2206
2207 /// Add and transform special-cased attributes and elements.
2208 ///
2209 /// This function handles:
2210 ///
2211 /// * relative URL rewriting
2212 /// * adding `<a rel>` attributes
2213 /// * filtering out banned style properties
2214 /// * filtering out banned classes
2215 fn adjust_node_attributes(
2216 &self,
2217 child: &mut Handle,
2218 link_rel: &Option<StrTendril>,
2219 id_prefix: Option<&'a str>,
2220 parent: &Handle,
2221 id_to_tag_name_map: &HashMap<String, Option<String>>,
2222 ) {
2223 if let NodeData::Element {
2224 ref name,
2225 ref attrs,
2226 ..
2227 } = child.data
2228 {
2229 if let Some(set_attrs) = self.set_tag_attribute_values.get(&*name.local) {
2230 let mut attrs = attrs.borrow_mut();
2231 for (&set_name, &set_value) in set_attrs {
2232 // set the value of the attribute if the attribute is already present
2233 if let Some(attr) = attrs.iter_mut().find(|attr| &*attr.name.local == set_name)
2234 {
2235 if &*attr.value != set_value {
2236 attr.value = set_value.into();
2237 }
2238 } else {
2239 // otherwise, add the attribute
2240 let attr = Attribute {
2241 name: QualName::new(None, ns!(), set_name.into()),
2242 value: set_value.into(),
2243 };
2244 attrs.push(attr);
2245 }
2246 }
2247 }
2248 if let Some(ref link_rel) = *link_rel {
2249 if &*name.local == "a" {
2250 attrs.borrow_mut().push(Attribute {
2251 name: QualName::new(None, ns!(), local_name!("rel")),
2252 value: link_rel.clone(),
2253 })
2254 }
2255 }
2256 if let Some(ref id_prefix) = id_prefix {
2257 for attr in &mut *attrs.borrow_mut() {
2258 if &attr.name.local == "id" && !attr.value.starts_with(id_prefix) {
2259 attr.value = format_tendril!("{}{}", id_prefix, attr.value);
2260 }
2261 }
2262 }
2263 if let Some(ref attr_filter) = self.attribute_filter {
2264 let mut drop_attrs = Vec::new();
2265 let mut attrs = attrs.borrow_mut();
2266 for (i, attr) in &mut attrs.iter_mut().enumerate() {
2267 let replace_with = if let Some(new) =
2268 attr_filter.filter(&name.local, &attr.name.local, &attr.value)
2269 {
2270 if *new != *attr.value {
2271 Some(format_tendril!("{}", new))
2272 } else {
2273 None // no need to replace the attr if filter returned the same value
2274 }
2275 } else {
2276 drop_attrs.push(i);
2277 None
2278 };
2279 if let Some(replace_with) = replace_with {
2280 attr.value = replace_with;
2281 }
2282 }
2283 for i in drop_attrs.into_iter().rev() {
2284 attrs.swap_remove(i);
2285 }
2286 }
2287 {
2288 let mut drop_attrs = Vec::new();
2289 let mut attrs = attrs.borrow_mut();
2290 for (i, attr) in attrs.iter_mut().enumerate() {
2291 if is_url_attr(&name.local, &attr.name.local) && is_url_relative(&attr.value) {
2292 let new_value = self.url_relative.evaluate(&attr.value);
2293 if let Some(new_value) = new_value {
2294 attr.value = new_value;
2295 } else {
2296 drop_attrs.push(i);
2297 }
2298 }
2299 }
2300 // Swap remove scrambles the vector after the current point.
2301 // We will not do anything except with items before the current point.
2302 // The `rev()` is, as such, necessary for correctness.
2303 // We could use regular `remove(usize)` and a forward iterator,
2304 // but that's slower.
2305 for i in drop_attrs.into_iter().rev() {
2306 attrs.swap_remove(i);
2307 }
2308 }
2309 if let Some(allowed_values) = &self.style_properties {
2310 for attr in &mut *attrs.borrow_mut() {
2311 if &attr.name.local == "style" {
2312 attr.value = style::filter_style_attribute(&attr.value, allowed_values).into();
2313 }
2314 }
2315 }
2316 if let Some(allowed_values) = self.allowed_classes.get(&*name.local) {
2317 for attr in &mut *attrs.borrow_mut() {
2318 if &attr.name.local == "class" {
2319 let mut classes = vec![];
2320 // https://html.spec.whatwg.org/#global-attributes:classes-2
2321 for class in attr.value.split_ascii_whitespace() {
2322 if allowed_values.contains(class) {
2323 classes.push(class.to_owned());
2324 }
2325 }
2326 attr.value = format_tendril!("{}", classes.join(" "));
2327 }
2328 }
2329 }
2330 if
2331 // https://svgwg.org/specs/animations/#AnimateElement
2332 name.ns == ns!(svg) &&
2333 (&*name.local == "animate" || &*name.local == "set")
2334 {
2335 let mut attrs = attrs.borrow_mut();
2336 let animate_name = attrs
2337 .iter()
2338 .find(|attr| &*attr.name.local == "attributeName")
2339 .map(|attr| attr.value.clone());
2340 let animate_href = attrs
2341 .iter()
2342 .find(|attr| &*attr.name.local == "href")
2343 .map(|attr| attr.value.clone());
2344 let animate_tag_name = animate_href
2345 .map(|href| {
2346 if href.starts_with("#") {
2347 id_to_tag_name_map.get(&href[1..]).and_then(|inner| Some(&inner.as_ref()?[..]))
2348 } else {
2349 None
2350 }
2351 })
2352 .unwrap_or_else(|| {
2353 if let &NodeData::Element { name: ref parent_name, .. } = &parent.data {
2354 Some(&*parent_name.local)
2355 } else {
2356 None
2357 }
2358 });
2359 if let (Some(animate_name), Some(animate_tag_name)) = (animate_name, animate_tag_name) {
2360 if let Some(ref attr_filter) = self.attribute_filter {
2361 if let Some((i, animate_values)) = attrs
2362 .iter_mut()
2363 .enumerate()
2364 .find(|(_, attr)| &*attr.name.local == "values")
2365 .map(|(i, attr)| (i, &mut attr.value))
2366 {
2367 let mut drop = false;
2368 let new_value = animate_values.split(';')
2369 .map(|value| {
2370 if let Some(new_value) = attr_filter.filter(animate_tag_name, &animate_name, &value) {
2371 String::from(new_value)
2372 } else {
2373 drop = true;
2374 String::new()
2375 }
2376 })
2377 .collect::<Vec<String>>()
2378 .join(";");
2379 if drop {
2380 attrs.swap_remove(i);
2381 } else {
2382 *animate_values = new_value.into();
2383 }
2384 }
2385 let mut drop_attrs = Vec::new();
2386 for (i, animate_value) in attrs
2387 .iter_mut()
2388 .enumerate()
2389 .filter(|(_, attr)| &*attr.name.local == "from" || &*attr.name.local == "to")
2390 .map(|(i, attr)| (i, &mut attr.value))
2391 {
2392 if let Some(new_value) = attr_filter.filter(animate_tag_name, &animate_name, &animate_value) {
2393 *animate_value = new_value[..].into();
2394 } else {
2395 drop_attrs.push(i);
2396 };
2397 }
2398 for i in drop_attrs.into_iter().rev() {
2399 attrs.swap_remove(i);
2400 }
2401 }
2402 if is_url_attr(animate_tag_name, &*animate_name) {
2403 if let Some((i, animate_values)) = attrs
2404 .iter_mut()
2405 .enumerate()
2406 .find(|(_, attr)| &*attr.name.local == "values")
2407 .map(|(i, attr)| (i, &mut attr.value))
2408 {
2409 let mut drop = false;
2410 let new_value = animate_values.split(';')
2411 .map(|value| {
2412 if !is_url_relative(value) {
2413 String::from(value)
2414 } else if let Some(new_value) = self.url_relative.evaluate(value) {
2415 String::from(new_value)
2416 } else {
2417 drop = true;
2418 String::new()
2419 }
2420 })
2421 .collect::<Vec<String>>()
2422 .join(";");
2423 if drop {
2424 attrs.swap_remove(i);
2425 } else {
2426 *animate_values = new_value.into();
2427 }
2428 }
2429 let mut drop_attrs = Vec::new();
2430 for (i, animate_value) in attrs
2431 .iter_mut()
2432 .enumerate()
2433 .filter(|(_, attr)| &*attr.name.local == "from" || &*attr.name.local == "to")
2434 .map(|(i, attr)| (i, &mut attr.value))
2435 {
2436 if !is_url_relative(animate_value) {
2437 // do nothing
2438 } else if let Some(new_value) = self.url_relative.evaluate(animate_value) {
2439 *animate_value = new_value;
2440 } else {
2441 drop_attrs.push(i);
2442 };
2443 }
2444 for i in drop_attrs.into_iter().rev() {
2445 attrs.swap_remove(i);
2446 }
2447 }
2448 if &*animate_name == "style" {
2449 if let Some(allowed_values) = &self.style_properties {
2450 if let Some(animate_values) = attrs
2451 .iter_mut()
2452 .find(|attr| &*attr.name.local == "values")
2453 .map(|attr| &mut attr.value)
2454 {
2455 let new_value = animate_values.split(';')
2456 .map(|value| {
2457 style::filter_style_attribute(&value, allowed_values)
2458 })
2459 .collect::<Vec<String>>()
2460 .join(";");
2461 *animate_values = new_value.into();
2462 }
2463 for animate_value in attrs
2464 .iter_mut()
2465 .filter(|attr| &*attr.name.local == "from" || &*attr.name.local == "to")
2466 .map(|attr| &mut attr.value)
2467 {
2468 *animate_value = style::filter_style_attribute(&animate_value, allowed_values).into();
2469 }
2470 }
2471 }
2472 if &*animate_name == "class" {
2473 if let Some(allowed_values) = self.allowed_classes.get(animate_tag_name) {
2474 if let Some(animate_values) = attrs
2475 .iter_mut()
2476 .find(|attr| &*attr.name.local == "values")
2477 .map(|attr| &mut attr.value)
2478 {
2479 let new_value = animate_values.split(';')
2480 .map(|value| {
2481 let mut classes = vec![];
2482 // https://html.spec.whatwg.org/#global-attributes:classes-2
2483 for class in value.split_ascii_whitespace() {
2484 if allowed_values.contains(class) {
2485 classes.push(class.to_owned());
2486 }
2487 }
2488 classes.join(" ")
2489 })
2490 .collect::<Vec<String>>()
2491 .join(";");
2492 *animate_values = new_value.into();
2493 }
2494 for animate_value in attrs
2495 .iter_mut()
2496 .filter(|attr| &*attr.name.local == "from" || &*attr.name.local == "to")
2497 .map(|attr| &mut attr.value)
2498 {
2499 let mut classes = vec![];
2500 // https://html.spec.whatwg.org/#global-attributes:classes-2
2501 for class in animate_value.split_ascii_whitespace() {
2502 if allowed_values.contains(class) {
2503 classes.push(class.to_owned());
2504 }
2505 }
2506 *animate_value = classes.join(" ").into();
2507 }
2508 }
2509 }
2510 }
2511 }
2512 }
2513 }
2514
2515 /// Initializes an HTML fragment parser.
2516 ///
2517 /// Ammonia conforms to the HTML5 fragment parsing rules,
2518 /// by parsing the given fragment as if it were included in a <div> tag.
2519 fn make_parser() -> html::Parser<RcDom> {
2520 html::parse_fragment(
2521 RcDom::default(),
2522 html::ParseOpts::default(),
2523 QualName::new(None, ns!(html), local_name!("div")),
2524 vec![],
2525 false,
2526 )
2527 }
2528}
2529
2530/// Given an element name and attribute name, determine if the given attribute contains a URL.
2531fn is_url_attr(element: &str, attr: &str) -> bool {
2532 (element != "animate" && element != "set" && attr == "href")
2533 // Don't have to worry about alternate xmlns prefixes, because HTML doesn't
2534 // parse them, anyway:
2535 // https://html.spec.whatwg.org/#adjust-foreign-attributes
2536 || (element != "animate" && element != "set" && attr == "xlink:href")
2537 || attr == "src"
2538 || (element == "form" && attr == "action")
2539 || (element == "object" && attr == "data")
2540 || ((element == "button" || element == "input") && attr == "formaction")
2541 || (element == "a" && attr == "ping")
2542 || (element == "video" && attr == "poster")
2543}
2544
2545fn is_html_tag(element: &str) -> bool {
2546 (!is_svg_tag(element) && !is_mathml_tag(element))
2547 || matches!(
2548 element,
2549 "title" | "style" | "font" | "a" | "script" | "span"
2550 )
2551}
2552
2553/// Given an element name, check if it's SVG
2554fn is_svg_tag(element: &str) -> bool {
2555 // https://svgwg.org/svg2-draft/eltindex.html
2556 matches!(
2557 element,
2558 "a" | "animate"
2559 | "animateMotion"
2560 | "animateTransform"
2561 | "circle"
2562 | "clipPath"
2563 | "defs"
2564 | "desc"
2565 | "discard"
2566 | "ellipse"
2567 | "feBlend"
2568 | "feColorMatrix"
2569 | "feComponentTransfer"
2570 | "feComposite"
2571 | "feConvolveMatrix"
2572 | "feDiffuseLighting"
2573 | "feDisplacementMap"
2574 | "feDistantLight"
2575 | "feDropShadow"
2576 | "feFlood"
2577 | "feFuncA"
2578 | "feFuncB"
2579 | "feFuncG"
2580 | "feFuncR"
2581 | "feGaussianBlur"
2582 | "feImage"
2583 | "feMerge"
2584 | "feMergeNode"
2585 | "feMorphology"
2586 | "feOffset"
2587 | "fePointLight"
2588 | "feSpecularLighting"
2589 | "feSpotLight"
2590 | "feTile"
2591 | "feTurbulence"
2592 | "filter"
2593 | "foreignObject"
2594 | "g"
2595 | "image"
2596 | "line"
2597 | "linearGradient"
2598 | "marker"
2599 | "mask"
2600 | "metadata"
2601 | "mpath"
2602 | "path"
2603 | "pattern"
2604 | "polygon"
2605 | "polyline"
2606 | "radialGradient"
2607 | "rect"
2608 | "script"
2609 | "set"
2610 | "stop"
2611 | "style"
2612 | "svg"
2613 | "switch"
2614 | "symbol"
2615 | "text"
2616 | "textPath"
2617 | "title"
2618 | "tspan"
2619 | "use"
2620 | "view"
2621 )
2622}
2623
2624/// Given an element name, check if it's Math
2625fn is_mathml_tag(element: &str) -> bool {
2626 // https://svgwg.org/svg2-draft/eltindex.html
2627 matches!(
2628 element,
2629 "abs"
2630 | "and"
2631 | "annotation"
2632 | "annotation-xml"
2633 | "apply"
2634 | "approx"
2635 | "arccos"
2636 | "arccosh"
2637 | "arccot"
2638 | "arccoth"
2639 | "arccsc"
2640 | "arccsch"
2641 | "arcsec"
2642 | "arcsech"
2643 | "arcsin"
2644 | "arcsinh"
2645 | "arctan"
2646 | "arctanh"
2647 | "arg"
2648 | "bind"
2649 | "bvar"
2650 | "card"
2651 | "cartesianproduct"
2652 | "cbytes"
2653 | "ceiling"
2654 | "cerror"
2655 | "ci"
2656 | "cn"
2657 | "codomain"
2658 | "complexes"
2659 | "compose"
2660 | "condition"
2661 | "conjugate"
2662 | "cos"
2663 | "cosh"
2664 | "cot"
2665 | "coth"
2666 | "cs"
2667 | "csc"
2668 | "csch"
2669 | "csymbol"
2670 | "curl"
2671 | "declare"
2672 | "degree"
2673 | "determinant"
2674 | "diff"
2675 | "divergence"
2676 | "divide"
2677 | "domain"
2678 | "domainofapplication"
2679 | "emptyset"
2680 | "eq"
2681 | "equivalent"
2682 | "eulergamma"
2683 | "exists"
2684 | "exp"
2685 | "exponentiale"
2686 | "factorial"
2687 | "factorof"
2688 | "false"
2689 | "floor"
2690 | "fn"
2691 | "forall"
2692 | "gcd"
2693 | "geq"
2694 | "grad"
2695 | "gt"
2696 | "ident"
2697 | "image"
2698 | "imaginary"
2699 | "imaginaryi"
2700 | "implies"
2701 | "in"
2702 | "infinity"
2703 | "int"
2704 | "integers"
2705 | "intersect"
2706 | "interval"
2707 | "inverse"
2708 | "lambda"
2709 | "laplacian"
2710 | "lcm"
2711 | "leq"
2712 | "limit"
2713 | "list"
2714 | "ln"
2715 | "log"
2716 | "logbase"
2717 | "lowlimit"
2718 | "lt"
2719 | "maction"
2720 | "maligngroup"
2721 | "malignmark"
2722 | "math"
2723 | "matrix"
2724 | "matrixrow"
2725 | "max"
2726 | "mean"
2727 | "median"
2728 | "menclose"
2729 | "merror"
2730 | "mfenced"
2731 | "mfrac"
2732 | "mglyph"
2733 | "mi"
2734 | "min"
2735 | "minus"
2736 | "mlabeledtr"
2737 | "mlongdiv"
2738 | "mmultiscripts"
2739 | "mn"
2740 | "mo"
2741 | "mode"
2742 | "moment"
2743 | "momentabout"
2744 | "mover"
2745 | "mpadded"
2746 | "mphantom"
2747 | "mprescripts"
2748 | "mroot"
2749 | "mrow"
2750 | "ms"
2751 | "mscarries"
2752 | "mscarry"
2753 | "msgroup"
2754 | "msline"
2755 | "mspace"
2756 | "msqrt"
2757 | "msrow"
2758 | "mstack"
2759 | "mstyle"
2760 | "msub"
2761 | "msubsup"
2762 | "msup"
2763 | "mtable"
2764 | "mtd"
2765 | "mtext"
2766 | "mtr"
2767 | "munder"
2768 | "munderover"
2769 | "naturalnumbers"
2770 | "neq"
2771 | "none"
2772 | "not"
2773 | "notanumber"
2774 | "notin"
2775 | "notprsubset"
2776 | "notsubset"
2777 | "or"
2778 | "otherwise"
2779 | "outerproduct"
2780 | "partialdiff"
2781 | "pi"
2782 | "piece"
2783 | "piecewise"
2784 | "plus"
2785 | "power"
2786 | "primes"
2787 | "product"
2788 | "prsubset"
2789 | "quotient"
2790 | "rationals"
2791 | "real"
2792 | "reals"
2793 | "reln"
2794 | "rem"
2795 | "root"
2796 | "scalarproduct"
2797 | "sdev"
2798 | "sec"
2799 | "sech"
2800 | "selector"
2801 | "semantics"
2802 | "sep"
2803 | "set"
2804 | "setdiff"
2805 | "share"
2806 | "sin"
2807 | "sinh"
2808 | "span"
2809 | "subset"
2810 | "sum"
2811 | "tan"
2812 | "tanh"
2813 | "tendsto"
2814 | "times"
2815 | "transpose"
2816 | "true"
2817 | "union"
2818 | "uplimit"
2819 | "variance"
2820 | "vector"
2821 | "vectorproduct"
2822 | "xor"
2823 )
2824}
2825
2826fn is_url_relative(url: &str) -> bool {
2827 matches!(
2828 Url::parse(url),
2829 Err(url::ParseError::RelativeUrlWithoutBase)
2830 )
2831}
2832
2833/// Policy for [relative URLs], that is, URLs that do not specify the scheme in full.
2834///
2835/// This policy kicks in, if set, for any attribute named `src` or `href`,
2836/// as well as the `data` attribute of an `object` tag.
2837///
2838/// [relative URLs]: struct.Builder.html#method.url_relative
2839///
2840/// # Examples
2841///
2842/// ## `Deny`
2843///
2844/// * `<a href="test">` is a file-relative URL, and will be removed
2845/// * `<a href="/test">` is a domain-relative URL, and will be removed
2846/// * `<a href="//example.com/test">` is a scheme-relative URL, and will be removed
2847/// * `<a href="http://example.com/test">` is an absolute URL, and will be kept
2848///
2849/// ## `PassThrough`
2850///
2851/// No changes will be made to any URLs, except if a disallowed scheme is used.
2852///
2853/// ## `RewriteWithBase`
2854///
2855/// If the base is set to `http://notriddle.com/some-directory/some-file`
2856///
2857/// * `<a href="test">` will be rewritten to `<a href="http://notriddle.com/some-directory/test">`
2858/// * `<a href="/test">` will be rewritten to `<a href="http://notriddle.com/test">`
2859/// * `<a href="//example.com/test">` will be rewritten to `<a href="http://example.com/test">`
2860/// * `<a href="http://example.com/test">` is an absolute URL, so it will be kept as-is
2861///
2862/// ## `Custom`
2863///
2864/// Pass the relative URL to a function.
2865/// If it returns `Some(string)`, then that one gets used.
2866/// Otherwise, it will remove the attribute (like `Deny` does).
2867///
2868/// use std::borrow::Cow;
2869/// fn is_absolute_path(url: &str) -> bool {
2870/// let u = url.as_bytes();
2871/// // `//a/b/c` is "protocol-relative", meaning "a" is a hostname
2872/// // `/a/b/c` is an absolute path, and what we want to do stuff to.
2873/// u.get(0) == Some(&b'/') && u.get(1) != Some(&b'/')
2874/// }
2875/// fn evaluate(url: &str) -> Option<Cow<str>> {
2876/// if is_absolute_path(url) {
2877/// Some(Cow::Owned(String::from("/root") + url))
2878/// } else {
2879/// Some(Cow::Borrowed(url))
2880/// }
2881/// }
2882/// fn main() {
2883/// let a = ammonia::Builder::new()
2884/// .url_relative(ammonia::UrlRelative::Custom(Box::new(evaluate)))
2885/// .clean("<a href=/test/path>fixed</a><a href=path>passed</a><a href=http://google.com/>skipped</a>")
2886/// .to_string();
2887/// assert_eq!(a, "<a href=\"/root/test/path\" rel=\"noopener noreferrer\">fixed</a><a href=\"path\" rel=\"noopener noreferrer\">passed</a><a href=\"http://google.com/\" rel=\"noopener noreferrer\">skipped</a>");
2888/// }
2889///
2890/// This function is only applied to relative URLs.
2891/// To filter all of the URLs,
2892/// use the not-yet-implemented Content Security Policy.
2893#[non_exhaustive]
2894pub enum UrlRelative<'a> {
2895 /// Relative URLs will be completely stripped from the document.
2896 Deny,
2897 /// Relative URLs will be passed through unchanged.
2898 PassThrough,
2899 /// Relative URLs will be changed into absolute URLs, based on this base URL.
2900 RewriteWithBase(Url),
2901 /// Force absolute and relative paths into a particular directory.
2902 ///
2903 /// Since the resolver does not affect fully-qualified URLs, it doesn't
2904 /// prevent users from linking wherever they want. This feature only
2905 /// serves to make content more portable.
2906 ///
2907 /// # Examples
2908 ///
2909 /// <table>
2910 /// <thead>
2911 /// <tr>
2912 /// <th>root</th>
2913 /// <th>path</th>
2914 /// <th>url</th>
2915 /// <th>result</th>
2916 /// </tr>
2917 /// </thead>
2918 /// <tbody>
2919 /// <tr>
2920 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2921 /// <td>README.md</td>
2922 /// <td></td>
2923 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/README.md</td>
2924 /// </tr><tr>
2925 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2926 /// <td>README.md</td>
2927 /// <td>/</td>
2928 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2929 /// </tr><tr>
2930 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2931 /// <td>README.md</td>
2932 /// <td>/CONTRIBUTING.md</td>
2933 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md</td>
2934 /// </tr><tr>
2935 /// <td>https://github.com/rust-ammonia/ammonia/blob/master</td>
2936 /// <td>README.md</td>
2937 /// <td></td>
2938 /// <td>https://github.com/rust-ammonia/ammonia/blob/README.md</td>
2939 /// </tr><tr>
2940 /// <td>https://github.com/rust-ammonia/ammonia/blob/master</td>
2941 /// <td>README.md</td>
2942 /// <td>/</td>
2943 /// <td>https://github.com/rust-ammonia/ammonia/blob/</td>
2944 /// </tr><tr>
2945 /// <td>https://github.com/rust-ammonia/ammonia/blob/master</td>
2946 /// <td>README.md</td>
2947 /// <td>/CONTRIBUTING.md</td>
2948 /// <td>https://github.com/rust-ammonia/ammonia/blob/CONTRIBUTING.md</td>
2949 /// </tr><tr>
2950 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2951 /// <td></td>
2952 /// <td></td>
2953 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2954 /// </tr><tr>
2955 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2956 /// <td></td>
2957 /// <td>/</td>
2958 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2959 /// </tr><tr>
2960 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2961 /// <td></td>
2962 /// <td>/CONTRIBUTING.md</td>
2963 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md</td>
2964 /// </tr><tr>
2965 /// <td>https://github.com/</td>
2966 /// <td>rust-ammonia/ammonia/blob/master/README.md</td>
2967 /// <td></td>
2968 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/README.md</td>
2969 /// </tr><tr>
2970 /// <td>https://github.com/</td>
2971 /// <td>rust-ammonia/ammonia/blob/master/README.md</td>
2972 /// <td>/</td>
2973 /// <td>https://github.com/</td>
2974 /// </tr><tr>
2975 /// <td>https://github.com/</td>
2976 /// <td>rust-ammonia/ammonia/blob/master/README.md</td>
2977 /// <td>CONTRIBUTING.md</td>
2978 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md</td>
2979 /// </tr><tr>
2980 /// <td>https://github.com/</td>
2981 /// <td>rust-ammonia/ammonia/blob/master/README.md</td>
2982 /// <td>/CONTRIBUTING.md</td>
2983 /// <td>https://github.com/CONTRIBUTING.md</td>
2984 /// </tr>
2985 /// </tbody>
2986 /// </table>
2987 RewriteWithRoot {
2988 /// The URL that is treated as the root by the resolver.
2989 root: Url,
2990 /// The "current path" used to resolve relative paths.
2991 path: String,
2992 },
2993 /// Rewrite URLs with a custom function.
2994 Custom(Box<dyn UrlRelativeEvaluate<'a>>),
2995}
2996
2997impl<'a> UrlRelative<'a> {
2998 fn evaluate(&self, url: &str) -> Option<html5ever::tendril::StrTendril> {
2999 match self {
3000 UrlRelative::RewriteWithBase(ref url_base) => url_base
3001 .join(url)
3002 .ok()
3003 .and_then(|x| StrTendril::from_str(x.as_str()).ok()),
3004 UrlRelative::RewriteWithRoot { ref root, ref path } => {
3005 (match url.as_bytes() {
3006 // Scheme-relative URL
3007 [b'/', b'/', ..] => root.join(url),
3008 // Path-absolute URL
3009 b"/" => root.join("."),
3010 [b'/', ..] => root.join(&url[1..]),
3011 // Path-relative URL
3012 _ => root.join(path).and_then(|r| r.join(url)),
3013 })
3014 .ok()
3015 .and_then(|x| StrTendril::from_str(x.as_str()).ok())
3016 }
3017 UrlRelative::Custom(ref evaluate) => evaluate
3018 .evaluate(url)
3019 .as_ref()
3020 .map(Cow::as_ref)
3021 .map(StrTendril::from_str)
3022 .and_then(Result::ok),
3023 UrlRelative::PassThrough => StrTendril::from_str(url).ok(),
3024 UrlRelative::Deny => None,
3025 }
3026 }
3027}
3028
3029impl<'a> fmt::Debug for UrlRelative<'a> {
3030 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3031 match *self {
3032 UrlRelative::Deny => write!(f, "UrlRelative::Deny"),
3033 UrlRelative::PassThrough => write!(f, "UrlRelative::PassThrough"),
3034 UrlRelative::RewriteWithBase(ref base) => {
3035 write!(f, "UrlRelative::RewriteWithBase({})", base)
3036 }
3037 UrlRelative::RewriteWithRoot { ref root, ref path } => {
3038 write!(
3039 f,
3040 "UrlRelative::RewriteWithRoot {{ root: {root}, path: {path} }}"
3041 )
3042 }
3043 UrlRelative::Custom(_) => write!(f, "UrlRelative::Custom"),
3044 }
3045 }
3046}
3047
3048/// Types that implement this trait can be used to convert a relative URL into an absolute URL.
3049///
3050/// This evaluator is only called when the URL is relative; absolute URLs are not evaluated.
3051///
3052/// See [`url_relative`][url_relative] for more details.
3053///
3054/// [url_relative]: struct.Builder.html#method.url_relative
3055pub trait UrlRelativeEvaluate<'a>: Send + Sync + 'a {
3056 /// Return `None` to remove the attribute. Return `Some(str)` to replace it with a new string.
3057 fn evaluate<'url>(&self, _: &'url str) -> Option<Cow<'url, str>>;
3058}
3059impl<'a, T> UrlRelativeEvaluate<'a> for T
3060where
3061 T: Fn(&str) -> Option<Cow<'_, str>> + Send + Sync + 'a,
3062{
3063 fn evaluate<'url>(&self, url: &'url str) -> Option<Cow<'url, str>> {
3064 self(url)
3065 }
3066}
3067
3068impl fmt::Debug for dyn AttributeFilter {
3069 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3070 f.write_str("AttributeFilter")
3071 }
3072}
3073
3074/// Types that implement this trait can be used to remove or rewrite arbitrary attributes.
3075///
3076/// See [`attribute_filter`][attribute_filter] for more details.
3077///
3078/// [attribute_filter]: struct.Builder.html#method.attribute_filter
3079pub trait AttributeFilter: Send + Sync {
3080 /// Return `None` to remove the attribute. Return `Some(str)` to replace it with a new string.
3081 fn filter<'a>(&self, _: &str, _: &str, _: &'a str) -> Option<Cow<'a, str>>;
3082}
3083
3084impl<T> AttributeFilter for T
3085where
3086 T: for<'a> Fn(&str, &str, &'a str) -> Option<Cow<'a, str>> + Send + Sync + 'static,
3087{
3088 fn filter<'a>(&self, element: &str, attribute: &str, value: &'a str) -> Option<Cow<'a, str>> {
3089 self(element, attribute, value)
3090 }
3091}
3092
3093/// A sanitized HTML document.
3094///
3095/// The `Document` type is an opaque struct representing an HTML fragment that was sanitized by
3096/// `ammonia`. It can be converted to a [`String`] or written to a [`Write`] instance. This allows
3097/// users to avoid buffering the serialized representation to a [`String`] when desired.
3098///
3099/// This type is opaque to insulate the caller from breaking changes in the `html5ever` interface.
3100///
3101/// Note that this type wraps an `html5ever` DOM tree. `ammonia` does not support streaming, so
3102/// the complete fragment needs to be stored in memory during processing.
3103///
3104/// [`String`]: https://doc.rust-lang.org/nightly/std/string/struct.String.html
3105/// [`Write`]: https://doc.rust-lang.org/nightly/std/io/trait.Write.html
3106///
3107/// # Examples
3108///
3109/// use ammonia::Builder;
3110///
3111/// let input = "<!-- comments will be stripped -->This is an Ammonia example.";
3112/// let output = "This is an Ammonia example.";
3113///
3114/// let document = Builder::new()
3115/// .clean(input);
3116/// assert_eq!(document.to_string(), output);
3117pub struct Document(RcDom);
3118
3119impl Document {
3120 /// Serializes a `Document` instance to a writer.
3121 ///
3122 /// This method writes the sanitized HTML to a [`Write`] instance, avoiding a buffering step.
3123 ///
3124 /// To avoid consuming the writer, a mutable reference can be passed, like in the example below.
3125 ///
3126 /// Note that the in-memory representation of `Document` is larger than the serialized
3127 /// `String`.
3128 ///
3129 /// [`Write`]: https://doc.rust-lang.org/nightly/std/io/trait.Write.html
3130 ///
3131 /// # Examples
3132 ///
3133 /// use ammonia::Builder;
3134 ///
3135 /// let input = "Some <style></style>HTML here";
3136 /// let expected = b"Some HTML here";
3137 ///
3138 /// let document = Builder::new()
3139 /// .clean(input);
3140 ///
3141 /// let mut sanitized = Vec::new();
3142 /// document.write_to(&mut sanitized)
3143 /// .expect("Writing to a string should not fail (except on OOM)");
3144 /// assert_eq!(sanitized, expected);
3145 pub fn write_to<W>(&self, writer: W) -> io::Result<()>
3146 where
3147 W: io::Write,
3148 {
3149 let opts = Self::serialize_opts();
3150 let inner: SerializableHandle = self.0.document.children.borrow()[0].clone().into();
3151 serialize(writer, &inner, opts)
3152 }
3153
3154 /// Exposes the `Document` instance as an [`rcdom::Handle`].
3155 ///
3156 /// This method returns the inner object backing the `Document` instance. This allows
3157 /// making further changes to the DOM without introducing redundant serialization and
3158 /// parsing.
3159 ///
3160 /// Note that this method should be considered unstable and sits outside of the semver
3161 /// stability guarantees. It may change, break, or go away at any time, either because
3162 /// of `html5ever` changes or `ammonia` implementation changes.
3163 ///
3164 /// For this method to be accessible, a `cfg` flag is required. The easiest way is to
3165 /// use the `RUSTFLAGS` environment variable:
3166 ///
3167 /// ```text
3168 /// RUSTFLAGS='--cfg ammonia_unstable' cargo build
3169 /// ```
3170 ///
3171 /// on Unix-like platforms, or
3172 ///
3173 /// ```text
3174 /// set RUSTFLAGS=--cfg ammonia_unstable
3175 /// cargo build
3176 /// ```
3177 ///
3178 /// on Windows.
3179 ///
3180 /// This requirement also applies to crates that transitively depend on crates that use
3181 /// this flag.
3182 ///
3183 /// # Examples
3184 ///
3185 /// use ammonia::Builder;
3186 /// use ammonia::rcdom::SerializableHandle;
3187 /// use maplit::hashset;
3188 /// use html5ever::serialize::{serialize, SerializeOpts};
3189 ///
3190 /// # use std::error::Error;
3191 /// # fn do_main() -> Result<(), Box<dyn Error>> {
3192 /// let input = "<a>one link</a> and <a>one more</a>";
3193 /// let expected = "<a>one more</a> and <a>one link</a>";
3194 ///
3195 /// let document = Builder::new()
3196 /// .link_rel(None)
3197 /// .clean(input);
3198 ///
3199 /// let node = document.to_dom_node();
3200 /// node.children.borrow_mut().reverse();
3201 ///
3202 /// let mut buf = Vec::new();
3203 /// let handle: SerializableHandle = node.into();
3204 /// serialize(&mut buf, &handle, SerializeOpts::default())?;
3205 /// let output = String::from_utf8(buf)?;
3206 ///
3207 /// assert_eq!(output, expected);
3208 /// # Ok(())
3209 /// # }
3210 /// # fn main() { do_main().unwrap() }
3211 #[cfg(ammonia_unstable)]
3212 pub fn to_dom_node(&self) -> Handle {
3213 self.0.document.children.borrow()[0].clone()
3214 }
3215
3216 fn serialize_opts() -> SerializeOpts {
3217 SerializeOpts::default()
3218 }
3219}
3220
3221impl Clone for Document {
3222 fn clone(&self) -> Self {
3223 let parser = Builder::make_parser();
3224 let dom = parser.one(&self.to_string()[..]);
3225 Document(dom)
3226 }
3227}
3228
3229/// Convert a `Document` to stringified HTML.
3230///
3231/// Since [`Document`] implements [`Display`], it can be converted to a [`String`] using the
3232/// standard [`ToString::to_string`] method. This is the simplest way to use `ammonia`.
3233///
3234/// [`Document`]: ammonia::Document
3235/// [`Display`]: std::fmt::Display
3236/// [`ToString::to_string`]: std::string::ToString
3237///
3238/// # Examples
3239///
3240/// use ammonia::Builder;
3241///
3242/// let input = "Some <style></style>HTML here";
3243/// let output = "Some HTML here";
3244///
3245/// let document = Builder::new()
3246/// .clean(input);
3247/// assert_eq!(document.to_string(), output);
3248impl Display for Document {
3249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3250 let opts = Self::serialize_opts();
3251 let mut ret_val = Vec::new();
3252 let inner: SerializableHandle = self.0.document.children.borrow()[0].clone().into();
3253 serialize(&mut ret_val, &inner, opts)
3254 .expect("Writing to a string shouldn't fail (expect on OOM)");
3255 String::from_utf8(ret_val)
3256 .expect("html5ever only supports UTF8")
3257 .fmt(f)
3258 }
3259}
3260
3261impl fmt::Debug for Document {
3262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3263 write!(f, "Document({})", self)
3264 }
3265}
3266
3267impl From<Document> for String {
3268 fn from(document: Document) -> Self {
3269 document.to_string()
3270 }
3271}
3272
3273#[cfg(test)]
3274mod test {
3275 use super::*;
3276 #[test]
3277 fn deeply_nested_whitelisted_does_not_cause_stack_overflow() {
3278 clean(&"<b>".repeat(60_000));
3279 }
3280 #[test]
3281 fn deeply_nested_blacklisted_does_not_cause_stack_overflow() {
3282 clean(&"<b-b>".repeat(60_000));
3283 }
3284 #[test]
3285 fn deeply_nested_alternating_does_not_cause_stack_overflow() {
3286 clean(&"<b-b>".repeat(35_000));
3287 }
3288 #[test]
3289 fn document_level_tags_cannot_be_whitelisted() {
3290 // Adding `html`, `head`, or `body` to the allowed tags has no effect
3291 // because the parser runs in fragment mode and strips them before
3292 // the sanitizer sees the tree. This test pins that documented
3293 // behavior; if it ever changes, the docs on `Builder::tags` need to
3294 // change too.
3295 let fragment =
3296 "<html><head>head content</head><body><div>test</div></body></html>";
3297 let result = Builder::default()
3298 .add_tags(["html", "head", "body"])
3299 .clean(fragment)
3300 .to_string();
3301 assert_eq!(result, "head content<div>test</div>");
3302 }
3303 #[test]
3304 fn included_angles() {
3305 let fragment = "1 < 2";
3306 let result = clean(fragment);
3307 assert_eq!(result, "1 < 2");
3308 }
3309 #[test]
3310 fn remove_script() {
3311 let fragment = "an <script>evil()</script> example";
3312 let result = clean(fragment);
3313 assert_eq!(result, "an example");
3314 }
3315 #[test]
3316 fn ignore_link() {
3317 let fragment = "a <a href=\"http://www.google.com\">good</a> example";
3318 let expected = "a <a href=\"http://www.google.com\" rel=\"noopener noreferrer\">\
3319 good</a> example";
3320 let result = clean(fragment);
3321 assert_eq!(result, expected);
3322 }
3323 #[test]
3324 fn remove_unsafe_link() {
3325 let fragment = "an <a onclick=\"evil()\" href=\"http://www.google.com\">evil</a> example";
3326 let result = clean(fragment);
3327 assert_eq!(
3328 result,
3329 "an <a href=\"http://www.google.com\" rel=\"noopener noreferrer\">evil</a> example"
3330 );
3331 }
3332 #[test]
3333 fn remove_js_link() {
3334 let fragment = "an <a href=\"javascript:evil()\">evil</a> example";
3335 let result = clean(fragment);
3336 assert_eq!(result, "an <a rel=\"noopener noreferrer\">evil</a> example");
3337 }
3338 #[test]
3339 fn tag_rebalance() {
3340 let fragment = "<b>AWESOME!";
3341 let result = clean(fragment);
3342 assert_eq!(result, "<b>AWESOME!</b>");
3343 }
3344 #[test]
3345 fn allow_url_relative() {
3346 let fragment = "<a href=test>Test</a>";
3347 let result = Builder::new()
3348 .url_relative(UrlRelative::PassThrough)
3349 .clean(fragment)
3350 .to_string();
3351 assert_eq!(
3352 result,
3353 "<a href=\"test\" rel=\"noopener noreferrer\">Test</a>"
3354 );
3355 }
3356 #[test]
3357 fn rewrite_url_relative() {
3358 let fragment = "<a href=test>Test</a>";
3359 let result = Builder::new()
3360 .url_relative(UrlRelative::RewriteWithBase(
3361 Url::parse("http://example.com/").unwrap(),
3362 ))
3363 .clean(fragment)
3364 .to_string();
3365 assert_eq!(
3366 result,
3367 "<a href=\"http://example.com/test\" rel=\"noopener noreferrer\">Test</a>"
3368 );
3369 }
3370 #[test]
3371 fn rewrite_url_relative_with_invalid_url() {
3372 // Reduced from https://github.com/Bauke/ammonia-crash-test
3373 let fragment = r##"<a href="\\"https://example.com\\"">test</a>"##;
3374 let result = Builder::new()
3375 .url_relative(UrlRelative::RewriteWithBase(
3376 Url::parse("http://example.com/").unwrap(),
3377 ))
3378 .clean(fragment)
3379 .to_string();
3380 assert_eq!(result, r##"<a rel="noopener noreferrer">test</a>"##);
3381 }
3382 #[test]
3383 fn attribute_filter_nop() {
3384 let fragment = "<a href=test>Test</a>";
3385 let result = Builder::new()
3386 .attribute_filter(|elem, attr, value| {
3387 assert_eq!("a", elem);
3388 assert!(
3389 matches!(
3390 (attr, value),
3391 ("href", "test") | ("rel", "noopener noreferrer")
3392 ),
3393 "{}",
3394 value.to_string()
3395 );
3396 Some(value.into())
3397 })
3398 .clean(fragment)
3399 .to_string();
3400 assert_eq!(
3401 result,
3402 "<a href=\"test\" rel=\"noopener noreferrer\">Test</a>"
3403 );
3404 }
3405
3406 #[test]
3407 fn attribute_filter_drop() {
3408 let fragment = "Test<img alt=test src=imgtest>";
3409 let result = Builder::new()
3410 .attribute_filter(|elem, attr, value| {
3411 assert_eq!("img", elem);
3412 match (attr, value) {
3413 ("src", "imgtest") => None,
3414 ("alt", "test") => Some(value.into()),
3415 _ => panic!("unexpected"),
3416 }
3417 })
3418 .clean(fragment)
3419 .to_string();
3420 assert_eq!(result, r#"Test<img alt="test">"#);
3421 }
3422
3423 #[test]
3424 fn url_filter_absolute() {
3425 let fragment = "Test<img alt=test src=imgtest>";
3426 let result = Builder::new()
3427 .attribute_filter(|elem, attr, value| {
3428 assert_eq!("img", elem);
3429 match (attr, value) {
3430 ("src", "imgtest") => {
3431 Some(format!("https://example.com/images/{}", value).into())
3432 }
3433 ("alt", "test") => None,
3434 _ => panic!("unexpected"),
3435 }
3436 })
3437 .url_relative(UrlRelative::RewriteWithBase(
3438 Url::parse("http://wrong.invalid/").unwrap(),
3439 ))
3440 .clean(fragment)
3441 .to_string();
3442 assert_eq!(
3443 result,
3444 r#"Test<img src="https://example.com/images/imgtest">"#
3445 );
3446 }
3447
3448 #[test]
3449 fn url_filter_relative() {
3450 let fragment = "Test<img alt=test src=imgtest>";
3451 let result = Builder::new()
3452 .attribute_filter(|elem, attr, value| {
3453 assert_eq!("img", elem);
3454 match (attr, value) {
3455 ("src", "imgtest") => Some("rewrite".into()),
3456 ("alt", "test") => Some("altalt".into()),
3457 _ => panic!("unexpected"),
3458 }
3459 })
3460 .url_relative(UrlRelative::RewriteWithBase(
3461 Url::parse("https://example.com/base/#").unwrap(),
3462 ))
3463 .clean(fragment)
3464 .to_string();
3465 assert_eq!(
3466 result,
3467 r#"Test<img alt="altalt" src="https://example.com/base/rewrite">"#
3468 );
3469 }
3470
3471 #[test]
3472 fn rewrite_url_relative_no_rel() {
3473 let fragment = "<a href=test>Test</a>";
3474 let result = Builder::new()
3475 .url_relative(UrlRelative::RewriteWithBase(
3476 Url::parse("http://example.com/").unwrap(),
3477 ))
3478 .link_rel(None)
3479 .clean(fragment)
3480 .to_string();
3481 assert_eq!(result, "<a href=\"http://example.com/test\">Test</a>");
3482 }
3483 #[test]
3484 fn deny_url_relative() {
3485 let fragment = "<a href=test>Test</a>";
3486 let result = Builder::new()
3487 .url_relative(UrlRelative::Deny)
3488 .clean(fragment)
3489 .to_string();
3490 assert_eq!(result, "<a rel=\"noopener noreferrer\">Test</a>");
3491 }
3492 #[test]
3493 fn replace_rel() {
3494 let fragment = "<a href=test rel=\"garbage\">Test</a>";
3495 let result = Builder::new()
3496 .url_relative(UrlRelative::PassThrough)
3497 .clean(fragment)
3498 .to_string();
3499 assert_eq!(
3500 result,
3501 "<a href=\"test\" rel=\"noopener noreferrer\">Test</a>"
3502 );
3503 }
3504 #[test]
3505 fn consider_rel_still_banned() {
3506 let fragment = "<a href=test rel=\"garbage\">Test</a>";
3507 let result = Builder::new()
3508 .url_relative(UrlRelative::PassThrough)
3509 .link_rel(None)
3510 .clean(fragment)
3511 .to_string();
3512 assert_eq!(result, "<a href=\"test\">Test</a>");
3513 }
3514 #[test]
3515 fn object_data() {
3516 let fragment = "<span data=\"javascript:evil()\">Test</span>\
3517 <object data=\"javascript:evil()\"></object>M";
3518 let expected = r#"<span data="javascript:evil()">Test</span><object></object>M"#;
3519 let result = Builder::new()
3520 .tags(hashset!["span", "object"])
3521 .generic_attributes(hashset!["data"])
3522 .clean(fragment)
3523 .to_string();
3524 assert_eq!(result, expected);
3525 }
3526 #[test]
3527 fn remove_attributes() {
3528 let fragment = "<table border=\"1\"><tr></tr></table>";
3529 let result = Builder::new().clean(fragment);
3530 assert_eq!(
3531 result.to_string(),
3532 "<table><tbody><tr></tr></tbody></table>"
3533 );
3534 }
3535 #[test]
3536 fn quotes_in_attrs() {
3537 let fragment = "<b title='\"'>contents</b>";
3538 let result = clean(fragment);
3539 assert_eq!(result, "<b title=\""\">contents</b>");
3540 }
3541 #[test]
3542 #[should_panic]
3543 fn panic_if_rel_is_allowed_and_replaced_generic() {
3544 Builder::new()
3545 .link_rel(Some("noopener noreferrer"))
3546 .generic_attributes(hashset!["rel"])
3547 .clean("something");
3548 }
3549 #[test]
3550 #[should_panic]
3551 fn panic_if_rel_is_allowed_and_replaced_a() {
3552 Builder::new()
3553 .link_rel(Some("noopener noreferrer"))
3554 .tag_attributes(hashmap![
3555 "a" => hashset!["rel"],
3556 ])
3557 .clean("something");
3558 }
3559 #[test]
3560 fn no_panic_if_rel_is_allowed_and_replaced_span() {
3561 Builder::new()
3562 .link_rel(Some("noopener noreferrer"))
3563 .tag_attributes(hashmap![
3564 "span" => hashset!["rel"],
3565 ])
3566 .clean("<span rel=\"what\">s</span>");
3567 }
3568 #[test]
3569 fn no_panic_if_rel_is_allowed_and_not_replaced_generic() {
3570 Builder::new()
3571 .link_rel(None)
3572 .generic_attributes(hashset!["rel"])
3573 .clean("<a rel=\"what\">s</a>");
3574 }
3575 #[test]
3576 fn no_panic_if_rel_is_allowed_and_not_replaced_a() {
3577 Builder::new()
3578 .link_rel(None)
3579 .tag_attributes(hashmap![
3580 "a" => hashset!["rel"],
3581 ])
3582 .clean("<a rel=\"what\">s</a>");
3583 }
3584 #[test]
3585 fn dont_close_void_elements() {
3586 let fragment = "<br>";
3587 let result = clean(fragment);
3588 assert_eq!(result.to_string(), "<br>");
3589 }
3590 #[should_panic]
3591 #[test]
3592 fn panic_on_allowed_classes_tag_attributes() {
3593 let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3594 Builder::new()
3595 .link_rel(None)
3596 .tag_attributes(hashmap![
3597 "p" => hashset!["class"],
3598 "a" => hashset!["class"],
3599 ])
3600 .allowed_classes(hashmap![
3601 "p" => hashset!["foo", "bar"],
3602 "a" => hashset!["baz"],
3603 ])
3604 .clean(fragment);
3605 }
3606 #[should_panic]
3607 #[test]
3608 fn panic_on_allowed_classes_generic_attributes() {
3609 let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3610 Builder::new()
3611 .link_rel(None)
3612 .generic_attributes(hashset!["class", "href", "some-foo"])
3613 .allowed_classes(hashmap![
3614 "p" => hashset!["foo", "bar"],
3615 "a" => hashset!["baz"],
3616 ])
3617 .clean(fragment);
3618 }
3619 #[test]
3620 fn remove_non_allowed_classes() {
3621 let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3622 let result = Builder::new()
3623 .link_rel(None)
3624 .allowed_classes(hashmap![
3625 "p" => hashset!["foo", "bar"],
3626 "a" => hashset!["baz"],
3627 ])
3628 .clean(fragment);
3629 assert_eq!(
3630 result.to_string(),
3631 "<p class=\"foo bar\"><a class=\"baz\">Hey</a></p>"
3632 );
3633 }
3634 #[test]
3635 fn remove_non_allowed_classes_with_tag_class() {
3636 let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3637 let result = Builder::new()
3638 .link_rel(None)
3639 .tag_attributes(hashmap![
3640 "div" => hashset!["class"],
3641 ])
3642 .allowed_classes(hashmap![
3643 "p" => hashset!["foo", "bar"],
3644 "a" => hashset!["baz"],
3645 ])
3646 .clean(fragment);
3647 assert_eq!(
3648 result.to_string(),
3649 "<p class=\"foo bar\"><a class=\"baz\">Hey</a></p>"
3650 );
3651 }
3652 #[test]
3653 fn allowed_classes_ascii_whitespace() {
3654 // According to https://infra.spec.whatwg.org/#ascii-whitespace,
3655 // TAB (\t), LF (\n), FF (\x0C), CR (\x0D) and SPACE (\x20) are
3656 // considered to be ASCII whitespace. Unicode whitespace characters
3657 // and VT (\x0B) aren't ASCII whitespace.
3658 let fragment = "<p class=\"a\tb\nc\x0Cd\re f\x0B g\u{2000}\">";
3659 let result = Builder::new()
3660 .allowed_classes(hashmap![
3661 "p" => hashset!["a", "b", "c", "d", "e", "f", "g"],
3662 ])
3663 .clean(fragment);
3664 assert_eq!(result.to_string(), r#"<p class="a b c d e"></p>"#);
3665 }
3666 #[test]
3667 fn remove_non_allowed_attributes_with_tag_attribute_values() {
3668 let fragment = "<p data-label=\"baz\" name=\"foo\"></p>";
3669 let result = Builder::new()
3670 .tag_attribute_values(hashmap![
3671 "p" => hashmap![
3672 "data-label" => hashset!["bar"],
3673 ],
3674 ])
3675 .tag_attributes(hashmap![
3676 "p" => hashset!["name"],
3677 ])
3678 .clean(fragment);
3679 assert_eq!(result.to_string(), "<p name=\"foo\"></p>",);
3680 }
3681 #[test]
3682 fn keep_allowed_attributes_with_tag_attribute_values() {
3683 let fragment = "<p data-label=\"bar\" name=\"foo\"></p>";
3684 let result = Builder::new()
3685 .tag_attribute_values(hashmap![
3686 "p" => hashmap![
3687 "data-label" => hashset!["bar"],
3688 ],
3689 ])
3690 .tag_attributes(hashmap![
3691 "p" => hashset!["name"],
3692 ])
3693 .clean(fragment);
3694 assert_eq!(
3695 result.to_string(),
3696 "<p data-label=\"bar\" name=\"foo\"></p>",
3697 );
3698 }
3699 #[test]
3700 fn tag_attribute_values_case_insensitive() {
3701 let fragment = "<input type=\"CHECKBOX\" name=\"foo\">";
3702 let result = Builder::new()
3703 .tags(hashset!["input"])
3704 .tag_attribute_values(hashmap![
3705 "input" => hashmap![
3706 "type" => hashset!["checkbox"],
3707 ],
3708 ])
3709 .tag_attributes(hashmap![
3710 "input" => hashset!["name"],
3711 ])
3712 .clean(fragment);
3713 assert_eq!(result.to_string(), "<input type=\"CHECKBOX\" name=\"foo\">",);
3714 }
3715 #[test]
3716 fn set_tag_attribute_values() {
3717 let fragment = "<a href=\"https://example.com/\">Link</a>";
3718 let result = Builder::new()
3719 .link_rel(None)
3720 .add_tag_attributes("a", &["target"])
3721 .set_tag_attribute_value("a", "target", "_blank")
3722 .clean(fragment);
3723 assert_eq!(
3724 result.to_string(),
3725 "<a href=\"https://example.com/\" target=\"_blank\">Link</a>",
3726 );
3727 }
3728 #[test]
3729 fn update_existing_set_tag_attribute_values() {
3730 let fragment = "<a target=\"bad\" href=\"https://example.com/\">Link</a>";
3731 let result = Builder::new()
3732 .link_rel(None)
3733 .add_tag_attributes("a", &["target"])
3734 .set_tag_attribute_value("a", "target", "_blank")
3735 .clean(fragment);
3736 assert_eq!(
3737 result.to_string(),
3738 "<a target=\"_blank\" href=\"https://example.com/\">Link</a>",
3739 );
3740 }
3741 #[test]
3742 fn unwhitelisted_set_tag_attribute_values() {
3743 let fragment = "<span>hi</span><my-elem>";
3744 let result = Builder::new()
3745 .set_tag_attribute_value("my-elem", "my-attr", "val")
3746 .clean(fragment);
3747 assert_eq!(result.to_string(), "<span>hi</span>",);
3748 }
3749 #[test]
3750 fn remove_entity_link() {
3751 let fragment = "<a href=\"javascript:a\
3752 lert('XSS')\">Click me!</a>";
3753 let result = clean(fragment);
3754 assert_eq!(
3755 result.to_string(),
3756 "<a rel=\"noopener noreferrer\">Click me!</a>"
3757 );
3758 }
3759 #[test]
3760 fn remove_relative_url_evaluate() {
3761 fn is_absolute_path(url: &str) -> bool {
3762 let u = url.as_bytes();
3763 // `//a/b/c` is "protocol-relative", meaning "a" is a hostname
3764 // `/a/b/c` is an absolute path, and what we want to do stuff to.
3765 u.first() == Some(&b'/') && u.get(1) != Some(&b'/')
3766 }
3767 fn is_banned(url: &str) -> bool {
3768 let u = url.as_bytes();
3769 u.first() == Some(&b'b') && u.get(1) == Some(&b'a')
3770 }
3771 fn evaluate(url: &str) -> Option<Cow<'_, str>> {
3772 if is_absolute_path(url) {
3773 Some(Cow::Owned(String::from("/root") + url))
3774 } else if is_banned(url) {
3775 None
3776 } else {
3777 Some(Cow::Borrowed(url))
3778 }
3779 }
3780 let a = Builder::new()
3781 .url_relative(UrlRelative::Custom(Box::new(evaluate)))
3782 .clean("<a href=banned>banned</a><a href=/test/path>fixed</a><a href=path>passed</a><a href=http://google.com/>skipped</a>")
3783 .to_string();
3784 assert_eq!(a, "<a rel=\"noopener noreferrer\">banned</a><a href=\"/root/test/path\" rel=\"noopener noreferrer\">fixed</a><a href=\"path\" rel=\"noopener noreferrer\">passed</a><a href=\"http://google.com/\" rel=\"noopener noreferrer\">skipped</a>");
3785 }
3786 #[test]
3787 fn remove_relative_url_evaluate_b() {
3788 fn is_absolute_path(url: &str) -> bool {
3789 let u = url.as_bytes();
3790 // `//a/b/c` is "protocol-relative", meaning "a" is a hostname
3791 // `/a/b/c` is an absolute path, and what we want to do stuff to.
3792 u.first() == Some(&b'/') && u.get(1) != Some(&b'/')
3793 }
3794 fn is_banned(url: &str) -> bool {
3795 let u = url.as_bytes();
3796 u.first() == Some(&b'b') && u.get(1) == Some(&b'a')
3797 }
3798 fn evaluate(url: &str) -> Option<Cow<'_, str>> {
3799 if is_absolute_path(url) {
3800 Some(Cow::Owned(String::from("/root") + url))
3801 } else if is_banned(url) {
3802 None
3803 } else {
3804 Some(Cow::Borrowed(url))
3805 }
3806 }
3807 let a = Builder::new()
3808 .url_relative(UrlRelative::Custom(Box::new(evaluate)))
3809 .clean("<a href=banned>banned</a><a href=banned title=test>banned</a><a title=test href=banned>banned</a>")
3810 .to_string();
3811 assert_eq!(a, "<a rel=\"noopener noreferrer\">banned</a><a rel=\"noopener noreferrer\" title=\"test\">banned</a><a title=\"test\" rel=\"noopener noreferrer\">banned</a>");
3812 }
3813 #[test]
3814 fn remove_relative_url_evaluate_c() {
3815 // Don't run on absolute URLs.
3816 fn evaluate(_: &str) -> Option<Cow<'_, str>> {
3817 return Some(Cow::Owned(String::from("invalid")));
3818 }
3819 let a = Builder::new()
3820 .url_relative(UrlRelative::Custom(Box::new(evaluate)))
3821 .clean("<a href=\"https://www.google.com/\">google</a>")
3822 .to_string();
3823 assert_eq!(
3824 a,
3825 "<a href=\"https://www.google.com/\" rel=\"noopener noreferrer\">google</a>"
3826 );
3827 }
3828 #[test]
3829 fn clean_children_of_bad_element() {
3830 let fragment = "<bad><evil>a</evil>b</bad>";
3831 let result = Builder::new().clean(fragment);
3832 assert_eq!(result.to_string(), "ab");
3833 }
3834 #[test]
3835 fn reader_input() {
3836 let fragment = b"an <script>evil()</script> example";
3837 let result = Builder::new().clean_from_reader(&fragment[..]);
3838 assert!(result.is_ok());
3839 assert_eq!(result.unwrap().to_string(), "an example");
3840 }
3841 #[test]
3842 fn reader_non_utf8() {
3843 let fragment = b"non-utf8 \xF0\x90\x80string";
3844 let result = Builder::new().clean_from_reader(&fragment[..]);
3845 assert!(result.is_ok());
3846 assert_eq!(result.unwrap().to_string(), "non-utf8 \u{fffd}string");
3847 }
3848 #[test]
3849 fn display_impl() {
3850 let fragment = r#"a <a>link</a>"#;
3851 let result = Builder::new().link_rel(None).clean(fragment);
3852 assert_eq!(format!("{}", result), "a <a>link</a>");
3853 }
3854 #[test]
3855 fn debug_impl() {
3856 let fragment = r#"a <a>link</a>"#;
3857 let result = Builder::new().link_rel(None).clean(fragment);
3858 assert_eq!(format!("{:?}", result), "Document(a <a>link</a>)");
3859 }
3860 #[cfg(ammonia_unstable)]
3861 #[test]
3862 fn to_dom_node() {
3863 let fragment = r#"a <a>link</a>"#;
3864 let result = Builder::new().link_rel(None).clean(fragment);
3865 let _node = result.to_dom_node();
3866 }
3867 #[test]
3868 fn string_from_document() {
3869 let fragment = r#"a <a>link"#;
3870 let result = String::from(Builder::new().link_rel(None).clean(fragment));
3871 assert_eq!(format!("{}", result), "a <a>link</a>");
3872 }
3873 fn require_sync<T: Sync>(_: T) {}
3874 fn require_send<T: Send>(_: T) {}
3875 #[test]
3876 fn require_sync_and_send() {
3877 require_sync(Builder::new());
3878 require_send(Builder::new());
3879 }
3880 #[test]
3881 fn id_prefixed() {
3882 let fragment = "<a id=\"hello\"></a><b id=\"hello\"></a>";
3883 let result = String::from(
3884 Builder::new()
3885 .tag_attributes(hashmap![
3886 "a" => hashset!["id"],
3887 ])
3888 .id_prefix(Some("prefix-"))
3889 .clean(fragment),
3890 );
3891 assert_eq!(
3892 result.to_string(),
3893 "<a id=\"prefix-hello\" rel=\"noopener noreferrer\"></a><b></b>"
3894 );
3895 }
3896 #[test]
3897 fn id_already_prefixed() {
3898 let fragment = "<a id=\"prefix-hello\"></a>";
3899 let result = String::from(
3900 Builder::new()
3901 .tag_attributes(hashmap![
3902 "a" => hashset!["id"],
3903 ])
3904 .id_prefix(Some("prefix-"))
3905 .clean(fragment),
3906 );
3907 assert_eq!(
3908 result.to_string(),
3909 "<a id=\"prefix-hello\" rel=\"noopener noreferrer\"></a>"
3910 );
3911 }
3912 #[test]
3913 fn clean_content_tags() {
3914 let fragment = "<script type=\"text/javascript\"><a>Hello!</a></script>";
3915 let result = String::from(
3916 Builder::new()
3917 .clean_content_tags(hashset!["script"])
3918 .clean(fragment),
3919 );
3920 assert_eq!(result.to_string(), "");
3921 }
3922 #[test]
3923 fn only_clean_content_tags() {
3924 let fragment = "<em>This is</em><script><a>Hello!</a></script><p>still here!</p>";
3925 let result = String::from(
3926 Builder::new()
3927 .clean_content_tags(hashset!["script"])
3928 .clean(fragment),
3929 );
3930 assert_eq!(result.to_string(), "<em>This is</em><p>still here!</p>");
3931 }
3932 #[test]
3933 fn clean_removed_default_tag() {
3934 let fragment = "<em>This is</em><script><a>Hello!</a></script><p>still here!</p>";
3935 let result = String::from(
3936 Builder::new()
3937 .rm_tags(hashset!["a"])
3938 .rm_tag_attributes("a", hashset!["href", "hreflang"])
3939 .clean_content_tags(hashset!["script"])
3940 .clean(fragment),
3941 );
3942 assert_eq!(result.to_string(), "<em>This is</em><p>still here!</p>");
3943 }
3944 #[test]
3945 #[should_panic]
3946 fn panic_on_clean_content_tag_attribute() {
3947 Builder::new()
3948 .rm_tags(std::iter::once("a"))
3949 .clean_content_tags(hashset!["a"])
3950 .clean("");
3951 }
3952 #[test]
3953 #[should_panic]
3954 fn panic_on_clean_content_tag() {
3955 Builder::new().clean_content_tags(hashset!["a"]).clean("");
3956 }
3957
3958 #[test]
3959 fn clean_text_test() {
3960 assert_eq!(
3961 clean_text("<this> is <a test function"),
3962 "<this> is <a test function"
3963 );
3964 }
3965
3966 #[test]
3967 fn clean_text_spaces_test() {
3968 assert_eq!(clean_text("\x09\x0a\x0c\x20"), "	  ");
3969 }
3970
3971 #[test]
3972 fn ns_svg() {
3973 // https://github.com/cure53/DOMPurify/pull/495
3974 let fragment = r##"<svg><iframe><a title="</iframe><img src onerror=alert(1)>">test"##;
3975 let result = String::from(Builder::new().add_tags(&["iframe"]).clean(fragment));
3976 assert_eq!(result.to_string(), "");
3977
3978 let fragment = "<svg><iframe>remove me</iframe></svg><iframe>keep me</iframe>";
3979 let result = String::from(Builder::new().add_tags(&["iframe"]).clean(fragment));
3980 assert_eq!(result.to_string(), "<iframe>keep me</iframe>");
3981
3982 let fragment = "<svg><a>remove me</a></svg><iframe>keep me</iframe>";
3983 let result = String::from(Builder::new().add_tags(&["iframe"]).clean(fragment));
3984 assert_eq!(result.to_string(), "<iframe>keep me</iframe>");
3985
3986 let fragment = "<svg><a>keep me</a></svg><iframe>keep me</iframe>";
3987 let result = String::from(Builder::new().add_tags(&["iframe", "svg"]).clean(fragment));
3988 assert_eq!(
3989 result.to_string(),
3990 "<svg><a rel=\"noopener noreferrer\">keep me</a></svg><iframe>keep me</iframe>"
3991 );
3992 }
3993
3994 #[test]
3995 fn ns_svg_2() {
3996 let fragment = "<svg><foreignObject><table><path><xmp><!--</xmp><img title'--><img src=1 onerror=alert(1)>'>";
3997 let result = Builder::default()
3998 .strip_comments(false)
3999 .add_tags(&["svg","foreignObject","table","path","xmp"])
4000 .clean(fragment);
4001 assert_eq!(
4002 result.to_string(),
4003 "<svg><foreignObject><table></table></foreignObject></svg>"
4004 );
4005 }
4006
4007 #[test]
4008 fn ns_mathml() {
4009 // https://github.com/cure53/DOMPurify/pull/495
4010 let fragment = "<mglyph></mglyph>";
4011 let result = String::from(
4012 Builder::new()
4013 .add_tags(&["math", "mtext", "mglyph"])
4014 .clean(fragment),
4015 );
4016 assert_eq!(result.to_string(), "");
4017 let fragment = "<math><mtext><div><mglyph>";
4018 let result = String::from(
4019 Builder::new()
4020 .add_tags(&["math", "mtext", "mglyph"])
4021 .clean(fragment),
4022 );
4023 assert_eq!(
4024 result.to_string(),
4025 "<math><mtext><div></div></mtext></math>"
4026 );
4027 let fragment = "<math><mtext><mglyph>";
4028 let result = String::from(
4029 Builder::new()
4030 .add_tags(&["math", "mtext", "mglyph"])
4031 .clean(fragment),
4032 );
4033 assert_eq!(
4034 result.to_string(),
4035 "<math><mtext><mglyph></mglyph></mtext></math>"
4036 );
4037 }
4038
4039 #[test]
4040 fn ns_mathml_2() {
4041 let fragment = "<math><mtext><table><mglyph><xmp><!--</xmp><img title='--><img src=1 onerror=alert(1)>'>";
4042 let result = Builder::default()
4043 .strip_comments(false)
4044 .add_tags(&["math","mtext","table","mglyph","xmp"])
4045 .clean(fragment);
4046 assert_eq!(
4047 result.to_string(),
4048 "<math><mtext><table></table></mtext></math>"
4049 );
4050 }
4051
4052 #[test]
4053 fn ns_mathml_3() {
4054 // try without the attr
4055 let fragment = "<math><annotation-xml encoding='text/html'><xmp><!--</xmp><img title='--><img src=1 onerror=alert(1)>'>";
4056 let result = Builder::default()
4057 .strip_comments(false)
4058 .add_tags(&["math","annotation-xml","table","mglyph","xmp"])
4059 .clean(fragment);
4060 assert_eq!(
4061 result.to_string(),
4062 "<math><annotation-xml></annotation-xml></math>"
4063 );
4064 // now with the attr
4065 let fragment = "<math><annotation-xml encoding='text/html'><xmp><!--</xmp><img title='--><img src=1 onerror=alert(1)>'>";
4066 let result = Builder::default()
4067 .strip_comments(false)
4068 .add_tags(&["math","annotation-xml","table","mglyph","xmp"])
4069 .add_tag_attribute_values("annotation-xml", "encoding", ["text/html"])
4070 .clean(fragment);
4071 assert_eq!(
4072 result.to_string(),
4073 // yes, I tried it in Firefox, and the script didn't run
4074 r#"<math><annotation-xml encoding="text/html"><xmp><!--</xmp><img title="--><img src=1 onerror=alert(1)>"></annotation-xml></math>"#
4075 );
4076 // now with a tweaked attr
4077 let fragment = "<math><annotation-xml encoding='image/svg+xml'><xmp><!--</xmp><img title='--><img src=1 onerror=alert(1)>'>";
4078 let result = Builder::default()
4079 .strip_comments(false)
4080 .add_tags(&["math","annotation-xml","table","mglyph","xmp"])
4081 .add_tag_attribute_values("annotation-xml", "encoding", ["image/svg+xml"])
4082 .clean(fragment);
4083 assert_eq!(
4084 result.to_string(),
4085 r#"<math><annotation-xml encoding="image/svg+xml"></annotation-xml></math>"#
4086 );
4087 // now with actual SVG
4088 let fragment = "<math><annotation-xml encoding='image/svg+xml'><svg>";
4089 let result = Builder::default()
4090 .strip_comments(false)
4091 .add_tags(&["math","annotation-xml","svg"])
4092 .add_tag_attribute_values("annotation-xml", "encoding", ["image/svg+xml"])
4093 .clean(fragment);
4094 assert_eq!(
4095 result.to_string(),
4096 r#"<math><annotation-xml encoding="image/svg+xml"><svg></svg></annotation-xml></math>"#
4097 );
4098 }
4099
4100 #[test]
4101 fn ns_svg_animate_url_attr() {
4102 let fragment = r##"
4103 <svg>
4104 <a>
4105 <animate attributeName="xss" values="http://example.com"></animate>
4106 <animate attributeName="href" values="http://example.com"></animate>
4107 <animate attributeName="href" values="http://example.com;/test"></animate>
4108 <animate attributeName="href" values="http://example.com;/test;javascript:xss"></animate>
4109 <animate attributeName="href" from="http://example.com" to="http://example.com"></animate>
4110 <animate attributeName="href" from="javascript:xss" to="http://example.com"></animate>
4111 <animate attributeName="href" from="http://example.com" to="javascript:xss"></animate>
4112 <animate attributeName="href" from="http://example.com" to="./test2"></animate>
4113 <animate attributeName="href" from="./test2" to="http://example.com"></animate>
4114 </a>
4115 </svg>
4116 "##;
4117 let filtered = r##"
4118 <svg>
4119 <a rel="noopener noreferrer">
4120
4121 <animate attributeName="href" values="http://example.com"></animate>
4122 <animate attributeName="href" values="http://example.com;http://notriddle.com/test"></animate>
4123
4124 <animate attributeName="href" from="http://example.com" to="http://example.com"></animate>
4125
4126
4127 <animate attributeName="href" from="http://example.com" to="http://notriddle.com/test2"></animate>
4128 <animate attributeName="href" from="http://notriddle.com/test2" to="http://example.com"></animate>
4129 </a>
4130 </svg>
4131 "##;
4132 let result = Builder::default()
4133 .add_tags(&["svg","a","animate"])
4134 .add_tag_attributes("animate", ["attributeName","values","from","to"])
4135 .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4136 .clean(fragment);
4137 assert_eq!(
4138 result.to_string(),
4139 filtered,
4140 );
4141 }
4142
4143 #[test]
4144 fn ns_svg_set_url_attr() {
4145 let fragment = r##"
4146 <svg>
4147 <a>
4148 <set attributeName="href" to="./test2"></set>
4149 <set attributeName="href" to="http://example.com"></set>
4150 <set attributeName="href" to="javascript:xss"></set>
4151 </a>
4152 </svg>
4153 "##;
4154 let filtered = r##"
4155 <svg>
4156 <a rel="noopener noreferrer">
4157 <set attributeName="href" to="http://notriddle.com/test2"></set>
4158 <set attributeName="href" to="http://example.com"></set>
4159
4160 </a>
4161 </svg>
4162 "##;
4163 let result = Builder::default()
4164 .add_tags(&["svg","a","set"])
4165 .add_tag_attributes("set", ["attributeName","values","from","to"])
4166 .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4167 .clean(fragment);
4168 assert_eq!(
4169 result.to_string(),
4170 filtered,
4171 );
4172 }
4173
4174 #[test]
4175 fn ns_svg_set_url_xlink_attr() {
4176 let fragment = r##"
4177 <svg>
4178 <a>
4179 <set attributeName="xlink:href" to="./test2"></set>
4180 <set attributeName="xlink:href" to="http://example.com"></set>
4181 <set attributeName="xlink:href" to="javascript:xss"></set>
4182 </a>
4183 </svg>
4184 "##;
4185 let filtered = r##"
4186 <svg>
4187 <a rel="noopener noreferrer">
4188 <set attributeName="xlink:href" to="http://notriddle.com/test2"></set>
4189 <set attributeName="xlink:href" to="http://example.com"></set>
4190
4191 </a>
4192 </svg>
4193 "##;
4194 let result = Builder::default()
4195 .add_tags(&["svg","a","set"])
4196 .add_tag_attributes("a", ["xlink:href"])
4197 .add_tag_attributes("set", ["attributeName","values","from","to"])
4198 .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4199 .clean(fragment);
4200 assert_eq!(
4201 result.to_string(),
4202 filtered,
4203 );
4204 }
4205
4206 #[test]
4207 fn ns_svg_set_url_attr_non_path() {
4208 let fragment = r##"
4209 <svg>
4210 <a>
4211 <set attributeName="href" to="./test2"></set>
4212 <set attributeName="href" to="http://example.com"></set>
4213 <set attributeName="href" to="javascript:xss"></set>
4214 </a>
4215 </svg>
4216 "##;
4217 let filtered = r##"
4218 <svg>
4219 <a rel="noopener noreferrer">
4220 <set attributeName="href"></set>
4221 <set attributeName="href" to="http://example.com"></set>
4222
4223 </a>
4224 </svg>
4225 "##;
4226 let result = Builder::default()
4227 .add_tags(&["svg","a","set"])
4228 .add_tag_attributes("set", ["attributeName","values","from","to"])
4229 .url_relative(UrlRelative::RewriteWithBase(Url::parse("magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709&xt=urn:btmh:1220e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855").unwrap()))
4230 .clean(fragment);
4231 assert_eq!(
4232 result.to_string(),
4233 filtered,
4234 );
4235 }
4236
4237 #[test]
4238 fn ns_svg_animate_set_attr() {
4239 let fragment = r##"
4240 <svg>
4241 <a>
4242 <animate attributeName="x" values="1;2;3"></animate>
4243 <animate attributeName="x" from="1" to="2"></animate>
4244 <animate attributeName="x" from="1"></animate>
4245 <animate attributeName="x" to="2"></animate>
4246 <animate attributeName="y" values="1;2;3"></animate>
4247 <animate attributeName="y" from="1" to="2"></animate>
4248 <animate attributeName="y" from="1"></animate>
4249 <animate attributeName="y" to="2"></animate>
4250 </a>
4251 </svg>
4252 "##;
4253 let filtered = r##"
4254 <svg>
4255 <a x="0" rel="noopener noreferrer">
4256
4257
4258
4259
4260 <animate attributeName="y" values="1;2;3"></animate>
4261 <animate attributeName="y" from="1" to="2"></animate>
4262 <animate attributeName="y" from="1"></animate>
4263 <animate attributeName="y" to="2"></animate>
4264 </a>
4265 </svg>
4266 "##;
4267 let result = Builder::default()
4268 .add_tags(&["svg","a","animate"])
4269 .add_tag_attributes("animate", ["attributeName","values","from","to"])
4270 .add_tag_attributes("a", ["x", "y"])
4271 .set_tag_attribute_value("a", "x", "0")
4272 .clean(fragment);
4273 assert_eq!(
4274 result.to_string(),
4275 filtered,
4276 );
4277 }
4278
4279 #[test]
4280 fn ns_svg_animate_attr_filter() {
4281 let fragment = r##"
4282 <svg>
4283 <a>
4284 <animate attributeName="x" values="1;2;3"></animate>
4285 <animate attributeName="x" from="1" to="2"></animate>
4286 <animate attributeName="x" from="1"></animate>
4287 <animate attributeName="x" to="2"></animate>
4288 <animate attributeName="y" values="1;2;3"></animate>
4289 <animate attributeName="y" from="1" to="2"></animate>
4290 <animate attributeName="y" from="1"></animate>
4291 <animate attributeName="y" to="2"></animate>
4292 </a>
4293 </svg>
4294 "##;
4295 let filtered = r##"
4296 <svg>
4297 <a rel="noopener noreferrer">
4298 <animate attributeName="x" values="0;0;0"></animate>
4299 <animate attributeName="x" from="0" to="0"></animate>
4300 <animate attributeName="x" from="0"></animate>
4301 <animate attributeName="x" to="0"></animate>
4302 <animate attributeName="y"></animate>
4303 <animate attributeName="y" to="2"></animate>
4304 <animate attributeName="y"></animate>
4305 <animate attributeName="y" to="2"></animate>
4306 </a>
4307 </svg>
4308 "##;
4309 let result = Builder::default()
4310 .add_tags(&["svg","a","animate"])
4311 .add_tag_attributes("animate", ["attributeName","values","from","to"])
4312 .add_tag_attributes("a", ["x", "y"])
4313 .attribute_filter(|_tag, key, value| Some(if key == "x" {
4314 "0".into()
4315 } else if key == "y" && value == "1" {
4316 return None;
4317 } else {
4318 value.into()
4319 }))
4320 .clean(fragment);
4321 assert_eq!(
4322 result.to_string(),
4323 filtered,
4324 );
4325 }
4326
4327 #[test]
4328 fn ns_svg_animate_allowed_classes() {
4329 let fragment = r##"
4330 <svg>
4331 <a>
4332 <animate attributeName="class" values="a b c;a b c d;a b"></animate>
4333 <animate attributeName="class" from="a b c" to="a b c d"></animate>
4334 <animate attributeName="class" from="a b c d"></animate>
4335 <animate attributeName="class" to="a d c"></animate>
4336 </a>
4337 </svg>
4338 "##;
4339 let filtered = r##"
4340 <svg>
4341 <a rel="noopener noreferrer">
4342 <animate attributeName="class" values="a b c;a b c;a b"></animate>
4343 <animate attributeName="class" from="a b c" to="a b c"></animate>
4344 <animate attributeName="class" from="a b c"></animate>
4345 <animate attributeName="class" to="a c"></animate>
4346 </a>
4347 </svg>
4348 "##;
4349 let result = Builder::default()
4350 .add_tags(&["svg","a","animate"])
4351 .add_tag_attributes("animate", ["attributeName","values","from","to"])
4352 .add_allowed_classes("a", ["a", "b", "c"])
4353 .clean(fragment);
4354 assert_eq!(
4355 result.to_string(),
4356 filtered,
4357 );
4358 }
4359
4360 #[test]
4361 fn ns_svg_animate_allowed_styles() {
4362 let fragment = r##"
4363 <svg>
4364 <a>
4365 <animate attributeName="style" values="background:red;color:blue"></animate>
4366 <animate attributeName="style" from="background:red;text-decoration:none" to="color: blue;background:red"></animate>
4367 <animate attributeName="style" from="background:red;text-decoration:none"></animate>
4368 <animate attributeName="style" to="text-decoration:none"></animate>
4369 </a>
4370 </svg>
4371 "##;
4372 let filtered = r##"
4373 <svg>
4374 <a rel="noopener noreferrer">
4375 <animate attributeName="style" values="background:red;"></animate>
4376 <animate attributeName="style" from="background:red" to="background:red"></animate>
4377 <animate attributeName="style" from="background:red"></animate>
4378 <animate attributeName="style" to=""></animate>
4379 </a>
4380 </svg>
4381 "##;
4382 let result = Builder::default()
4383 .add_tags(&["svg","a","animate"])
4384 .add_tag_attributes("animate", ["attributeName","values","from","to"])
4385 .add_tag_attributes("a", ["style"])
4386 .filter_style_properties(["background"].into())
4387 .clean(fragment);
4388 assert_eq!(
4389 result.to_string(),
4390 filtered,
4391 );
4392 }
4393
4394 #[test]
4395 fn ns_svg_animate_url_attr_href() {
4396 let fragment = r##"
4397 <svg>
4398 <a id="x1">
4399 </a>
4400 <animate href="#x1" attributeName="xss" values="http://example.com"></animate>
4401 <animate href="#x1" attributeName="href" values="http://example.com"></animate>
4402 <animate href="#x2" attributeName="href" values="http://example.com;/test"></animate>
4403 <animate href="#x2" attributeName="href" values="http://example.com;/test;javascript:xss"></animate>
4404 <animate href="#x2" attributeName="href" from="http://example.com" to="http://example.com"></animate>
4405 <animate href="#x2" attributeName="href" from="javascript:xss" to="http://example.com"></animate>
4406 <animate href="#x2" attributeName="href" from="http://example.com" to="javascript:xss"></animate>
4407 <animate href="#x2" attributeName="href" from="http://example.com" to="./test2"></animate>
4408 <animate href="#x2" attributeName="href" from="./test2" to="http://example.com"></animate>
4409 </svg>
4410 "##;
4411 let filtered = r##"
4412 <svg>
4413 <a rel="noopener noreferrer">
4414 </a>
4415
4416 <animate href="#x1" attributeName="href" values="http://example.com"></animate>
4417
4418
4419
4420
4421
4422
4423
4424 </svg>
4425 "##;
4426 let result = Builder::default()
4427 .add_tags(&["svg","a","animate"])
4428 .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4429 .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4430 .clean(fragment);
4431 assert_eq!(
4432 result.to_string(),
4433 filtered,
4434 );
4435 }
4436
4437 #[test]
4438 fn ns_svg_animate_url_attr_href_depends_on_tag_name() {
4439 let fragment = r##"
4440 <object id="x2"></object>
4441 <svg>
4442 <g id="x1">
4443 </g>
4444 <animate href="#x1" attributeName="data" values="javascript:xss"></animate>
4445 <animate href="#x2" attributeName="data" values="javascript:xss"></animate>
4446 </svg>
4447 "##;
4448 let filtered = r##"
4449 <object id="x2"></object>
4450 <svg>
4451 <g id="x1">
4452 </g>
4453 <animate href="#x1" attributeName="data" values="javascript:xss"></animate>
4454
4455 </svg>
4456 "##;
4457 let result = Builder::default()
4458 .add_tags(&["svg","g","animate","object"])
4459 .add_tag_attributes("animate", ["attributeName","values","href"])
4460 .add_tag_attributes("g", ["data","id"])
4461 .add_tag_attributes("object", ["data","id"])
4462 .clean(fragment);
4463 assert_eq!(
4464 result.to_string(),
4465 filtered,
4466 );
4467 }
4468
4469 #[test]
4470 fn ns_svg_animate_set_attr_href() {
4471 let fragment = r##"
4472 <svg>
4473 <a id="x1">
4474 </a>
4475 <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4476 <animate href="#x1" attributeName="x" from="1" to="2"></animate>
4477 <animate href="#x1" attributeName="x" from="1"></animate>
4478 <animate href="#x1" attributeName="x" to="2"></animate>
4479 <animate href="#x1" attributeName="y" values="1;2;3"></animate>
4480 <animate href="#x1" attributeName="y" from="1" to="2"></animate>
4481 <animate href="#x1" attributeName="y" from="1"></animate>
4482 <animate href="#x2" attributeName="y" to="2"></animate>
4483 </svg>
4484 "##;
4485 let filtered = r##"
4486 <svg>
4487 <a id="x1" x="0" rel="noopener noreferrer">
4488 </a>
4489
4490
4491
4492
4493 <animate href="#x1" attributeName="y" values="1;2;3"></animate>
4494 <animate href="#x1" attributeName="y" from="1" to="2"></animate>
4495 <animate href="#x1" attributeName="y" from="1"></animate>
4496
4497 </svg>
4498 "##;
4499 let result = Builder::default()
4500 .add_tags(&["svg","a","animate"])
4501 .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4502 .add_tag_attributes("a", ["id", "x", "y"])
4503 .set_tag_attribute_value("a", "x", "0")
4504 .clean(fragment);
4505 assert_eq!(
4506 result.to_string(),
4507 filtered,
4508 );
4509 }
4510
4511 #[test]
4512 fn ns_svg_animate_attr_filter_href() {
4513 let fragment = r##"
4514 <svg>
4515 <a id="x1">
4516 </a>
4517 <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4518 <animate href="#x1" attributeName="x" from="1" to="2"></animate>
4519 <animate href="#x1" attributeName="x" from="1"></animate>
4520 <animate href="#x1" attributeName="x" to="2"></animate>
4521 <animate href="#x1" attributeName="y" values="1;2;3"></animate>
4522 <animate href="#x1" attributeName="y" from="1" to="2"></animate>
4523 <animate href="x1" attributeName="y" from="1"></animate>
4524 <animate href="#x2" attributeName="y" to="2"></animate>
4525 </svg>
4526 "##;
4527 let filtered = r##"
4528 <svg>
4529 <a id="x1" rel="noopener noreferrer">
4530 </a>
4531 <animate href="#x1" attributeName="x" values="0;0;0"></animate>
4532 <animate href="#x1" attributeName="x" from="0" to="0"></animate>
4533 <animate href="#x1" attributeName="x" from="0"></animate>
4534 <animate href="#x1" attributeName="x" to="0"></animate>
4535 <animate href="#x1" attributeName="y"></animate>
4536 <animate href="#x1" attributeName="y" to="2"></animate>
4537
4538
4539 </svg>
4540 "##;
4541 let result = Builder::default()
4542 .add_tags(&["svg","a","animate"])
4543 .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4544 .add_tag_attributes("a", ["id", "x", "y"])
4545 .attribute_filter(|_tag, key, value| Some(if key == "x" {
4546 "0".into()
4547 } else if key == "y" && value == "1" {
4548 return None;
4549 } else {
4550 value.into()
4551 }))
4552 .clean(fragment);
4553 assert_eq!(
4554 result.to_string(),
4555 filtered,
4556 );
4557 }
4558
4559 #[test]
4560 fn ns_svg_animate_id_conflict() {
4561 let fragment = r##"
4562 <svg>
4563 <a id="x1">
4564 </a>
4565 <a id="x2">
4566 </a>
4567 <a id="x2">
4568 </a>
4569 <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4570 <animate href="#x2" attributeName="x" values="1;2;3"></animate>
4571 <animate href="#x3" attributeName="x" values="1;2;3"></animate>
4572 </svg>
4573 "##;
4574 let filtered = r##"
4575 <svg>
4576 <a id="x1" rel="noopener noreferrer">
4577 </a>
4578 <a id="x2" rel="noopener noreferrer">
4579 </a>
4580 <a id="x2" rel="noopener noreferrer">
4581 </a>
4582 <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4583
4584
4585 </svg>
4586 "##;
4587 let result = Builder::default()
4588 .add_tags(&["svg","a","animate"])
4589 .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4590 .add_tag_attributes("a", ["id", "x", "y"])
4591 .clean(fragment);
4592 assert_eq!(
4593 result.to_string(),
4594 filtered,
4595 );
4596 }
4597
4598 #[test]
4599 fn xml_processing_instruction() {
4600 // https://blog.slonser.info/posts/dompurify-node-type-confusion/
4601 let fragment = r##"<svg><?xml-stylesheet src='slonser' ?></svg>"##;
4602 let result = String::from(Builder::new().clean(fragment));
4603 assert_eq!(result.to_string(), "");
4604
4605 let fragment = r##"<svg><?xml-stylesheet src='slonser' ?></svg>"##;
4606 let result = String::from(Builder::new().add_tags(&["svg"]).clean(fragment));
4607 assert_eq!(result.to_string(), "<svg></svg>");
4608
4609 let fragment = r##"<svg><?xml-stylesheet ><img src=x onerror="alert('Ammonia bypassed!!!')"> ?></svg>"##;
4610 let result = String::from(Builder::new().add_tags(&["svg"]).clean(fragment));
4611 assert_eq!(result.to_string(), "<svg></svg><img src=\"x\"> ?>");
4612 }
4613
4614 #[test]
4615 fn generic_attribute_prefixes() {
4616 let prefix_data = ["data-"];
4617 let prefix_code = ["code-"];
4618 let mut b = Builder::new();
4619 let mut hs: HashSet<&'_ str> = HashSet::new();
4620 hs.insert("data-");
4621 assert!(b.generic_attribute_prefixes.is_none());
4622 b.generic_attribute_prefixes(hs);
4623 assert!(b.generic_attribute_prefixes.is_some());
4624 assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4625 b.add_generic_attribute_prefixes(&prefix_data);
4626 assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4627 b.add_generic_attribute_prefixes(&prefix_code);
4628 assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 2);
4629 b.rm_generic_attribute_prefixes(&prefix_code);
4630 assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4631 b.rm_generic_attribute_prefixes(&prefix_code);
4632 assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4633 b.rm_generic_attribute_prefixes(&prefix_data);
4634 assert!(b.generic_attribute_prefixes.is_none());
4635 }
4636
4637 #[test]
4638 fn selectedcontent() {
4639 // https://github.com/servo/html5ever/issues/712
4640 let fragment1 = r#"<select><selectedcontent></selectedcontent><option>X"#;
4641 let fragment2 = r#"<select><selectedcontent></selectedcontent><option>X</option></select>"#;
4642 let expected = r#"<select><selectedcontent></selectedcontent><option>X</option></select>"#;
4643 assert_eq!(String::from(Builder::new().add_tags(&["select", "selectedcontent", "option"]).clean(fragment1)), expected);
4644 assert_eq!(String::from(Builder::new().add_tags(&["select", "selectedcontent", "option"]).clean(fragment2)), expected);
4645 }
4646
4647 #[test]
4648 fn new_select_parse() {
4649 // https://github.com/whatwg/html/issues/10310#issuecomment-2304377029
4650 let fragment = r#"
4651<select><style></select><img src onerror=xss()></style></select>
4652 "#;
4653 let expected = r#"
4654<select></select>
4655 "#;
4656 assert_eq!(String::from(Builder::new().add_tags(&["select", "new-select"]).clean_content_tags(hashset!["style"]).clean(fragment)), expected);
4657 }
4658
4659 #[test]
4660 fn selectedcontent_not_in_select() {
4661 // https://github.com/whatwg/html/issues/10310#issuecomment-2304377029
4662 let fragment = r#"
4663<selectedcontent>first</selectedcontent>
4664<div><selectedcontent>second</selectedcontent></div>
4665<select><selectedcontent>third</selectedcontent></select>
4666 "#;
4667 let expected = r#"
4668<selectedcontent>first</selectedcontent>
4669<div><selectedcontent>second</selectedcontent></div>
4670<select><selectedcontent></selectedcontent></select>
4671 "#;
4672 assert_eq!(String::from(Builder::new().add_tags(&["select", "selectedcontent"]).clean(fragment)), expected);
4673 }
4674
4675 #[test]
4676 fn generic_attribute_prefixes_clean() {
4677 let fragment = r#"<a data-1 data-2 code-1 code-2><a>Hello!</a></a>"#;
4678 let result_cleaned = String::from(
4679 Builder::new()
4680 .add_tag_attributes("a", &["data-1"])
4681 .clean(fragment),
4682 );
4683 assert_eq!(
4684 result_cleaned,
4685 r#"<a data-1="" rel="noopener noreferrer"></a><a rel="noopener noreferrer">Hello!</a>"#
4686 );
4687 let result_allowed = String::from(
4688 Builder::new()
4689 .add_tag_attributes("a", &["data-1"])
4690 .add_generic_attribute_prefixes(&["data-"])
4691 .clean(fragment),
4692 );
4693 assert_eq!(
4694 result_allowed,
4695 r#"<a data-1="" data-2="" rel="noopener noreferrer"></a><a rel="noopener noreferrer">Hello!</a>"#
4696 );
4697 let result_allowed = String::from(
4698 Builder::new()
4699 .add_tag_attributes("a", &["data-1", "code-1"])
4700 .add_generic_attribute_prefixes(&["data-", "code-"])
4701 .clean(fragment),
4702 );
4703 assert_eq!(
4704 result_allowed,
4705 r#"<a data-1="" data-2="" code-1="" code-2="" rel="noopener noreferrer"></a><a rel="noopener noreferrer">Hello!</a>"#
4706 );
4707 }
4708 #[test]
4709 fn lesser_than_isnt_html() {
4710 let fragment = "1 < 2";
4711 assert!(!is_html(fragment));
4712 }
4713 #[test]
4714 fn dense_lesser_than_isnt_html() {
4715 let fragment = "1<2";
4716 assert!(!is_html(fragment));
4717 }
4718 #[test]
4719 fn what_about_number_elements() {
4720 let fragment = "foo<2>bar";
4721 assert!(!is_html(fragment));
4722 }
4723 #[test]
4724 fn turbofish_is_html_sadly() {
4725 let fragment = "Vec::<u8>::new()";
4726 assert!(is_html(fragment));
4727 }
4728 #[test]
4729 fn stop_grinning() {
4730 let fragment = "did you really believe me? <g>";
4731 assert!(is_html(fragment));
4732 }
4733 #[test]
4734 fn dont_be_bold() {
4735 let fragment = "<b>";
4736 assert!(is_html(fragment));
4737 }
4738
4739 #[test]
4740 fn rewrite_with_root() {
4741 let tests = [
4742 (
4743 "https://github.com/rust-ammonia/ammonia/blob/master/",
4744 "README.md",
4745 "",
4746 "https://github.com/rust-ammonia/ammonia/blob/master/README.md",
4747 ),
4748 (
4749 "https://github.com/rust-ammonia/ammonia/blob/master/",
4750 "README.md",
4751 "/",
4752 "https://github.com/rust-ammonia/ammonia/blob/master/",
4753 ),
4754 (
4755 "https://github.com/rust-ammonia/ammonia/blob/master/",
4756 "README.md",
4757 "/CONTRIBUTING.md",
4758 "https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md",
4759 ),
4760 (
4761 "https://github.com/rust-ammonia/ammonia/blob/master",
4762 "README.md",
4763 "",
4764 "https://github.com/rust-ammonia/ammonia/blob/README.md",
4765 ),
4766 (
4767 "https://github.com/rust-ammonia/ammonia/blob/master",
4768 "README.md",
4769 "/",
4770 "https://github.com/rust-ammonia/ammonia/blob/",
4771 ),
4772 (
4773 "https://github.com/rust-ammonia/ammonia/blob/master",
4774 "README.md",
4775 "/CONTRIBUTING.md",
4776 "https://github.com/rust-ammonia/ammonia/blob/CONTRIBUTING.md",
4777 ),
4778 (
4779 "https://github.com/rust-ammonia/ammonia/blob/master/",
4780 "",
4781 "",
4782 "https://github.com/rust-ammonia/ammonia/blob/master/",
4783 ),
4784 (
4785 "https://github.com/rust-ammonia/ammonia/blob/master/",
4786 "",
4787 "/",
4788 "https://github.com/rust-ammonia/ammonia/blob/master/",
4789 ),
4790 (
4791 "https://github.com/rust-ammonia/ammonia/blob/master/",
4792 "",
4793 "/CONTRIBUTING.md",
4794 "https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md",
4795 ),
4796 (
4797 "https://github.com/",
4798 "rust-ammonia/ammonia/blob/master/README.md",
4799 "",
4800 "https://github.com/rust-ammonia/ammonia/blob/master/README.md",
4801 ),
4802 (
4803 "https://github.com/",
4804 "rust-ammonia/ammonia/blob/master/README.md",
4805 "/",
4806 "https://github.com/",
4807 ),
4808 (
4809 "https://github.com/",
4810 "rust-ammonia/ammonia/blob/master/README.md",
4811 "CONTRIBUTING.md",
4812 "https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md",
4813 ),
4814 (
4815 "https://github.com/",
4816 "rust-ammonia/ammonia/blob/master/README.md",
4817 "/CONTRIBUTING.md",
4818 "https://github.com/CONTRIBUTING.md",
4819 ),
4820 ];
4821 for (root, path, url, result) in tests {
4822 let h = format!(r#"<a href="{url}">test</a>"#);
4823 let r = format!(r#"<a href="{result}" rel="noopener noreferrer">test</a>"#);
4824 let a = Builder::new()
4825 .url_relative(UrlRelative::RewriteWithRoot {
4826 root: Url::parse(root).unwrap(),
4827 path: path.to_string(),
4828 })
4829 .clean(&h)
4830 .to_string();
4831 if r != a {
4832 println!(
4833 "failed to check ({root}, {path}, {url}, {result})\n{r} != {a}",
4834 r = r
4835 );
4836 assert_eq!(r, a);
4837 }
4838 }
4839 }
4840}