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 /// Returns `true` if the relative URL resolver is set to `RewriteWithBase`.
1444 ///
1445 /// # Examples
1446 ///
1447 /// use ammonia::{Builder, Url, UrlRelative};
1448 /// # fn main() -> Result<(), url::ParseError> {
1449 /// let mut a = Builder::default();
1450 /// a.url_relative(UrlRelative::RewriteWithBase(Url::parse("https://example.com/")?));
1451 /// assert!(a.is_url_relative_rewrite_with_base());
1452 /// a.url_relative(UrlRelative::PassThrough);
1453 /// assert!(!a.is_url_relative_rewrite_with_base());
1454 /// # Ok(())
1455 /// # }
1456 pub fn is_url_relative_rewrite_with_base(&self) -> bool {
1457 matches!(self.url_relative, UrlRelative::RewriteWithBase(_))
1458 }
1459
1460 /// Returns `true` if the relative URL resolver is set to `RewriteWithRoot`.
1461 ///
1462 /// # Examples
1463 ///
1464 /// use ammonia::{Builder, Url, UrlRelative};
1465 /// # fn main() -> Result<(), url::ParseError> {
1466 /// let mut a = Builder::default();
1467 /// a.url_relative(UrlRelative::RewriteWithRoot {
1468 /// root: Url::parse("https://example.com/")?,
1469 /// path: "index.html".to_string(),
1470 /// });
1471 /// assert!(a.is_url_relative_rewrite_with_root());
1472 /// a.url_relative(UrlRelative::PassThrough);
1473 /// assert!(!a.is_url_relative_rewrite_with_root());
1474 /// # Ok(())
1475 /// # }
1476 pub fn is_url_relative_rewrite_with_root(&self) -> bool {
1477 matches!(self.url_relative, UrlRelative::RewriteWithRoot { .. })
1478 }
1479
1480 /// Returns the base [`Url`] when the relative URL resolver is set to
1481 /// [`UrlRelative::RewriteWithBase`], or `None` otherwise.
1482 ///
1483 /// # Examples
1484 ///
1485 /// use ammonia::{Builder, Url, UrlRelative};
1486 /// # fn main() -> Result<(), url::ParseError> {
1487 /// let base = Url::parse("https://example.com/")?;
1488 /// let mut a = Builder::default();
1489 /// a.url_relative(UrlRelative::RewriteWithBase(base.clone()));
1490 /// assert_eq!(a.url_relative_base(), Some(&base));
1491 /// a.url_relative(UrlRelative::PassThrough);
1492 /// assert_eq!(a.url_relative_base(), None);
1493 /// # Ok(())
1494 /// # }
1495 pub fn url_relative_base(&self) -> Option<&Url> {
1496 match self.url_relative {
1497 UrlRelative::RewriteWithBase(ref base) => Some(base),
1498 _ => None,
1499 }
1500 }
1501
1502 /// Configures a `rel` attribute that will be added on links.
1503 ///
1504 /// If `rel` is in the generic or tag attributes, this must be set to `None`.
1505 /// Common `rel` values to include:
1506 ///
1507 /// * `noopener`: This prevents [a particular type of XSS attack],
1508 /// and should usually be turned on for untrusted HTML.
1509 /// * `noreferrer`: This prevents the browser from [sending the source URL]
1510 /// to the website that is linked to.
1511 /// * `nofollow`: This prevents search engines from [using this link for
1512 /// ranking], which disincentivizes spammers.
1513 ///
1514 /// To turn on rel-insertion, call this function with a space-separated list.
1515 /// Ammonia does not parse rel-attributes;
1516 /// it just puts the given string into the attribute directly.
1517 ///
1518 /// [a particular type of XSS attack]: https://mathiasbynens.github.io/rel-noopener/
1519 /// [sending the source URL]: https://en.wikipedia.org/wiki/HTTP_referer
1520 /// [using this link for ranking]: https://en.wikipedia.org/wiki/Nofollow
1521 ///
1522 /// # Examples
1523 ///
1524 /// use ammonia::Builder;
1525 ///
1526 /// let a = Builder::new().link_rel(None)
1527 /// .clean("<a href=https://rust-lang.org/>Rust</a>")
1528 /// .to_string();
1529 /// assert_eq!(
1530 /// a,
1531 /// "<a href=\"https://rust-lang.org/\">Rust</a>");
1532 ///
1533 /// # Defaults
1534 ///
1535 /// ```notest
1536 /// Some("noopener noreferrer")
1537 /// ```
1538 pub fn link_rel(&mut self, value: Option<&'a str>) -> &mut Self {
1539 self.link_rel = value;
1540 self
1541 }
1542
1543 /// Returns the settings for links' `rel` attribute, if one is set.
1544 ///
1545 /// # Examples
1546 ///
1547 /// use ammonia::{Builder, UrlRelative};
1548 /// let mut a = Builder::default();
1549 /// a.link_rel(Some("a b"));
1550 /// assert_eq!(a.get_link_rel(), Some("a b"));
1551 pub fn get_link_rel(&self) -> Option<&str> {
1552 self.link_rel
1553 }
1554
1555 /// Sets the CSS classes that are allowed on specific tags.
1556 ///
1557 /// The values is structured as a map from tag names to a set of class names.
1558 ///
1559 /// If the `class` attribute is itself whitelisted for a tag, then adding entries to
1560 /// this map will cause a panic.
1561 ///
1562 /// # Examples
1563 ///
1564 /// use ammonia::Builder;
1565 /// use maplit::{hashmap, hashset};
1566 ///
1567 /// # fn main() {
1568 /// let allowed_classes = hashmap![
1569 /// "code" => hashset!["rs", "ex", "c", "cxx", "js"]
1570 /// ];
1571 /// let a = Builder::new()
1572 /// .allowed_classes(allowed_classes)
1573 /// .clean("<code class=rs>fn main() {}</code>")
1574 /// .to_string();
1575 /// assert_eq!(
1576 /// a,
1577 /// "<code class=\"rs\">fn main() {}</code>");
1578 /// # }
1579 ///
1580 /// # Defaults
1581 ///
1582 /// The set of allowed classes is empty by default.
1583 pub fn allowed_classes(&mut self, value: HashMap<&'a str, HashSet<&'a str>>) -> &mut Self {
1584 self.allowed_classes = value;
1585 self
1586 }
1587
1588 /// Add additonal whitelisted classes without overwriting old ones.
1589 ///
1590 /// # Examples
1591 ///
1592 /// let a = ammonia::Builder::default()
1593 /// .add_allowed_classes("a", &["onebox"])
1594 /// .clean("<a href=/ class=onebox>mess</span>").to_string();
1595 /// assert_eq!("<a href=\"/\" class=\"onebox\" rel=\"noopener noreferrer\">mess</a>", a);
1596 pub fn add_allowed_classes<
1597 T: 'a + ?Sized + Borrow<str>,
1598 U: 'a + ?Sized + Borrow<str>,
1599 I: IntoIter<Item = &'a T>,
1600 >(
1601 &mut self,
1602 tag: &'a U,
1603 it: I,
1604 ) -> &mut Self {
1605 self.allowed_classes
1606 .entry(tag.borrow())
1607 .or_default()
1608 .extend(it.into_iter().map(Borrow::borrow));
1609 self
1610 }
1611
1612 /// Remove already-whitelisted attributes.
1613 ///
1614 /// Does nothing if the attribute is already gone.
1615 ///
1616 /// # Examples
1617 ///
1618 /// let a = ammonia::Builder::default()
1619 /// .add_allowed_classes("span", &["active"])
1620 /// .rm_allowed_classes("span", &["active"])
1621 /// .clean("<span class=active>").to_string();
1622 /// assert_eq!("<span class=\"\"></span>", a);
1623 pub fn rm_allowed_classes<
1624 'b,
1625 'c,
1626 T: 'b + ?Sized + Borrow<str>,
1627 U: 'c + ?Sized + Borrow<str>,
1628 I: IntoIter<Item = &'b T>,
1629 >(
1630 &mut self,
1631 tag: &'c U,
1632 it: I,
1633 ) -> &mut Self {
1634 if let Some(tag) = self.allowed_classes.get_mut(tag.borrow()) {
1635 for i in it {
1636 tag.remove(i.borrow());
1637 }
1638 }
1639 self
1640 }
1641
1642 /// Returns a copy of the set of whitelisted class attributes.
1643 ///
1644 /// # Examples
1645 ///
1646 /// use maplit::{hashmap, hashset};
1647 ///
1648 /// let allowed_classes = hashmap![
1649 /// "my-tag" => hashset!["my-class-1", "my-class-2"]
1650 /// ];
1651 ///
1652 /// let mut b = ammonia::Builder::default();
1653 /// b.allowed_classes(Clone::clone(&allowed_classes));
1654 /// assert_eq!(allowed_classes, b.clone_allowed_classes());
1655 pub fn clone_allowed_classes(&self) -> HashMap<&'a str, HashSet<&'a str>> {
1656 self.allowed_classes.clone()
1657 }
1658
1659 /// Configures the handling of HTML comments.
1660 ///
1661 /// If this option is false, comments will be preserved.
1662 ///
1663 /// # Examples
1664 ///
1665 /// use ammonia::Builder;
1666 ///
1667 /// let a = Builder::new().strip_comments(false)
1668 /// .clean("<!-- yes -->")
1669 /// .to_string();
1670 /// assert_eq!(
1671 /// a,
1672 /// "<!-- yes -->");
1673 ///
1674 /// # Defaults
1675 ///
1676 /// `true`
1677 pub fn strip_comments(&mut self, value: bool) -> &mut Self {
1678 self.strip_comments = value;
1679 self
1680 }
1681
1682 /// Returns `true` if comment stripping is turned on.
1683 ///
1684 /// # Examples
1685 ///
1686 /// let mut a = ammonia::Builder::new();
1687 /// a.strip_comments(true);
1688 /// assert!(a.will_strip_comments());
1689 /// a.strip_comments(false);
1690 /// assert!(!a.will_strip_comments());
1691 pub fn will_strip_comments(&self) -> bool {
1692 self.strip_comments
1693 }
1694
1695 /// Prefixes all "id" attribute values with a given string. Note that the tag and
1696 /// attribute themselves must still be whitelisted.
1697 ///
1698 /// # Examples
1699 ///
1700 /// use ammonia::Builder;
1701 /// use maplit::hashset;
1702 ///
1703 /// # fn main() {
1704 /// let attributes = hashset!["id"];
1705 /// let a = Builder::new()
1706 /// .generic_attributes(attributes)
1707 /// .id_prefix(Some("safe-"))
1708 /// .clean("<b id=42>")
1709 /// .to_string();
1710 /// assert_eq!(a, "<b id=\"safe-42\"></b>");
1711 /// # }
1712
1713 ///
1714 /// # Defaults
1715 ///
1716 /// `None`
1717 pub fn id_prefix(&mut self, value: Option<&'a str>) -> &mut Self {
1718 self.id_prefix = value;
1719 self
1720 }
1721
1722 /// Only allows the specified properties in `style` attributes.
1723 ///
1724 /// Irrelevant if `style` is not an allowed attribute.
1725 ///
1726 /// Note that if style filtering is enabled style properties will be normalised e.g.
1727 /// invalid declarations and @rules will be removed, with only syntactically valid
1728 /// declarations kept.
1729 ///
1730 /// # Examples
1731 ///
1732 /// use ammonia::Builder;
1733 /// use maplit::hashset;
1734 ///
1735 /// # fn main() {
1736 /// let attributes = hashset!["style"];
1737 /// let properties = hashset!["color"];
1738 /// let a = Builder::new()
1739 /// .generic_attributes(attributes)
1740 /// .filter_style_properties(properties)
1741 /// .clean("<p style=\"font-weight: heavy; color: red\">my html</p>")
1742 /// .to_string();
1743 /// assert_eq!(a, "<p style=\"color:red\">my html</p>");
1744 /// # }
1745 pub fn filter_style_properties(&mut self, value: HashSet<&'a str>) -> &mut Self {
1746 self.style_properties = Some(value);
1747 self
1748 }
1749
1750 /// Constructs a [`Builder`] instance configured with the [default options].
1751 ///
1752 /// # Examples
1753 ///
1754 /// use ammonia::{Builder, Url, UrlRelative};
1755 /// # use std::error::Error;
1756 ///
1757 /// # fn do_main() -> Result<(), Box<dyn Error>> {
1758 /// 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>.";
1759 /// 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>.";
1760 ///
1761 /// let result = Builder::new() // <--
1762 /// .url_relative(UrlRelative::RewriteWithBase(Url::parse("https://docs.rs/ammonia/1.0/ammonia/")?))
1763 /// .clean(input)
1764 /// .to_string();
1765 /// assert_eq!(result, output);
1766 /// # Ok(())
1767 /// # }
1768 /// # fn main() { do_main().unwrap() }
1769 ///
1770 /// [default options]: fn.clean.html
1771 /// [`Builder`]: struct.Builder.html
1772 pub fn new() -> Self {
1773 Self::default()
1774 }
1775
1776 /// Constructs a [`Builder`] instance configured with no allowed tags.
1777 ///
1778 /// # Examples
1779 ///
1780 /// use ammonia::{Builder, Url, UrlRelative};
1781 /// # use std::error::Error;
1782 ///
1783 /// # fn do_main() -> Result<(), Box<dyn Error>> {
1784 /// 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>.";
1785 /// let output = "This is an Ammonia example using the empty() function.";
1786 ///
1787 /// let result = Builder::empty() // <--
1788 /// .url_relative(UrlRelative::RewriteWithBase(Url::parse("https://docs.rs/ammonia/1.0/ammonia/")?))
1789 /// .clean(input)
1790 /// .to_string();
1791 /// assert_eq!(result, output);
1792 /// # Ok(())
1793 /// # }
1794 /// # fn main() { do_main().unwrap() }
1795 ///
1796 /// [default options]: fn.clean.html
1797 /// [`Builder`]: struct.Builder.html
1798 pub fn empty() -> Self {
1799 Self {
1800 tags: hashset![],
1801 ..Self::default()
1802 }
1803 }
1804
1805 /// Sanitizes an HTML fragment in a string according to the configured options.
1806 ///
1807 /// # Examples
1808 ///
1809 /// use ammonia::{Builder, Url, UrlRelative};
1810 /// # use std::error::Error;
1811 ///
1812 /// # fn do_main() -> Result<(), Box<dyn Error>> {
1813 /// 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>.";
1814 /// 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>.";
1815 ///
1816 /// let result = Builder::new()
1817 /// .url_relative(UrlRelative::RewriteWithBase(Url::parse("https://docs.rs/ammonia/1.0/ammonia/")?))
1818 /// .clean(input)
1819 /// .to_string(); // <--
1820 /// assert_eq!(result, output);
1821 /// # Ok(())
1822 /// # }
1823 /// # fn main() { do_main().unwrap() }
1824 pub fn clean(&self, src: &str) -> Document {
1825 let parser = Self::make_parser();
1826 let dom = parser.one(src);
1827 self.clean_dom(dom)
1828 }
1829
1830 /// Sanitizes an HTML fragment from a reader according to the configured options.
1831 ///
1832 /// The input should be in UTF-8 encoding, otherwise the decoding is lossy, just
1833 /// like when using [`String::from_utf8_lossy`].
1834 ///
1835 /// To avoid consuming the reader, a mutable reference can be passed to this method.
1836 ///
1837 /// # Examples
1838 ///
1839 /// use ammonia::Builder;
1840 /// # use std::error::Error;
1841 ///
1842 /// # fn do_main() -> Result<(), Box<dyn Error>> {
1843 /// let a = Builder::new()
1844 /// .clean_from_reader(&b"<!-- no -->"[..])? // notice the `b`
1845 /// .to_string();
1846 /// assert_eq!(a, "");
1847 /// # Ok(()) }
1848 /// # fn main() { do_main().unwrap() }
1849 ///
1850 /// [`String::from_utf8_lossy`]: https://doc.rust-lang.org/std/string/struct.String.html#method.from_utf8_lossy
1851 pub fn clean_from_reader<R>(&self, mut src: R) -> io::Result<Document>
1852 where
1853 R: io::Read,
1854 {
1855 let parser = Self::make_parser().from_utf8();
1856 let dom = parser.read_from(&mut src)?;
1857 Ok(self.clean_dom(dom))
1858 }
1859
1860 /// Clean a post-parsing DOM.
1861 ///
1862 /// This is not a public API because RcDom isn't really stable.
1863 /// We want to be able to take breaking changes to html5ever itself
1864 /// without having to break Ammonia's API.
1865 fn clean_dom(&self, dom: RcDom) -> Document {
1866 let mut id_to_tag_name_map = HashMap::new();
1867 let mut id_to_tag_name_stack = vec![{
1868 let children = dom.document.children.borrow();
1869 children[0].clone()
1870 }];
1871 while let Some(tag) = id_to_tag_name_stack.pop() {
1872 if let NodeData::Element { name, attrs, .. } = &tag.data {
1873 let attrs = attrs.borrow();
1874 for attr in &attrs[..] {
1875 if &*attr.name.local == "id" {
1876 id_to_tag_name_map.entry(attr.value.to_string()).and_modify(|ent| *ent = None).or_insert_with(|| Some(name.local.to_string()));
1877 }
1878 }
1879 }
1880 id_to_tag_name_stack.extend(tag.children.borrow().iter().map(|x| x.clone()));
1881 }
1882
1883 let mut stack = Vec::new();
1884 let mut removed = Vec::new();
1885 let link_rel = self
1886 .link_rel
1887 .map(|link_rel| format_tendril!("{}", link_rel));
1888 if link_rel.is_some() {
1889 assert!(self.generic_attributes.get("rel").is_none());
1890 assert!(self
1891 .tag_attributes
1892 .get("a")
1893 .and_then(|a| a.get("rel"))
1894 .is_none());
1895 }
1896 assert!(self.allowed_classes.is_empty() || !self.generic_attributes.contains("class"));
1897 for tag_name in self.allowed_classes.keys() {
1898 assert!(self
1899 .tag_attributes
1900 .get(tag_name)
1901 .and_then(|a| a.get("class"))
1902 .is_none());
1903 }
1904 for tag_name in &self.clean_content_tags {
1905 assert!(!self.tags.contains(tag_name), "`{tag_name}` appears in `clean_content_tags` and in `tags` at the same time");
1906 assert!(!self.tag_attributes.contains_key(tag_name), "`{tag_name}` appears in `clean_content_tags` and in `tag_attributes` at the same time");
1907 }
1908 let body = {
1909 let children = dom.document.children.borrow();
1910 children[0].clone()
1911 };
1912 stack.extend(
1913 mem::take(&mut *body.children.borrow_mut())
1914 .into_iter()
1915 .rev(),
1916 );
1917 // This design approach is used to prevent pathological content from producing
1918 // a stack overflow. The `stack` contains to-be-cleaned nodes, while `remove`,
1919 // of course, contains nodes that need to be dropped (we can't just drop them,
1920 // because they could have a very deep child tree).
1921 while let Some(mut node) = stack.pop() {
1922 if matches!(node.data, NodeData::Element { ref name, .. } if &*name.local == "selectedcontent" && name.ns == ns!(html)) &&
1923 self.is_within(node.clone(), ns!(html), "select")
1924 {
1925 for sub in node.children.borrow_mut().iter_mut() {
1926 sub.parent.replace(None);
1927 }
1928 *node.children.borrow_mut() = Vec::new();
1929 }
1930 let parent = node.parent
1931 .replace(None).expect("a node in the DOM will have a parent, except the root, which is not processed")
1932 .upgrade().expect("a node's parent will be pointed to by its parent (or the root pointer), and will not be dropped");
1933 let pass = self.clean_child(&mut node, &parent, &id_to_tag_name_map);
1934 self.adjust_node_attributes(&mut node, &link_rel, self.id_prefix, &parent, &id_to_tag_name_map);
1935 if self.clean_node_content(&node) || !self.check_expected_namespace(&parent, &node) {
1936 removed.push(node);
1937 continue;
1938 }
1939 if pass {
1940 dom.append(&parent.clone(), NodeOrText::AppendNode(node.clone()));
1941 } else {
1942 for sub in node.children.borrow_mut().iter_mut() {
1943 sub.parent.replace(Some(Rc::downgrade(&parent)));
1944 }
1945 }
1946 stack.extend(
1947 mem::take(&mut *node.children.borrow_mut())
1948 .into_iter()
1949 .rev(),
1950 );
1951 if !pass {
1952 removed.push(node);
1953 }
1954 }
1955 // Now, imperatively clean up all of the child nodes.
1956 // Otherwise, we could wind up with a DoS, either caused by a memory leak,
1957 // or caused by a stack overflow.
1958 while let Some(node) = removed.pop() {
1959 removed.extend_from_slice(&mem::take(&mut *node.children.borrow_mut())[..]);
1960 }
1961 Document(dom)
1962 }
1963
1964 fn is_within(&self, mut child: Handle, ns: Namespace, tag: &str) -> bool {
1965 while let Some(parent) = child.parent.take() {
1966 child.parent.set(Some(parent.clone()));
1967 match child.data {
1968 NodeData::Element { ref name, .. } if name.ns == ns && &*name.local == tag => return true,
1969 _ => {
1970 if let Some(parent) = parent.upgrade() {
1971 child = parent;
1972 } else {
1973 return false;
1974 }
1975 }
1976 }
1977 }
1978 false
1979 }
1980
1981 /// Returns `true` if a node and all its content should be removed.
1982 fn clean_node_content(&self, node: &Handle) -> bool {
1983 match node.data {
1984 NodeData::Text { .. }
1985 | NodeData::Comment { .. }
1986 | NodeData::Doctype { .. }
1987 | NodeData::Document
1988 | NodeData::ProcessingInstruction { .. } => false,
1989 NodeData::Element { ref name, .. } => self.clean_content_tags.contains(&*name.local),
1990 }
1991 }
1992
1993 /// Remove unwanted attributes, and check if the node should be kept or not.
1994 ///
1995 /// The root node doesn't need cleaning because we create the root node ourselves,
1996 /// and it doesn't get serialized, and ... it just exists to give the parser
1997 /// a context (in this case, a div-like block context).
1998 fn clean_child(&self, child: &mut Handle, parent: &Handle, id_to_tag_name_map: &HashMap<String, Option<String>>) -> bool {
1999 match child.data {
2000 NodeData::Text { .. } => true,
2001 NodeData::Comment { .. } => !self.strip_comments,
2002 NodeData::Doctype { .. }
2003 | NodeData::Document
2004 | NodeData::ProcessingInstruction { .. } => false,
2005 NodeData::Element {
2006 ref name,
2007 ref attrs,
2008 ..
2009 } => {
2010 if self.tags.contains(&*name.local) {
2011 let whitelisted = |tag_name: &str, attr_name: &str, attr_val: &str|
2012 self.generic_attributes.contains(attr_name)
2013 || self.generic_attribute_prefixes.as_ref().map(|prefixes| {
2014 prefixes.iter().any(|&p| attr_name.starts_with(p))
2015 }) == Some(true)
2016 || self
2017 .tag_attributes
2018 .get(tag_name)
2019 .map(|ta| ta.contains(attr_name))
2020 == Some(true)
2021 || self
2022 .tag_attribute_values
2023 .get(tag_name)
2024 .and_then(|tav| tav.get(attr_name))
2025 .map(|vs| {
2026 vs.iter().any(|v| v.to_lowercase() == attr_val.to_lowercase())
2027 })
2028 == Some(true);
2029 let attr_filter = |tag_name: &str, attr_name: &str, attr_val: &str| {
2030 if !whitelisted(tag_name, attr_name, attr_val) {
2031 // If the class attribute is not whitelisted,
2032 // but there is a whitelisted set of allowed_classes,
2033 // do not strip out the class attribute.
2034 // Banned classes will be filtered later.
2035 attr_name == "class" && self.allowed_classes.contains_key(tag_name)
2036 } else if is_url_attr(tag_name, attr_name) {
2037 let url = Url::parse(attr_val);
2038 if let Ok(url) = url {
2039 self.url_schemes.contains(url.scheme())
2040 } else if url == Err(url::ParseError::RelativeUrlWithoutBase) {
2041 !matches!(self.url_relative, UrlRelative::Deny)
2042 } else {
2043 false
2044 }
2045 } else {
2046 true
2047 }
2048 };
2049 attrs.borrow_mut().retain(|attr| attr_filter(&*name.local, &*attr.name.local, &*attr.value));
2050 if
2051 // https://svgwg.org/specs/animations/#AnimateElement
2052 name.ns == ns!(svg) &&
2053 (&*name.local == "animate" || &*name.local == "set")
2054 {
2055 let animate_name = attrs.borrow()
2056 .iter()
2057 .find(|attr| &*attr.name.local == "attributeName")
2058 .map(|attr| attr.value.clone());
2059 let animate_values = attrs.borrow()
2060 .iter()
2061 .find(|attr| &*attr.name.local == "values")
2062 .map(|attr| attr.value.clone());
2063 let animate_from = attrs.borrow()
2064 .iter()
2065 .find(|attr| &*attr.name.local == "from")
2066 .map(|attr| attr.value.clone());
2067 let animate_to = attrs.borrow()
2068 .iter()
2069 .find(|attr| &*attr.name.local == "to")
2070 .map(|attr| attr.value.clone());
2071 let animate_href = attrs.borrow()
2072 .iter()
2073 .find(|attr| &*attr.name.local == "href")
2074 .map(|attr| attr.value.clone());
2075 let animate_tag_name = animate_href
2076 .map(|href| {
2077 if href.starts_with("#") {
2078 id_to_tag_name_map.get(&href[1..]).and_then(|inner| Some(&inner.as_ref()?[..]))
2079 } else {
2080 None
2081 }
2082 })
2083 .unwrap_or_else(|| {
2084 if let &NodeData::Element { name: ref parent_name, .. } = &parent.data {
2085 Some(&*parent_name.local)
2086 } else {
2087 None
2088 }
2089 });
2090 match (animate_name, animate_values, animate_from, animate_to, animate_tag_name) {
2091 (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,
2092 (Some(animate_name), Some(animate_values), None, None, Some(animate_tag_name)) => {
2093 // https://svgwg.org/specs/animations/#ValuesAttribute
2094 animate_values.split(';').all(|attr_val| attr_filter(animate_tag_name, &*animate_name, attr_val))
2095 }
2096 (Some(animate_name), None, Some(animate_from), Some(animate_to), Some(animate_tag_name)) => {
2097 // https://svgwg.org/specs/animations/#FromAttribute
2098 attr_filter(animate_tag_name, &*animate_name, &*animate_from) &&
2099 attr_filter(animate_tag_name, &*animate_name, &*animate_to)
2100 }
2101 (Some(animate_name), None, Some(animate_from), None, Some(animate_tag_name)) => {
2102 // https://svgwg.org/specs/animations/#FromAttribute
2103 attr_filter(animate_tag_name, &*animate_name, &*animate_from)
2104 }
2105 (Some(animate_name), None, None, Some(animate_to), Some(animate_tag_name)) => {
2106 // https://svgwg.org/specs/animations/#FromAttribute
2107 attr_filter(animate_tag_name, &*animate_name, &*animate_to)
2108 }
2109 _ => false,
2110 }
2111 } else {
2112 true
2113 }
2114 } else {
2115 false
2116 }
2117 }
2118 }
2119 }
2120
2121 // Check for unexpected namespace changes.
2122 //
2123 // The issue happens if developers added to the list of allowed tags any
2124 // tag which is parsed in RCDATA state, PLAINTEXT state or RAWTEXT state,
2125 // that is:
2126 //
2127 // * title
2128 // * textarea
2129 // * xmp
2130 // * iframe
2131 // * noembed
2132 // * noframes
2133 // * plaintext
2134 // * noscript
2135 // * style
2136 // * script
2137 //
2138 // An example in the wild is Plume, that allows iframe [1]. So in next
2139 // examples I'll assume the following policy:
2140 //
2141 // Builder::new()
2142 // .add_tags(&["iframe"])
2143 //
2144 // In HTML namespace `<iframe>` is parsed specially; that is, its content is
2145 // treated as text. For instance, the following html:
2146 //
2147 // <iframe><a>test
2148 //
2149 // Is parsed into the following DOM tree:
2150 //
2151 // iframe
2152 // └─ #text: <a>test
2153 //
2154 // So iframe cannot have any children other than a text node.
2155 //
2156 // The same is not true, though, in "foreign content"; that is, within
2157 // <svg> or <math> tags. The following html:
2158 //
2159 // <svg><iframe><a>test
2160 //
2161 // is parsed differently:
2162 //
2163 // svg
2164 // └─ iframe
2165 // └─ a
2166 // └─ #text: test
2167 //
2168 // So in SVG namespace iframe can have children.
2169 //
2170 // Ammonia disallows <svg> but it keeps its content after deleting it. And
2171 // the parser internally keeps track of the namespace of the element. So
2172 // assume we have the following snippet:
2173 //
2174 // <svg><iframe><a title="</iframe><img src onerror=alert(1)>">test
2175 //
2176 // It is parsed into:
2177 //
2178 // svg
2179 // └─ iframe
2180 // └─ a title="</iframe><img src onerror=alert(1)>"
2181 // └─ #text: test
2182 //
2183 // This DOM tree is harmless from ammonia point of view because the piece
2184 // of code that looks like XSS is in a title attribute. Hence, the
2185 // resulting "safe" HTML from ammonia would be:
2186 //
2187 // <iframe><a title="</iframe><img src onerror=alert(1)>" rel="noopener
2188 // noreferrer">test</a></iframe>
2189 //
2190 // However, at this point, the information about namespace is lost, which
2191 // means that the browser will parse this snippet into:
2192 //
2193 // ├─ iframe
2194 // │ └─ #text: <a title="
2195 // ├─ img src="" onerror="alert(1)"
2196 // └─ #text: " rel="noopener noreferrer">test
2197 //
2198 // Leading to XSS.
2199 //
2200 // To solve this issue, check for unexpected namespace switches after cleanup.
2201 // Elements which change namespace at an unexpected point are removed.
2202 // This function returns `true` if `child` should be kept, and `false` if it
2203 // should be removed.
2204 //
2205 // [1]: https://github.com/Plume-org/Plume/blob/main/plume-models/src/safe_string.rs#L21
2206 fn check_expected_namespace(&self, parent: &Handle, child: &Handle) -> bool {
2207 let (parent, parent_attr, child) = match (&parent.data, &child.data) {
2208 (NodeData::Element { name: pn, attrs, .. }, NodeData::Element { name: cn, .. }) => (pn, attrs, cn),
2209 _ => return true,
2210 };
2211 // The only way to switch from html to svg is with the <svg> tag
2212 if parent.ns == ns!(html) && child.ns == ns!(svg) {
2213 child.local == local_name!("svg")
2214 // The only way to switch from html to mathml is with the <math> tag
2215 } else if parent.ns == ns!(html) && child.ns == ns!(mathml) {
2216 child.local == local_name!("math")
2217 // The only way to switch from mathml to svg/html is with a text integration point
2218 } else if parent.ns == ns!(mathml) && child.ns != ns!(mathml) {
2219 // https://html.spec.whatwg.org/#mathml
2220 if &*parent.local == "annotation-xml" {
2221 let parent_attr = parent_attr.borrow();
2222 // https://html.spec.whatwg.org/#tree-construction
2223 if child.ns == ns!(html)
2224 && parent_attr
2225 .iter()
2226 .filter(|attr| attr.name.local == local_name!("encoding"))
2227 .all(|attr| {
2228 &*attr.value == "text/html" || &*attr.value == "application/xhtml+xml"
2229 })
2230 {
2231 is_html_tag(&child.local)
2232 && parent_attr
2233 .iter()
2234 .filter(|attr| attr.name.local == local_name!("encoding"))
2235 .count()
2236 == 1
2237 } else {
2238 child.local == local_name!("svg") && child.ns == ns!(svg)
2239 }
2240 } else {
2241 matches!(&*parent.local, "mi" | "mo" | "mn" | "ms" | "mtext")
2242 && if child.ns == ns!(html) {
2243 is_html_tag(&child.local)
2244 } else {
2245 true
2246 }
2247 }
2248
2249 // The only way to switch from svg to mathml/html is with an html integration point
2250 } else if parent.ns == ns!(svg) && child.ns != ns!(svg) {
2251 // https://html.spec.whatwg.org/#svg-0
2252 matches!(&*parent.local, "foreignObject")
2253 && if child.ns == ns!(html) { is_html_tag(&child.local) } else { true }
2254 } else if child.ns == ns!(svg) {
2255 is_svg_tag(&child.local)
2256 } else if child.ns == ns!(mathml) {
2257 is_mathml_tag(&child.local)
2258 } else if child.ns == ns!(html) {
2259 is_html_tag(&child.local)
2260 } else {
2261 // There are no other supported ways to switch namespace
2262 parent.ns == child.ns
2263 }
2264 }
2265
2266 /// Add and transform special-cased attributes and elements.
2267 ///
2268 /// This function handles:
2269 ///
2270 /// * relative URL rewriting
2271 /// * adding `<a rel>` attributes
2272 /// * filtering out banned style properties
2273 /// * filtering out banned classes
2274 fn adjust_node_attributes(
2275 &self,
2276 child: &mut Handle,
2277 link_rel: &Option<StrTendril>,
2278 id_prefix: Option<&'a str>,
2279 parent: &Handle,
2280 id_to_tag_name_map: &HashMap<String, Option<String>>,
2281 ) {
2282 if let NodeData::Element {
2283 ref name,
2284 ref attrs,
2285 ..
2286 } = child.data
2287 {
2288 if let Some(set_attrs) = self.set_tag_attribute_values.get(&*name.local) {
2289 let mut attrs = attrs.borrow_mut();
2290 for (&set_name, &set_value) in set_attrs {
2291 // set the value of the attribute if the attribute is already present
2292 if let Some(attr) = attrs.iter_mut().find(|attr| &*attr.name.local == set_name)
2293 {
2294 if &*attr.value != set_value {
2295 attr.value = set_value.into();
2296 }
2297 } else {
2298 // otherwise, add the attribute
2299 let attr = Attribute {
2300 name: QualName::new(None, ns!(), set_name.into()),
2301 value: set_value.into(),
2302 };
2303 attrs.push(attr);
2304 }
2305 }
2306 }
2307 if let Some(ref link_rel) = *link_rel {
2308 if &*name.local == "a" {
2309 attrs.borrow_mut().push(Attribute {
2310 name: QualName::new(None, ns!(), local_name!("rel")),
2311 value: link_rel.clone(),
2312 })
2313 }
2314 }
2315 if let Some(ref id_prefix) = id_prefix {
2316 for attr in &mut *attrs.borrow_mut() {
2317 if &attr.name.local == "id" && !attr.value.starts_with(id_prefix) {
2318 attr.value = format_tendril!("{}{}", id_prefix, attr.value);
2319 }
2320 }
2321 }
2322 if let Some(ref attr_filter) = self.attribute_filter {
2323 let mut drop_attrs = Vec::new();
2324 let mut attrs = attrs.borrow_mut();
2325 for (i, attr) in &mut attrs.iter_mut().enumerate() {
2326 let replace_with = if let Some(new) =
2327 attr_filter.filter(&name.local, &attr.name.local, &attr.value)
2328 {
2329 if *new != *attr.value {
2330 Some(format_tendril!("{}", new))
2331 } else {
2332 None // no need to replace the attr if filter returned the same value
2333 }
2334 } else {
2335 drop_attrs.push(i);
2336 None
2337 };
2338 if let Some(replace_with) = replace_with {
2339 attr.value = replace_with;
2340 }
2341 }
2342 for i in drop_attrs.into_iter().rev() {
2343 attrs.swap_remove(i);
2344 }
2345 }
2346 {
2347 let mut drop_attrs = Vec::new();
2348 let mut attrs = attrs.borrow_mut();
2349 for (i, attr) in attrs.iter_mut().enumerate() {
2350 if is_url_attr(&name.local, &attr.name.local) && is_url_relative(&attr.value) {
2351 let new_value = self.url_relative.evaluate(&attr.value);
2352 if let Some(new_value) = new_value {
2353 attr.value = new_value;
2354 } else {
2355 drop_attrs.push(i);
2356 }
2357 }
2358 }
2359 // Swap remove scrambles the vector after the current point.
2360 // We will not do anything except with items before the current point.
2361 // The `rev()` is, as such, necessary for correctness.
2362 // We could use regular `remove(usize)` and a forward iterator,
2363 // but that's slower.
2364 for i in drop_attrs.into_iter().rev() {
2365 attrs.swap_remove(i);
2366 }
2367 }
2368 if let Some(allowed_values) = &self.style_properties {
2369 for attr in &mut *attrs.borrow_mut() {
2370 if &attr.name.local == "style" {
2371 attr.value = style::filter_style_attribute(&attr.value, allowed_values).into();
2372 }
2373 }
2374 }
2375 if let Some(allowed_values) = self.allowed_classes.get(&*name.local) {
2376 for attr in &mut *attrs.borrow_mut() {
2377 if &attr.name.local == "class" {
2378 let mut classes = vec![];
2379 // https://html.spec.whatwg.org/#global-attributes:classes-2
2380 for class in attr.value.split_ascii_whitespace() {
2381 if allowed_values.contains(class) {
2382 classes.push(class.to_owned());
2383 }
2384 }
2385 attr.value = format_tendril!("{}", classes.join(" "));
2386 }
2387 }
2388 }
2389 if
2390 // https://svgwg.org/specs/animations/#AnimateElement
2391 name.ns == ns!(svg) &&
2392 (&*name.local == "animate" || &*name.local == "set")
2393 {
2394 let mut attrs = attrs.borrow_mut();
2395 let animate_name = attrs
2396 .iter()
2397 .find(|attr| &*attr.name.local == "attributeName")
2398 .map(|attr| attr.value.clone());
2399 let animate_href = attrs
2400 .iter()
2401 .find(|attr| &*attr.name.local == "href")
2402 .map(|attr| attr.value.clone());
2403 let animate_tag_name = animate_href
2404 .map(|href| {
2405 if href.starts_with("#") {
2406 id_to_tag_name_map.get(&href[1..]).and_then(|inner| Some(&inner.as_ref()?[..]))
2407 } else {
2408 None
2409 }
2410 })
2411 .unwrap_or_else(|| {
2412 if let &NodeData::Element { name: ref parent_name, .. } = &parent.data {
2413 Some(&*parent_name.local)
2414 } else {
2415 None
2416 }
2417 });
2418 if let (Some(animate_name), Some(animate_tag_name)) = (animate_name, animate_tag_name) {
2419 if let Some(ref attr_filter) = self.attribute_filter {
2420 if let Some((i, animate_values)) = attrs
2421 .iter_mut()
2422 .enumerate()
2423 .find(|(_, attr)| &*attr.name.local == "values")
2424 .map(|(i, attr)| (i, &mut attr.value))
2425 {
2426 let mut drop = false;
2427 let new_value = animate_values.split(';')
2428 .map(|value| {
2429 if let Some(new_value) = attr_filter.filter(animate_tag_name, &animate_name, &value) {
2430 String::from(new_value)
2431 } else {
2432 drop = true;
2433 String::new()
2434 }
2435 })
2436 .collect::<Vec<String>>()
2437 .join(";");
2438 if drop {
2439 attrs.swap_remove(i);
2440 } else {
2441 *animate_values = new_value.into();
2442 }
2443 }
2444 let mut drop_attrs = Vec::new();
2445 for (i, animate_value) in attrs
2446 .iter_mut()
2447 .enumerate()
2448 .filter(|(_, attr)| &*attr.name.local == "from" || &*attr.name.local == "to")
2449 .map(|(i, attr)| (i, &mut attr.value))
2450 {
2451 if let Some(new_value) = attr_filter.filter(animate_tag_name, &animate_name, &animate_value) {
2452 *animate_value = new_value[..].into();
2453 } else {
2454 drop_attrs.push(i);
2455 };
2456 }
2457 for i in drop_attrs.into_iter().rev() {
2458 attrs.swap_remove(i);
2459 }
2460 }
2461 if is_url_attr(animate_tag_name, &*animate_name) {
2462 if let Some((i, animate_values)) = attrs
2463 .iter_mut()
2464 .enumerate()
2465 .find(|(_, attr)| &*attr.name.local == "values")
2466 .map(|(i, attr)| (i, &mut attr.value))
2467 {
2468 let mut drop = false;
2469 let new_value = animate_values.split(';')
2470 .map(|value| {
2471 if !is_url_relative(value) {
2472 String::from(value)
2473 } else if let Some(new_value) = self.url_relative.evaluate(value) {
2474 String::from(new_value)
2475 } else {
2476 drop = true;
2477 String::new()
2478 }
2479 })
2480 .collect::<Vec<String>>()
2481 .join(";");
2482 if drop {
2483 attrs.swap_remove(i);
2484 } else {
2485 *animate_values = new_value.into();
2486 }
2487 }
2488 let mut drop_attrs = Vec::new();
2489 for (i, animate_value) in attrs
2490 .iter_mut()
2491 .enumerate()
2492 .filter(|(_, attr)| &*attr.name.local == "from" || &*attr.name.local == "to")
2493 .map(|(i, attr)| (i, &mut attr.value))
2494 {
2495 if !is_url_relative(animate_value) {
2496 // do nothing
2497 } else if let Some(new_value) = self.url_relative.evaluate(animate_value) {
2498 *animate_value = new_value;
2499 } else {
2500 drop_attrs.push(i);
2501 };
2502 }
2503 for i in drop_attrs.into_iter().rev() {
2504 attrs.swap_remove(i);
2505 }
2506 }
2507 if &*animate_name == "style" {
2508 if let Some(allowed_values) = &self.style_properties {
2509 if let Some(animate_values) = attrs
2510 .iter_mut()
2511 .find(|attr| &*attr.name.local == "values")
2512 .map(|attr| &mut attr.value)
2513 {
2514 let new_value = animate_values.split(';')
2515 .map(|value| {
2516 style::filter_style_attribute(&value, allowed_values)
2517 })
2518 .collect::<Vec<String>>()
2519 .join(";");
2520 *animate_values = new_value.into();
2521 }
2522 for animate_value in attrs
2523 .iter_mut()
2524 .filter(|attr| &*attr.name.local == "from" || &*attr.name.local == "to")
2525 .map(|attr| &mut attr.value)
2526 {
2527 *animate_value = style::filter_style_attribute(&animate_value, allowed_values).into();
2528 }
2529 }
2530 }
2531 if &*animate_name == "class" {
2532 if let Some(allowed_values) = self.allowed_classes.get(animate_tag_name) {
2533 if let Some(animate_values) = attrs
2534 .iter_mut()
2535 .find(|attr| &*attr.name.local == "values")
2536 .map(|attr| &mut attr.value)
2537 {
2538 let new_value = animate_values.split(';')
2539 .map(|value| {
2540 let mut classes = vec![];
2541 // https://html.spec.whatwg.org/#global-attributes:classes-2
2542 for class in value.split_ascii_whitespace() {
2543 if allowed_values.contains(class) {
2544 classes.push(class.to_owned());
2545 }
2546 }
2547 classes.join(" ")
2548 })
2549 .collect::<Vec<String>>()
2550 .join(";");
2551 *animate_values = new_value.into();
2552 }
2553 for animate_value in attrs
2554 .iter_mut()
2555 .filter(|attr| &*attr.name.local == "from" || &*attr.name.local == "to")
2556 .map(|attr| &mut attr.value)
2557 {
2558 let mut classes = vec![];
2559 // https://html.spec.whatwg.org/#global-attributes:classes-2
2560 for class in animate_value.split_ascii_whitespace() {
2561 if allowed_values.contains(class) {
2562 classes.push(class.to_owned());
2563 }
2564 }
2565 *animate_value = classes.join(" ").into();
2566 }
2567 }
2568 }
2569 }
2570 }
2571 }
2572 }
2573
2574 /// Initializes an HTML fragment parser.
2575 ///
2576 /// Ammonia conforms to the HTML5 fragment parsing rules,
2577 /// by parsing the given fragment as if it were included in a <div> tag.
2578 fn make_parser() -> html::Parser<RcDom> {
2579 html::parse_fragment(
2580 RcDom::default(),
2581 html::ParseOpts::default(),
2582 QualName::new(None, ns!(html), local_name!("div")),
2583 vec![],
2584 false,
2585 )
2586 }
2587}
2588
2589/// Given an element name and attribute name, determine if the given attribute contains a URL.
2590fn is_url_attr(element: &str, attr: &str) -> bool {
2591 (element != "animate" && element != "set" && attr == "href")
2592 // Don't have to worry about alternate xmlns prefixes, because HTML doesn't
2593 // parse them, anyway:
2594 // https://html.spec.whatwg.org/#adjust-foreign-attributes
2595 || (element != "animate" && element != "set" && attr == "xlink:href")
2596 || attr == "src"
2597 || (element == "form" && attr == "action")
2598 || (element == "object" && attr == "data")
2599 || ((element == "button" || element == "input") && attr == "formaction")
2600 || (element == "a" && attr == "ping")
2601 || (element == "video" && attr == "poster")
2602}
2603
2604fn is_html_tag(element: &str) -> bool {
2605 (!is_svg_tag(element) && !is_mathml_tag(element))
2606 || matches!(
2607 element,
2608 "title" | "style" | "font" | "a" | "script" | "span"
2609 )
2610}
2611
2612/// Given an element name, check if it's SVG
2613fn is_svg_tag(element: &str) -> bool {
2614 // https://svgwg.org/svg2-draft/eltindex.html
2615 matches!(
2616 element,
2617 "a" | "animate"
2618 | "animateMotion"
2619 | "animateTransform"
2620 | "circle"
2621 | "clipPath"
2622 | "defs"
2623 | "desc"
2624 | "discard"
2625 | "ellipse"
2626 | "feBlend"
2627 | "feColorMatrix"
2628 | "feComponentTransfer"
2629 | "feComposite"
2630 | "feConvolveMatrix"
2631 | "feDiffuseLighting"
2632 | "feDisplacementMap"
2633 | "feDistantLight"
2634 | "feDropShadow"
2635 | "feFlood"
2636 | "feFuncA"
2637 | "feFuncB"
2638 | "feFuncG"
2639 | "feFuncR"
2640 | "feGaussianBlur"
2641 | "feImage"
2642 | "feMerge"
2643 | "feMergeNode"
2644 | "feMorphology"
2645 | "feOffset"
2646 | "fePointLight"
2647 | "feSpecularLighting"
2648 | "feSpotLight"
2649 | "feTile"
2650 | "feTurbulence"
2651 | "filter"
2652 | "foreignObject"
2653 | "g"
2654 | "image"
2655 | "line"
2656 | "linearGradient"
2657 | "marker"
2658 | "mask"
2659 | "metadata"
2660 | "mpath"
2661 | "path"
2662 | "pattern"
2663 | "polygon"
2664 | "polyline"
2665 | "radialGradient"
2666 | "rect"
2667 | "script"
2668 | "set"
2669 | "stop"
2670 | "style"
2671 | "svg"
2672 | "switch"
2673 | "symbol"
2674 | "text"
2675 | "textPath"
2676 | "title"
2677 | "tspan"
2678 | "use"
2679 | "view"
2680 )
2681}
2682
2683/// Given an element name, check if it's Math
2684fn is_mathml_tag(element: &str) -> bool {
2685 // https://svgwg.org/svg2-draft/eltindex.html
2686 matches!(
2687 element,
2688 "abs"
2689 | "and"
2690 | "annotation"
2691 | "annotation-xml"
2692 | "apply"
2693 | "approx"
2694 | "arccos"
2695 | "arccosh"
2696 | "arccot"
2697 | "arccoth"
2698 | "arccsc"
2699 | "arccsch"
2700 | "arcsec"
2701 | "arcsech"
2702 | "arcsin"
2703 | "arcsinh"
2704 | "arctan"
2705 | "arctanh"
2706 | "arg"
2707 | "bind"
2708 | "bvar"
2709 | "card"
2710 | "cartesianproduct"
2711 | "cbytes"
2712 | "ceiling"
2713 | "cerror"
2714 | "ci"
2715 | "cn"
2716 | "codomain"
2717 | "complexes"
2718 | "compose"
2719 | "condition"
2720 | "conjugate"
2721 | "cos"
2722 | "cosh"
2723 | "cot"
2724 | "coth"
2725 | "cs"
2726 | "csc"
2727 | "csch"
2728 | "csymbol"
2729 | "curl"
2730 | "declare"
2731 | "degree"
2732 | "determinant"
2733 | "diff"
2734 | "divergence"
2735 | "divide"
2736 | "domain"
2737 | "domainofapplication"
2738 | "emptyset"
2739 | "eq"
2740 | "equivalent"
2741 | "eulergamma"
2742 | "exists"
2743 | "exp"
2744 | "exponentiale"
2745 | "factorial"
2746 | "factorof"
2747 | "false"
2748 | "floor"
2749 | "fn"
2750 | "forall"
2751 | "gcd"
2752 | "geq"
2753 | "grad"
2754 | "gt"
2755 | "ident"
2756 | "image"
2757 | "imaginary"
2758 | "imaginaryi"
2759 | "implies"
2760 | "in"
2761 | "infinity"
2762 | "int"
2763 | "integers"
2764 | "intersect"
2765 | "interval"
2766 | "inverse"
2767 | "lambda"
2768 | "laplacian"
2769 | "lcm"
2770 | "leq"
2771 | "limit"
2772 | "list"
2773 | "ln"
2774 | "log"
2775 | "logbase"
2776 | "lowlimit"
2777 | "lt"
2778 | "maction"
2779 | "maligngroup"
2780 | "malignmark"
2781 | "math"
2782 | "matrix"
2783 | "matrixrow"
2784 | "max"
2785 | "mean"
2786 | "median"
2787 | "menclose"
2788 | "merror"
2789 | "mfenced"
2790 | "mfrac"
2791 | "mglyph"
2792 | "mi"
2793 | "min"
2794 | "minus"
2795 | "mlabeledtr"
2796 | "mlongdiv"
2797 | "mmultiscripts"
2798 | "mn"
2799 | "mo"
2800 | "mode"
2801 | "moment"
2802 | "momentabout"
2803 | "mover"
2804 | "mpadded"
2805 | "mphantom"
2806 | "mprescripts"
2807 | "mroot"
2808 | "mrow"
2809 | "ms"
2810 | "mscarries"
2811 | "mscarry"
2812 | "msgroup"
2813 | "msline"
2814 | "mspace"
2815 | "msqrt"
2816 | "msrow"
2817 | "mstack"
2818 | "mstyle"
2819 | "msub"
2820 | "msubsup"
2821 | "msup"
2822 | "mtable"
2823 | "mtd"
2824 | "mtext"
2825 | "mtr"
2826 | "munder"
2827 | "munderover"
2828 | "naturalnumbers"
2829 | "neq"
2830 | "none"
2831 | "not"
2832 | "notanumber"
2833 | "notin"
2834 | "notprsubset"
2835 | "notsubset"
2836 | "or"
2837 | "otherwise"
2838 | "outerproduct"
2839 | "partialdiff"
2840 | "pi"
2841 | "piece"
2842 | "piecewise"
2843 | "plus"
2844 | "power"
2845 | "primes"
2846 | "product"
2847 | "prsubset"
2848 | "quotient"
2849 | "rationals"
2850 | "real"
2851 | "reals"
2852 | "reln"
2853 | "rem"
2854 | "root"
2855 | "scalarproduct"
2856 | "sdev"
2857 | "sec"
2858 | "sech"
2859 | "selector"
2860 | "semantics"
2861 | "sep"
2862 | "set"
2863 | "setdiff"
2864 | "share"
2865 | "sin"
2866 | "sinh"
2867 | "span"
2868 | "subset"
2869 | "sum"
2870 | "tan"
2871 | "tanh"
2872 | "tendsto"
2873 | "times"
2874 | "transpose"
2875 | "true"
2876 | "union"
2877 | "uplimit"
2878 | "variance"
2879 | "vector"
2880 | "vectorproduct"
2881 | "xor"
2882 )
2883}
2884
2885fn is_url_relative(url: &str) -> bool {
2886 matches!(
2887 Url::parse(url),
2888 Err(url::ParseError::RelativeUrlWithoutBase)
2889 )
2890}
2891
2892/// Policy for [relative URLs], that is, URLs that do not specify the scheme in full.
2893///
2894/// This policy kicks in, if set, for any attribute named `src` or `href`,
2895/// as well as the `data` attribute of an `object` tag.
2896///
2897/// [relative URLs]: struct.Builder.html#method.url_relative
2898///
2899/// # Examples
2900///
2901/// ## `Deny`
2902///
2903/// * `<a href="test">` is a file-relative URL, and will be removed
2904/// * `<a href="/test">` is a domain-relative URL, and will be removed
2905/// * `<a href="//example.com/test">` is a scheme-relative URL, and will be removed
2906/// * `<a href="http://example.com/test">` is an absolute URL, and will be kept
2907///
2908/// ## `PassThrough`
2909///
2910/// No changes will be made to any URLs, except if a disallowed scheme is used.
2911///
2912/// ## `RewriteWithBase`
2913///
2914/// If the base is set to `http://notriddle.com/some-directory/some-file`
2915///
2916/// * `<a href="test">` will be rewritten to `<a href="http://notriddle.com/some-directory/test">`
2917/// * `<a href="/test">` will be rewritten to `<a href="http://notriddle.com/test">`
2918/// * `<a href="//example.com/test">` will be rewritten to `<a href="http://example.com/test">`
2919/// * `<a href="http://example.com/test">` is an absolute URL, so it will be kept as-is
2920///
2921/// ## `Custom`
2922///
2923/// Pass the relative URL to a function.
2924/// If it returns `Some(string)`, then that one gets used.
2925/// Otherwise, it will remove the attribute (like `Deny` does).
2926///
2927/// use std::borrow::Cow;
2928/// fn is_absolute_path(url: &str) -> bool {
2929/// let u = url.as_bytes();
2930/// // `//a/b/c` is "protocol-relative", meaning "a" is a hostname
2931/// // `/a/b/c` is an absolute path, and what we want to do stuff to.
2932/// u.get(0) == Some(&b'/') && u.get(1) != Some(&b'/')
2933/// }
2934/// fn evaluate(url: &str) -> Option<Cow<str>> {
2935/// if is_absolute_path(url) {
2936/// Some(Cow::Owned(String::from("/root") + url))
2937/// } else {
2938/// Some(Cow::Borrowed(url))
2939/// }
2940/// }
2941/// fn main() {
2942/// let a = ammonia::Builder::new()
2943/// .url_relative(ammonia::UrlRelative::Custom(Box::new(evaluate)))
2944/// .clean("<a href=/test/path>fixed</a><a href=path>passed</a><a href=http://google.com/>skipped</a>")
2945/// .to_string();
2946/// 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>");
2947/// }
2948///
2949/// This function is only applied to relative URLs.
2950/// To filter all of the URLs,
2951/// use the not-yet-implemented Content Security Policy.
2952#[non_exhaustive]
2953pub enum UrlRelative<'a> {
2954 /// Relative URLs will be completely stripped from the document.
2955 Deny,
2956 /// Relative URLs will be passed through unchanged.
2957 PassThrough,
2958 /// Relative URLs will be changed into absolute URLs, based on this base URL.
2959 RewriteWithBase(Url),
2960 /// Force absolute and relative paths into a particular directory.
2961 ///
2962 /// Since the resolver does not affect fully-qualified URLs, it doesn't
2963 /// prevent users from linking wherever they want. This feature only
2964 /// serves to make content more portable.
2965 ///
2966 /// # Examples
2967 ///
2968 /// <table>
2969 /// <thead>
2970 /// <tr>
2971 /// <th>root</th>
2972 /// <th>path</th>
2973 /// <th>url</th>
2974 /// <th>result</th>
2975 /// </tr>
2976 /// </thead>
2977 /// <tbody>
2978 /// <tr>
2979 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2980 /// <td>README.md</td>
2981 /// <td></td>
2982 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/README.md</td>
2983 /// </tr><tr>
2984 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2985 /// <td>README.md</td>
2986 /// <td>/</td>
2987 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2988 /// </tr><tr>
2989 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2990 /// <td>README.md</td>
2991 /// <td>/CONTRIBUTING.md</td>
2992 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md</td>
2993 /// </tr><tr>
2994 /// <td>https://github.com/rust-ammonia/ammonia/blob/master</td>
2995 /// <td>README.md</td>
2996 /// <td></td>
2997 /// <td>https://github.com/rust-ammonia/ammonia/blob/README.md</td>
2998 /// </tr><tr>
2999 /// <td>https://github.com/rust-ammonia/ammonia/blob/master</td>
3000 /// <td>README.md</td>
3001 /// <td>/</td>
3002 /// <td>https://github.com/rust-ammonia/ammonia/blob/</td>
3003 /// </tr><tr>
3004 /// <td>https://github.com/rust-ammonia/ammonia/blob/master</td>
3005 /// <td>README.md</td>
3006 /// <td>/CONTRIBUTING.md</td>
3007 /// <td>https://github.com/rust-ammonia/ammonia/blob/CONTRIBUTING.md</td>
3008 /// </tr><tr>
3009 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
3010 /// <td></td>
3011 /// <td></td>
3012 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
3013 /// </tr><tr>
3014 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
3015 /// <td></td>
3016 /// <td>/</td>
3017 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
3018 /// </tr><tr>
3019 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
3020 /// <td></td>
3021 /// <td>/CONTRIBUTING.md</td>
3022 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md</td>
3023 /// </tr><tr>
3024 /// <td>https://github.com/</td>
3025 /// <td>rust-ammonia/ammonia/blob/master/README.md</td>
3026 /// <td></td>
3027 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/README.md</td>
3028 /// </tr><tr>
3029 /// <td>https://github.com/</td>
3030 /// <td>rust-ammonia/ammonia/blob/master/README.md</td>
3031 /// <td>/</td>
3032 /// <td>https://github.com/</td>
3033 /// </tr><tr>
3034 /// <td>https://github.com/</td>
3035 /// <td>rust-ammonia/ammonia/blob/master/README.md</td>
3036 /// <td>CONTRIBUTING.md</td>
3037 /// <td>https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md</td>
3038 /// </tr><tr>
3039 /// <td>https://github.com/</td>
3040 /// <td>rust-ammonia/ammonia/blob/master/README.md</td>
3041 /// <td>/CONTRIBUTING.md</td>
3042 /// <td>https://github.com/CONTRIBUTING.md</td>
3043 /// </tr>
3044 /// </tbody>
3045 /// </table>
3046 RewriteWithRoot {
3047 /// The URL that is treated as the root by the resolver.
3048 root: Url,
3049 /// The "current path" used to resolve relative paths.
3050 path: String,
3051 },
3052 /// Rewrite URLs with a custom function.
3053 Custom(Box<dyn UrlRelativeEvaluate<'a>>),
3054}
3055
3056impl<'a> UrlRelative<'a> {
3057 fn evaluate(&self, url: &str) -> Option<html5ever::tendril::StrTendril> {
3058 match self {
3059 UrlRelative::RewriteWithBase(ref url_base) => url_base
3060 .join(url)
3061 .ok()
3062 .and_then(|x| StrTendril::from_str(x.as_str()).ok()),
3063 UrlRelative::RewriteWithRoot { ref root, ref path } => {
3064 (match url.as_bytes() {
3065 // Scheme-relative URL
3066 [b'/', b'/', ..] => root.join(url),
3067 // Path-absolute URL
3068 b"/" => root.join("."),
3069 [b'/', ..] => root.join(&url[1..]),
3070 // Path-relative URL
3071 _ => root.join(path).and_then(|r| r.join(url)),
3072 })
3073 .ok()
3074 .and_then(|x| StrTendril::from_str(x.as_str()).ok())
3075 }
3076 UrlRelative::Custom(ref evaluate) => evaluate
3077 .evaluate(url)
3078 .as_ref()
3079 .map(Cow::as_ref)
3080 .map(StrTendril::from_str)
3081 .and_then(Result::ok),
3082 UrlRelative::PassThrough => StrTendril::from_str(url).ok(),
3083 UrlRelative::Deny => None,
3084 }
3085 }
3086}
3087
3088impl<'a> fmt::Debug for UrlRelative<'a> {
3089 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3090 match *self {
3091 UrlRelative::Deny => write!(f, "UrlRelative::Deny"),
3092 UrlRelative::PassThrough => write!(f, "UrlRelative::PassThrough"),
3093 UrlRelative::RewriteWithBase(ref base) => {
3094 write!(f, "UrlRelative::RewriteWithBase({})", base)
3095 }
3096 UrlRelative::RewriteWithRoot { ref root, ref path } => {
3097 write!(
3098 f,
3099 "UrlRelative::RewriteWithRoot {{ root: {root}, path: {path} }}"
3100 )
3101 }
3102 UrlRelative::Custom(_) => write!(f, "UrlRelative::Custom"),
3103 }
3104 }
3105}
3106
3107/// Types that implement this trait can be used to convert a relative URL into an absolute URL.
3108///
3109/// This evaluator is only called when the URL is relative; absolute URLs are not evaluated.
3110///
3111/// See [`url_relative`][url_relative] for more details.
3112///
3113/// [url_relative]: struct.Builder.html#method.url_relative
3114pub trait UrlRelativeEvaluate<'a>: Send + Sync + 'a {
3115 /// Return `None` to remove the attribute. Return `Some(str)` to replace it with a new string.
3116 fn evaluate<'url>(&self, _: &'url str) -> Option<Cow<'url, str>>;
3117}
3118impl<'a, T> UrlRelativeEvaluate<'a> for T
3119where
3120 T: Fn(&str) -> Option<Cow<'_, str>> + Send + Sync + 'a,
3121{
3122 fn evaluate<'url>(&self, url: &'url str) -> Option<Cow<'url, str>> {
3123 self(url)
3124 }
3125}
3126
3127impl fmt::Debug for dyn AttributeFilter {
3128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3129 f.write_str("AttributeFilter")
3130 }
3131}
3132
3133/// Types that implement this trait can be used to remove or rewrite arbitrary attributes.
3134///
3135/// See [`attribute_filter`][attribute_filter] for more details.
3136///
3137/// [attribute_filter]: struct.Builder.html#method.attribute_filter
3138pub trait AttributeFilter: Send + Sync {
3139 /// Return `None` to remove the attribute. Return `Some(str)` to replace it with a new string.
3140 fn filter<'a>(&self, _: &str, _: &str, _: &'a str) -> Option<Cow<'a, str>>;
3141}
3142
3143impl<T> AttributeFilter for T
3144where
3145 T: for<'a> Fn(&str, &str, &'a str) -> Option<Cow<'a, str>> + Send + Sync + 'static,
3146{
3147 fn filter<'a>(&self, element: &str, attribute: &str, value: &'a str) -> Option<Cow<'a, str>> {
3148 self(element, attribute, value)
3149 }
3150}
3151
3152/// A sanitized HTML document.
3153///
3154/// The `Document` type is an opaque struct representing an HTML fragment that was sanitized by
3155/// `ammonia`. It can be converted to a [`String`] or written to a [`Write`] instance. This allows
3156/// users to avoid buffering the serialized representation to a [`String`] when desired.
3157///
3158/// This type is opaque to insulate the caller from breaking changes in the `html5ever` interface.
3159///
3160/// Note that this type wraps an `html5ever` DOM tree. `ammonia` does not support streaming, so
3161/// the complete fragment needs to be stored in memory during processing.
3162///
3163/// [`String`]: https://doc.rust-lang.org/nightly/std/string/struct.String.html
3164/// [`Write`]: https://doc.rust-lang.org/nightly/std/io/trait.Write.html
3165///
3166/// # Examples
3167///
3168/// use ammonia::Builder;
3169///
3170/// let input = "<!-- comments will be stripped -->This is an Ammonia example.";
3171/// let output = "This is an Ammonia example.";
3172///
3173/// let document = Builder::new()
3174/// .clean(input);
3175/// assert_eq!(document.to_string(), output);
3176pub struct Document(RcDom);
3177
3178impl Document {
3179 /// Serializes a `Document` instance to a writer.
3180 ///
3181 /// This method writes the sanitized HTML to a [`Write`] instance, avoiding a buffering step.
3182 ///
3183 /// To avoid consuming the writer, a mutable reference can be passed, like in the example below.
3184 ///
3185 /// Note that the in-memory representation of `Document` is larger than the serialized
3186 /// `String`.
3187 ///
3188 /// [`Write`]: https://doc.rust-lang.org/nightly/std/io/trait.Write.html
3189 ///
3190 /// # Examples
3191 ///
3192 /// use ammonia::Builder;
3193 ///
3194 /// let input = "Some <style></style>HTML here";
3195 /// let expected = b"Some HTML here";
3196 ///
3197 /// let document = Builder::new()
3198 /// .clean(input);
3199 ///
3200 /// let mut sanitized = Vec::new();
3201 /// document.write_to(&mut sanitized)
3202 /// .expect("Writing to a string should not fail (except on OOM)");
3203 /// assert_eq!(sanitized, expected);
3204 pub fn write_to<W>(&self, writer: W) -> io::Result<()>
3205 where
3206 W: io::Write,
3207 {
3208 let opts = Self::serialize_opts();
3209 let inner: SerializableHandle = self.0.document.children.borrow()[0].clone().into();
3210 serialize(writer, &inner, opts)
3211 }
3212
3213 /// Exposes the `Document` instance as an [`rcdom::Handle`].
3214 ///
3215 /// This method returns the inner object backing the `Document` instance. This allows
3216 /// making further changes to the DOM without introducing redundant serialization and
3217 /// parsing.
3218 ///
3219 /// Note that this method should be considered unstable and sits outside of the semver
3220 /// stability guarantees. It may change, break, or go away at any time, either because
3221 /// of `html5ever` changes or `ammonia` implementation changes.
3222 ///
3223 /// For this method to be accessible, a `cfg` flag is required. The easiest way is to
3224 /// use the `RUSTFLAGS` environment variable:
3225 ///
3226 /// ```text
3227 /// RUSTFLAGS='--cfg ammonia_unstable' cargo build
3228 /// ```
3229 ///
3230 /// on Unix-like platforms, or
3231 ///
3232 /// ```text
3233 /// set RUSTFLAGS=--cfg ammonia_unstable
3234 /// cargo build
3235 /// ```
3236 ///
3237 /// on Windows.
3238 ///
3239 /// This requirement also applies to crates that transitively depend on crates that use
3240 /// this flag.
3241 ///
3242 /// # Examples
3243 ///
3244 /// use ammonia::Builder;
3245 /// use ammonia::rcdom::SerializableHandle;
3246 /// use maplit::hashset;
3247 /// use html5ever::serialize::{serialize, SerializeOpts};
3248 ///
3249 /// # use std::error::Error;
3250 /// # fn do_main() -> Result<(), Box<dyn Error>> {
3251 /// let input = "<a>one link</a> and <a>one more</a>";
3252 /// let expected = "<a>one more</a> and <a>one link</a>";
3253 ///
3254 /// let document = Builder::new()
3255 /// .link_rel(None)
3256 /// .clean(input);
3257 ///
3258 /// let node = document.to_dom_node();
3259 /// node.children.borrow_mut().reverse();
3260 ///
3261 /// let mut buf = Vec::new();
3262 /// let handle: SerializableHandle = node.into();
3263 /// serialize(&mut buf, &handle, SerializeOpts::default())?;
3264 /// let output = String::from_utf8(buf)?;
3265 ///
3266 /// assert_eq!(output, expected);
3267 /// # Ok(())
3268 /// # }
3269 /// # fn main() { do_main().unwrap() }
3270 #[cfg(ammonia_unstable)]
3271 pub fn to_dom_node(&self) -> Handle {
3272 self.0.document.children.borrow()[0].clone()
3273 }
3274
3275 fn serialize_opts() -> SerializeOpts {
3276 SerializeOpts::default()
3277 }
3278}
3279
3280impl Clone for Document {
3281 fn clone(&self) -> Self {
3282 let parser = Builder::make_parser();
3283 let dom = parser.one(&self.to_string()[..]);
3284 Document(dom)
3285 }
3286}
3287
3288/// Convert a `Document` to stringified HTML.
3289///
3290/// Since [`Document`] implements [`Display`], it can be converted to a [`String`] using the
3291/// standard [`ToString::to_string`] method. This is the simplest way to use `ammonia`.
3292///
3293/// [`Document`]: ammonia::Document
3294/// [`Display`]: std::fmt::Display
3295/// [`ToString::to_string`]: std::string::ToString
3296///
3297/// # Examples
3298///
3299/// use ammonia::Builder;
3300///
3301/// let input = "Some <style></style>HTML here";
3302/// let output = "Some HTML here";
3303///
3304/// let document = Builder::new()
3305/// .clean(input);
3306/// assert_eq!(document.to_string(), output);
3307impl Display for Document {
3308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3309 let opts = Self::serialize_opts();
3310 let mut ret_val = Vec::new();
3311 let inner: SerializableHandle = self.0.document.children.borrow()[0].clone().into();
3312 serialize(&mut ret_val, &inner, opts)
3313 .expect("Writing to a string shouldn't fail (expect on OOM)");
3314 String::from_utf8(ret_val)
3315 .expect("html5ever only supports UTF8")
3316 .fmt(f)
3317 }
3318}
3319
3320impl fmt::Debug for Document {
3321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3322 write!(f, "Document({})", self)
3323 }
3324}
3325
3326impl From<Document> for String {
3327 fn from(document: Document) -> Self {
3328 document.to_string()
3329 }
3330}
3331
3332#[cfg(test)]
3333mod test {
3334 use super::*;
3335 #[test]
3336 fn deeply_nested_whitelisted_does_not_cause_stack_overflow() {
3337 clean(&"<b>".repeat(60_000));
3338 }
3339 #[test]
3340 fn deeply_nested_blacklisted_does_not_cause_stack_overflow() {
3341 clean(&"<b-b>".repeat(60_000));
3342 }
3343 #[test]
3344 fn deeply_nested_alternating_does_not_cause_stack_overflow() {
3345 clean(&"<b-b>".repeat(35_000));
3346 }
3347 #[test]
3348 fn document_level_tags_cannot_be_whitelisted() {
3349 // Adding `html`, `head`, or `body` to the allowed tags has no effect
3350 // because the parser runs in fragment mode and strips them before
3351 // the sanitizer sees the tree. This test pins that documented
3352 // behavior; if it ever changes, the docs on `Builder::tags` need to
3353 // change too.
3354 let fragment =
3355 "<html><head>head content</head><body><div>test</div></body></html>";
3356 let result = Builder::default()
3357 .add_tags(["html", "head", "body"])
3358 .clean(fragment)
3359 .to_string();
3360 assert_eq!(result, "head content<div>test</div>");
3361 }
3362 #[test]
3363 fn included_angles() {
3364 let fragment = "1 < 2";
3365 let result = clean(fragment);
3366 assert_eq!(result, "1 < 2");
3367 }
3368 #[test]
3369 fn remove_script() {
3370 let fragment = "an <script>evil()</script> example";
3371 let result = clean(fragment);
3372 assert_eq!(result, "an example");
3373 }
3374 #[test]
3375 fn ignore_link() {
3376 let fragment = "a <a href=\"http://www.google.com\">good</a> example";
3377 let expected = "a <a href=\"http://www.google.com\" rel=\"noopener noreferrer\">\
3378 good</a> example";
3379 let result = clean(fragment);
3380 assert_eq!(result, expected);
3381 }
3382 #[test]
3383 fn remove_unsafe_link() {
3384 let fragment = "an <a onclick=\"evil()\" href=\"http://www.google.com\">evil</a> example";
3385 let result = clean(fragment);
3386 assert_eq!(
3387 result,
3388 "an <a href=\"http://www.google.com\" rel=\"noopener noreferrer\">evil</a> example"
3389 );
3390 }
3391 #[test]
3392 fn remove_js_link() {
3393 let fragment = "an <a href=\"javascript:evil()\">evil</a> example";
3394 let result = clean(fragment);
3395 assert_eq!(result, "an <a rel=\"noopener noreferrer\">evil</a> example");
3396 }
3397 #[test]
3398 fn tag_rebalance() {
3399 let fragment = "<b>AWESOME!";
3400 let result = clean(fragment);
3401 assert_eq!(result, "<b>AWESOME!</b>");
3402 }
3403 #[test]
3404 fn allow_url_relative() {
3405 let fragment = "<a href=test>Test</a>";
3406 let result = Builder::new()
3407 .url_relative(UrlRelative::PassThrough)
3408 .clean(fragment)
3409 .to_string();
3410 assert_eq!(
3411 result,
3412 "<a href=\"test\" rel=\"noopener noreferrer\">Test</a>"
3413 );
3414 }
3415 #[test]
3416 fn rewrite_url_relative() {
3417 let fragment = "<a href=test>Test</a>";
3418 let result = Builder::new()
3419 .url_relative(UrlRelative::RewriteWithBase(
3420 Url::parse("http://example.com/").unwrap(),
3421 ))
3422 .clean(fragment)
3423 .to_string();
3424 assert_eq!(
3425 result,
3426 "<a href=\"http://example.com/test\" rel=\"noopener noreferrer\">Test</a>"
3427 );
3428 }
3429 #[test]
3430 fn rewrite_url_relative_with_invalid_url() {
3431 // Reduced from https://github.com/Bauke/ammonia-crash-test
3432 let fragment = r##"<a href="\\"https://example.com\\"">test</a>"##;
3433 let result = Builder::new()
3434 .url_relative(UrlRelative::RewriteWithBase(
3435 Url::parse("http://example.com/").unwrap(),
3436 ))
3437 .clean(fragment)
3438 .to_string();
3439 assert_eq!(result, r##"<a rel="noopener noreferrer">test</a>"##);
3440 }
3441 #[test]
3442 fn attribute_filter_nop() {
3443 let fragment = "<a href=test>Test</a>";
3444 let result = Builder::new()
3445 .attribute_filter(|elem, attr, value| {
3446 assert_eq!("a", elem);
3447 assert!(
3448 matches!(
3449 (attr, value),
3450 ("href", "test") | ("rel", "noopener noreferrer")
3451 ),
3452 "{}",
3453 value.to_string()
3454 );
3455 Some(value.into())
3456 })
3457 .clean(fragment)
3458 .to_string();
3459 assert_eq!(
3460 result,
3461 "<a href=\"test\" rel=\"noopener noreferrer\">Test</a>"
3462 );
3463 }
3464
3465 #[test]
3466 fn attribute_filter_drop() {
3467 let fragment = "Test<img alt=test src=imgtest>";
3468 let result = Builder::new()
3469 .attribute_filter(|elem, attr, value| {
3470 assert_eq!("img", elem);
3471 match (attr, value) {
3472 ("src", "imgtest") => None,
3473 ("alt", "test") => Some(value.into()),
3474 _ => panic!("unexpected"),
3475 }
3476 })
3477 .clean(fragment)
3478 .to_string();
3479 assert_eq!(result, r#"Test<img alt="test">"#);
3480 }
3481
3482 #[test]
3483 fn url_filter_absolute() {
3484 let fragment = "Test<img alt=test src=imgtest>";
3485 let result = Builder::new()
3486 .attribute_filter(|elem, attr, value| {
3487 assert_eq!("img", elem);
3488 match (attr, value) {
3489 ("src", "imgtest") => {
3490 Some(format!("https://example.com/images/{}", value).into())
3491 }
3492 ("alt", "test") => None,
3493 _ => panic!("unexpected"),
3494 }
3495 })
3496 .url_relative(UrlRelative::RewriteWithBase(
3497 Url::parse("http://wrong.invalid/").unwrap(),
3498 ))
3499 .clean(fragment)
3500 .to_string();
3501 assert_eq!(
3502 result,
3503 r#"Test<img src="https://example.com/images/imgtest">"#
3504 );
3505 }
3506
3507 #[test]
3508 fn url_filter_relative() {
3509 let fragment = "Test<img alt=test src=imgtest>";
3510 let result = Builder::new()
3511 .attribute_filter(|elem, attr, value| {
3512 assert_eq!("img", elem);
3513 match (attr, value) {
3514 ("src", "imgtest") => Some("rewrite".into()),
3515 ("alt", "test") => Some("altalt".into()),
3516 _ => panic!("unexpected"),
3517 }
3518 })
3519 .url_relative(UrlRelative::RewriteWithBase(
3520 Url::parse("https://example.com/base/#").unwrap(),
3521 ))
3522 .clean(fragment)
3523 .to_string();
3524 assert_eq!(
3525 result,
3526 r#"Test<img alt="altalt" src="https://example.com/base/rewrite">"#
3527 );
3528 }
3529
3530 #[test]
3531 fn rewrite_url_relative_no_rel() {
3532 let fragment = "<a href=test>Test</a>";
3533 let result = Builder::new()
3534 .url_relative(UrlRelative::RewriteWithBase(
3535 Url::parse("http://example.com/").unwrap(),
3536 ))
3537 .link_rel(None)
3538 .clean(fragment)
3539 .to_string();
3540 assert_eq!(result, "<a href=\"http://example.com/test\">Test</a>");
3541 }
3542 #[test]
3543 fn deny_url_relative() {
3544 let fragment = "<a href=test>Test</a>";
3545 let result = Builder::new()
3546 .url_relative(UrlRelative::Deny)
3547 .clean(fragment)
3548 .to_string();
3549 assert_eq!(result, "<a rel=\"noopener noreferrer\">Test</a>");
3550 }
3551 #[test]
3552 fn replace_rel() {
3553 let fragment = "<a href=test rel=\"garbage\">Test</a>";
3554 let result = Builder::new()
3555 .url_relative(UrlRelative::PassThrough)
3556 .clean(fragment)
3557 .to_string();
3558 assert_eq!(
3559 result,
3560 "<a href=\"test\" rel=\"noopener noreferrer\">Test</a>"
3561 );
3562 }
3563 #[test]
3564 fn consider_rel_still_banned() {
3565 let fragment = "<a href=test rel=\"garbage\">Test</a>";
3566 let result = Builder::new()
3567 .url_relative(UrlRelative::PassThrough)
3568 .link_rel(None)
3569 .clean(fragment)
3570 .to_string();
3571 assert_eq!(result, "<a href=\"test\">Test</a>");
3572 }
3573 #[test]
3574 fn object_data() {
3575 let fragment = "<span data=\"javascript:evil()\">Test</span>\
3576 <object data=\"javascript:evil()\"></object>M";
3577 let expected = r#"<span data="javascript:evil()">Test</span><object></object>M"#;
3578 let result = Builder::new()
3579 .tags(hashset!["span", "object"])
3580 .generic_attributes(hashset!["data"])
3581 .clean(fragment)
3582 .to_string();
3583 assert_eq!(result, expected);
3584 }
3585 #[test]
3586 fn remove_attributes() {
3587 let fragment = "<table border=\"1\"><tr></tr></table>";
3588 let result = Builder::new().clean(fragment);
3589 assert_eq!(
3590 result.to_string(),
3591 "<table><tbody><tr></tr></tbody></table>"
3592 );
3593 }
3594 #[test]
3595 fn quotes_in_attrs() {
3596 let fragment = "<b title='\"'>contents</b>";
3597 let result = clean(fragment);
3598 assert_eq!(result, "<b title=\""\">contents</b>");
3599 }
3600 #[test]
3601 #[should_panic]
3602 fn panic_if_rel_is_allowed_and_replaced_generic() {
3603 Builder::new()
3604 .link_rel(Some("noopener noreferrer"))
3605 .generic_attributes(hashset!["rel"])
3606 .clean("something");
3607 }
3608 #[test]
3609 #[should_panic]
3610 fn panic_if_rel_is_allowed_and_replaced_a() {
3611 Builder::new()
3612 .link_rel(Some("noopener noreferrer"))
3613 .tag_attributes(hashmap![
3614 "a" => hashset!["rel"],
3615 ])
3616 .clean("something");
3617 }
3618 #[test]
3619 fn no_panic_if_rel_is_allowed_and_replaced_span() {
3620 Builder::new()
3621 .link_rel(Some("noopener noreferrer"))
3622 .tag_attributes(hashmap![
3623 "span" => hashset!["rel"],
3624 ])
3625 .clean("<span rel=\"what\">s</span>");
3626 }
3627 #[test]
3628 fn no_panic_if_rel_is_allowed_and_not_replaced_generic() {
3629 Builder::new()
3630 .link_rel(None)
3631 .generic_attributes(hashset!["rel"])
3632 .clean("<a rel=\"what\">s</a>");
3633 }
3634 #[test]
3635 fn no_panic_if_rel_is_allowed_and_not_replaced_a() {
3636 Builder::new()
3637 .link_rel(None)
3638 .tag_attributes(hashmap![
3639 "a" => hashset!["rel"],
3640 ])
3641 .clean("<a rel=\"what\">s</a>");
3642 }
3643 #[test]
3644 fn dont_close_void_elements() {
3645 let fragment = "<br>";
3646 let result = clean(fragment);
3647 assert_eq!(result.to_string(), "<br>");
3648 }
3649 #[should_panic]
3650 #[test]
3651 fn panic_on_allowed_classes_tag_attributes() {
3652 let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3653 Builder::new()
3654 .link_rel(None)
3655 .tag_attributes(hashmap![
3656 "p" => hashset!["class"],
3657 "a" => hashset!["class"],
3658 ])
3659 .allowed_classes(hashmap![
3660 "p" => hashset!["foo", "bar"],
3661 "a" => hashset!["baz"],
3662 ])
3663 .clean(fragment);
3664 }
3665 #[should_panic]
3666 #[test]
3667 fn panic_on_allowed_classes_generic_attributes() {
3668 let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3669 Builder::new()
3670 .link_rel(None)
3671 .generic_attributes(hashset!["class", "href", "some-foo"])
3672 .allowed_classes(hashmap![
3673 "p" => hashset!["foo", "bar"],
3674 "a" => hashset!["baz"],
3675 ])
3676 .clean(fragment);
3677 }
3678 #[test]
3679 fn remove_non_allowed_classes() {
3680 let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3681 let result = Builder::new()
3682 .link_rel(None)
3683 .allowed_classes(hashmap![
3684 "p" => hashset!["foo", "bar"],
3685 "a" => hashset!["baz"],
3686 ])
3687 .clean(fragment);
3688 assert_eq!(
3689 result.to_string(),
3690 "<p class=\"foo bar\"><a class=\"baz\">Hey</a></p>"
3691 );
3692 }
3693 #[test]
3694 fn remove_non_allowed_classes_with_tag_class() {
3695 let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3696 let result = Builder::new()
3697 .link_rel(None)
3698 .tag_attributes(hashmap![
3699 "div" => hashset!["class"],
3700 ])
3701 .allowed_classes(hashmap![
3702 "p" => hashset!["foo", "bar"],
3703 "a" => hashset!["baz"],
3704 ])
3705 .clean(fragment);
3706 assert_eq!(
3707 result.to_string(),
3708 "<p class=\"foo bar\"><a class=\"baz\">Hey</a></p>"
3709 );
3710 }
3711 #[test]
3712 fn allowed_classes_ascii_whitespace() {
3713 // According to https://infra.spec.whatwg.org/#ascii-whitespace,
3714 // TAB (\t), LF (\n), FF (\x0C), CR (\x0D) and SPACE (\x20) are
3715 // considered to be ASCII whitespace. Unicode whitespace characters
3716 // and VT (\x0B) aren't ASCII whitespace.
3717 let fragment = "<p class=\"a\tb\nc\x0Cd\re f\x0B g\u{2000}\">";
3718 let result = Builder::new()
3719 .allowed_classes(hashmap![
3720 "p" => hashset!["a", "b", "c", "d", "e", "f", "g"],
3721 ])
3722 .clean(fragment);
3723 assert_eq!(result.to_string(), r#"<p class="a b c d e"></p>"#);
3724 }
3725 #[test]
3726 fn remove_non_allowed_attributes_with_tag_attribute_values() {
3727 let fragment = "<p data-label=\"baz\" name=\"foo\"></p>";
3728 let result = Builder::new()
3729 .tag_attribute_values(hashmap![
3730 "p" => hashmap![
3731 "data-label" => hashset!["bar"],
3732 ],
3733 ])
3734 .tag_attributes(hashmap![
3735 "p" => hashset!["name"],
3736 ])
3737 .clean(fragment);
3738 assert_eq!(result.to_string(), "<p name=\"foo\"></p>",);
3739 }
3740 #[test]
3741 fn keep_allowed_attributes_with_tag_attribute_values() {
3742 let fragment = "<p data-label=\"bar\" name=\"foo\"></p>";
3743 let result = Builder::new()
3744 .tag_attribute_values(hashmap![
3745 "p" => hashmap![
3746 "data-label" => hashset!["bar"],
3747 ],
3748 ])
3749 .tag_attributes(hashmap![
3750 "p" => hashset!["name"],
3751 ])
3752 .clean(fragment);
3753 assert_eq!(
3754 result.to_string(),
3755 "<p data-label=\"bar\" name=\"foo\"></p>",
3756 );
3757 }
3758 #[test]
3759 fn tag_attribute_values_case_insensitive() {
3760 let fragment = "<input type=\"CHECKBOX\" name=\"foo\">";
3761 let result = Builder::new()
3762 .tags(hashset!["input"])
3763 .tag_attribute_values(hashmap![
3764 "input" => hashmap![
3765 "type" => hashset!["checkbox"],
3766 ],
3767 ])
3768 .tag_attributes(hashmap![
3769 "input" => hashset!["name"],
3770 ])
3771 .clean(fragment);
3772 assert_eq!(result.to_string(), "<input type=\"CHECKBOX\" name=\"foo\">",);
3773 }
3774 #[test]
3775 fn set_tag_attribute_values() {
3776 let fragment = "<a href=\"https://example.com/\">Link</a>";
3777 let result = Builder::new()
3778 .link_rel(None)
3779 .add_tag_attributes("a", &["target"])
3780 .set_tag_attribute_value("a", "target", "_blank")
3781 .clean(fragment);
3782 assert_eq!(
3783 result.to_string(),
3784 "<a href=\"https://example.com/\" target=\"_blank\">Link</a>",
3785 );
3786 }
3787 #[test]
3788 fn update_existing_set_tag_attribute_values() {
3789 let fragment = "<a target=\"bad\" href=\"https://example.com/\">Link</a>";
3790 let result = Builder::new()
3791 .link_rel(None)
3792 .add_tag_attributes("a", &["target"])
3793 .set_tag_attribute_value("a", "target", "_blank")
3794 .clean(fragment);
3795 assert_eq!(
3796 result.to_string(),
3797 "<a target=\"_blank\" href=\"https://example.com/\">Link</a>",
3798 );
3799 }
3800 #[test]
3801 fn unwhitelisted_set_tag_attribute_values() {
3802 let fragment = "<span>hi</span><my-elem>";
3803 let result = Builder::new()
3804 .set_tag_attribute_value("my-elem", "my-attr", "val")
3805 .clean(fragment);
3806 assert_eq!(result.to_string(), "<span>hi</span>",);
3807 }
3808 #[test]
3809 fn remove_entity_link() {
3810 let fragment = "<a href=\"javascript:a\
3811 lert('XSS')\">Click me!</a>";
3812 let result = clean(fragment);
3813 assert_eq!(
3814 result.to_string(),
3815 "<a rel=\"noopener noreferrer\">Click me!</a>"
3816 );
3817 }
3818 #[test]
3819 fn remove_relative_url_evaluate() {
3820 fn is_absolute_path(url: &str) -> bool {
3821 let u = url.as_bytes();
3822 // `//a/b/c` is "protocol-relative", meaning "a" is a hostname
3823 // `/a/b/c` is an absolute path, and what we want to do stuff to.
3824 u.first() == Some(&b'/') && u.get(1) != Some(&b'/')
3825 }
3826 fn is_banned(url: &str) -> bool {
3827 let u = url.as_bytes();
3828 u.first() == Some(&b'b') && u.get(1) == Some(&b'a')
3829 }
3830 fn evaluate(url: &str) -> Option<Cow<'_, str>> {
3831 if is_absolute_path(url) {
3832 Some(Cow::Owned(String::from("/root") + url))
3833 } else if is_banned(url) {
3834 None
3835 } else {
3836 Some(Cow::Borrowed(url))
3837 }
3838 }
3839 let a = Builder::new()
3840 .url_relative(UrlRelative::Custom(Box::new(evaluate)))
3841 .clean("<a href=banned>banned</a><a href=/test/path>fixed</a><a href=path>passed</a><a href=http://google.com/>skipped</a>")
3842 .to_string();
3843 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>");
3844 }
3845 #[test]
3846 fn remove_relative_url_evaluate_b() {
3847 fn is_absolute_path(url: &str) -> bool {
3848 let u = url.as_bytes();
3849 // `//a/b/c` is "protocol-relative", meaning "a" is a hostname
3850 // `/a/b/c` is an absolute path, and what we want to do stuff to.
3851 u.first() == Some(&b'/') && u.get(1) != Some(&b'/')
3852 }
3853 fn is_banned(url: &str) -> bool {
3854 let u = url.as_bytes();
3855 u.first() == Some(&b'b') && u.get(1) == Some(&b'a')
3856 }
3857 fn evaluate(url: &str) -> Option<Cow<'_, str>> {
3858 if is_absolute_path(url) {
3859 Some(Cow::Owned(String::from("/root") + url))
3860 } else if is_banned(url) {
3861 None
3862 } else {
3863 Some(Cow::Borrowed(url))
3864 }
3865 }
3866 let a = Builder::new()
3867 .url_relative(UrlRelative::Custom(Box::new(evaluate)))
3868 .clean("<a href=banned>banned</a><a href=banned title=test>banned</a><a title=test href=banned>banned</a>")
3869 .to_string();
3870 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>");
3871 }
3872 #[test]
3873 fn remove_relative_url_evaluate_c() {
3874 // Don't run on absolute URLs.
3875 fn evaluate(_: &str) -> Option<Cow<'_, str>> {
3876 return Some(Cow::Owned(String::from("invalid")));
3877 }
3878 let a = Builder::new()
3879 .url_relative(UrlRelative::Custom(Box::new(evaluate)))
3880 .clean("<a href=\"https://www.google.com/\">google</a>")
3881 .to_string();
3882 assert_eq!(
3883 a,
3884 "<a href=\"https://www.google.com/\" rel=\"noopener noreferrer\">google</a>"
3885 );
3886 }
3887 #[test]
3888 fn clean_children_of_bad_element() {
3889 let fragment = "<bad><evil>a</evil>b</bad>";
3890 let result = Builder::new().clean(fragment);
3891 assert_eq!(result.to_string(), "ab");
3892 }
3893 #[test]
3894 fn reader_input() {
3895 let fragment = b"an <script>evil()</script> example";
3896 let result = Builder::new().clean_from_reader(&fragment[..]);
3897 assert!(result.is_ok());
3898 assert_eq!(result.unwrap().to_string(), "an example");
3899 }
3900 #[test]
3901 fn reader_non_utf8() {
3902 let fragment = b"non-utf8 \xF0\x90\x80string";
3903 let result = Builder::new().clean_from_reader(&fragment[..]);
3904 assert!(result.is_ok());
3905 assert_eq!(result.unwrap().to_string(), "non-utf8 \u{fffd}string");
3906 }
3907 #[test]
3908 fn display_impl() {
3909 let fragment = r#"a <a>link</a>"#;
3910 let result = Builder::new().link_rel(None).clean(fragment);
3911 assert_eq!(format!("{}", result), "a <a>link</a>");
3912 }
3913 #[test]
3914 fn debug_impl() {
3915 let fragment = r#"a <a>link</a>"#;
3916 let result = Builder::new().link_rel(None).clean(fragment);
3917 assert_eq!(format!("{:?}", result), "Document(a <a>link</a>)");
3918 }
3919 #[cfg(ammonia_unstable)]
3920 #[test]
3921 fn to_dom_node() {
3922 let fragment = r#"a <a>link</a>"#;
3923 let result = Builder::new().link_rel(None).clean(fragment);
3924 let _node = result.to_dom_node();
3925 }
3926 #[test]
3927 fn string_from_document() {
3928 let fragment = r#"a <a>link"#;
3929 let result = String::from(Builder::new().link_rel(None).clean(fragment));
3930 assert_eq!(format!("{}", result), "a <a>link</a>");
3931 }
3932 fn require_sync<T: Sync>(_: T) {}
3933 fn require_send<T: Send>(_: T) {}
3934 #[test]
3935 fn require_sync_and_send() {
3936 require_sync(Builder::new());
3937 require_send(Builder::new());
3938 }
3939 #[test]
3940 fn id_prefixed() {
3941 let fragment = "<a id=\"hello\"></a><b id=\"hello\"></a>";
3942 let result = String::from(
3943 Builder::new()
3944 .tag_attributes(hashmap![
3945 "a" => hashset!["id"],
3946 ])
3947 .id_prefix(Some("prefix-"))
3948 .clean(fragment),
3949 );
3950 assert_eq!(
3951 result.to_string(),
3952 "<a id=\"prefix-hello\" rel=\"noopener noreferrer\"></a><b></b>"
3953 );
3954 }
3955 #[test]
3956 fn id_already_prefixed() {
3957 let fragment = "<a id=\"prefix-hello\"></a>";
3958 let result = String::from(
3959 Builder::new()
3960 .tag_attributes(hashmap![
3961 "a" => hashset!["id"],
3962 ])
3963 .id_prefix(Some("prefix-"))
3964 .clean(fragment),
3965 );
3966 assert_eq!(
3967 result.to_string(),
3968 "<a id=\"prefix-hello\" rel=\"noopener noreferrer\"></a>"
3969 );
3970 }
3971 #[test]
3972 fn clean_content_tags() {
3973 let fragment = "<script type=\"text/javascript\"><a>Hello!</a></script>";
3974 let result = String::from(
3975 Builder::new()
3976 .clean_content_tags(hashset!["script"])
3977 .clean(fragment),
3978 );
3979 assert_eq!(result.to_string(), "");
3980 }
3981 #[test]
3982 fn only_clean_content_tags() {
3983 let fragment = "<em>This is</em><script><a>Hello!</a></script><p>still here!</p>";
3984 let result = String::from(
3985 Builder::new()
3986 .clean_content_tags(hashset!["script"])
3987 .clean(fragment),
3988 );
3989 assert_eq!(result.to_string(), "<em>This is</em><p>still here!</p>");
3990 }
3991 #[test]
3992 fn clean_removed_default_tag() {
3993 let fragment = "<em>This is</em><script><a>Hello!</a></script><p>still here!</p>";
3994 let result = String::from(
3995 Builder::new()
3996 .rm_tags(hashset!["a"])
3997 .rm_tag_attributes("a", hashset!["href", "hreflang"])
3998 .clean_content_tags(hashset!["script"])
3999 .clean(fragment),
4000 );
4001 assert_eq!(result.to_string(), "<em>This is</em><p>still here!</p>");
4002 }
4003 #[test]
4004 #[should_panic]
4005 fn panic_on_clean_content_tag_attribute() {
4006 Builder::new()
4007 .rm_tags(std::iter::once("a"))
4008 .clean_content_tags(hashset!["a"])
4009 .clean("");
4010 }
4011 #[test]
4012 #[should_panic]
4013 fn panic_on_clean_content_tag() {
4014 Builder::new().clean_content_tags(hashset!["a"]).clean("");
4015 }
4016
4017 #[test]
4018 fn clean_text_test() {
4019 assert_eq!(
4020 clean_text("<this> is <a test function"),
4021 "<this> is <a test function"
4022 );
4023 }
4024
4025 #[test]
4026 fn clean_text_spaces_test() {
4027 assert_eq!(clean_text("\x09\x0a\x0c\x20"), "	  ");
4028 }
4029
4030 #[test]
4031 fn ns_svg() {
4032 // https://github.com/cure53/DOMPurify/pull/495
4033 let fragment = r##"<svg><iframe><a title="</iframe><img src onerror=alert(1)>">test"##;
4034 let result = String::from(Builder::new().add_tags(&["iframe"]).clean(fragment));
4035 assert_eq!(result.to_string(), "");
4036
4037 let fragment = "<svg><iframe>remove me</iframe></svg><iframe>keep me</iframe>";
4038 let result = String::from(Builder::new().add_tags(&["iframe"]).clean(fragment));
4039 assert_eq!(result.to_string(), "<iframe>keep me</iframe>");
4040
4041 let fragment = "<svg><a>remove me</a></svg><iframe>keep me</iframe>";
4042 let result = String::from(Builder::new().add_tags(&["iframe"]).clean(fragment));
4043 assert_eq!(result.to_string(), "<iframe>keep me</iframe>");
4044
4045 let fragment = "<svg><a>keep me</a></svg><iframe>keep me</iframe>";
4046 let result = String::from(Builder::new().add_tags(&["iframe", "svg"]).clean(fragment));
4047 assert_eq!(
4048 result.to_string(),
4049 "<svg><a rel=\"noopener noreferrer\">keep me</a></svg><iframe>keep me</iframe>"
4050 );
4051 }
4052
4053 #[test]
4054 fn ns_svg_2() {
4055 let fragment = "<svg><foreignObject><table><path><xmp><!--</xmp><img title'--><img src=1 onerror=alert(1)>'>";
4056 let result = Builder::default()
4057 .strip_comments(false)
4058 .add_tags(&["svg","foreignObject","table","path","xmp"])
4059 .clean(fragment);
4060 assert_eq!(
4061 result.to_string(),
4062 "<svg><foreignObject><table></table></foreignObject></svg>"
4063 );
4064 }
4065
4066 #[test]
4067 fn ns_mathml() {
4068 // https://github.com/cure53/DOMPurify/pull/495
4069 let fragment = "<mglyph></mglyph>";
4070 let result = String::from(
4071 Builder::new()
4072 .add_tags(&["math", "mtext", "mglyph"])
4073 .clean(fragment),
4074 );
4075 assert_eq!(result.to_string(), "");
4076 let fragment = "<math><mtext><div><mglyph>";
4077 let result = String::from(
4078 Builder::new()
4079 .add_tags(&["math", "mtext", "mglyph"])
4080 .clean(fragment),
4081 );
4082 assert_eq!(
4083 result.to_string(),
4084 "<math><mtext><div></div></mtext></math>"
4085 );
4086 let fragment = "<math><mtext><mglyph>";
4087 let result = String::from(
4088 Builder::new()
4089 .add_tags(&["math", "mtext", "mglyph"])
4090 .clean(fragment),
4091 );
4092 assert_eq!(
4093 result.to_string(),
4094 "<math><mtext><mglyph></mglyph></mtext></math>"
4095 );
4096 }
4097
4098 #[test]
4099 fn ns_mathml_2() {
4100 let fragment = "<math><mtext><table><mglyph><xmp><!--</xmp><img title='--><img src=1 onerror=alert(1)>'>";
4101 let result = Builder::default()
4102 .strip_comments(false)
4103 .add_tags(&["math","mtext","table","mglyph","xmp"])
4104 .clean(fragment);
4105 assert_eq!(
4106 result.to_string(),
4107 "<math><mtext><table></table></mtext></math>"
4108 );
4109 }
4110
4111 #[test]
4112 fn ns_mathml_3() {
4113 // try without the attr
4114 let fragment = "<math><annotation-xml encoding='text/html'><xmp><!--</xmp><img title='--><img src=1 onerror=alert(1)>'>";
4115 let result = Builder::default()
4116 .strip_comments(false)
4117 .add_tags(&["math","annotation-xml","table","mglyph","xmp"])
4118 .clean(fragment);
4119 assert_eq!(
4120 result.to_string(),
4121 "<math><annotation-xml></annotation-xml></math>"
4122 );
4123 // now with the attr
4124 let fragment = "<math><annotation-xml encoding='text/html'><xmp><!--</xmp><img title='--><img src=1 onerror=alert(1)>'>";
4125 let result = Builder::default()
4126 .strip_comments(false)
4127 .add_tags(&["math","annotation-xml","table","mglyph","xmp"])
4128 .add_tag_attribute_values("annotation-xml", "encoding", ["text/html"])
4129 .clean(fragment);
4130 assert_eq!(
4131 result.to_string(),
4132 // yes, I tried it in Firefox, and the script didn't run
4133 r#"<math><annotation-xml encoding="text/html"><xmp><!--</xmp><img title="--><img src=1 onerror=alert(1)>"></annotation-xml></math>"#
4134 );
4135 // now with a tweaked attr
4136 let fragment = "<math><annotation-xml encoding='image/svg+xml'><xmp><!--</xmp><img title='--><img src=1 onerror=alert(1)>'>";
4137 let result = Builder::default()
4138 .strip_comments(false)
4139 .add_tags(&["math","annotation-xml","table","mglyph","xmp"])
4140 .add_tag_attribute_values("annotation-xml", "encoding", ["image/svg+xml"])
4141 .clean(fragment);
4142 assert_eq!(
4143 result.to_string(),
4144 r#"<math><annotation-xml encoding="image/svg+xml"></annotation-xml></math>"#
4145 );
4146 // now with actual SVG
4147 let fragment = "<math><annotation-xml encoding='image/svg+xml'><svg>";
4148 let result = Builder::default()
4149 .strip_comments(false)
4150 .add_tags(&["math","annotation-xml","svg"])
4151 .add_tag_attribute_values("annotation-xml", "encoding", ["image/svg+xml"])
4152 .clean(fragment);
4153 assert_eq!(
4154 result.to_string(),
4155 r#"<math><annotation-xml encoding="image/svg+xml"><svg></svg></annotation-xml></math>"#
4156 );
4157 }
4158
4159 #[test]
4160 fn ns_svg_animate_url_attr() {
4161 let fragment = r##"
4162 <svg>
4163 <a>
4164 <animate attributeName="xss" values="http://example.com"></animate>
4165 <animate attributeName="href" values="http://example.com"></animate>
4166 <animate attributeName="href" values="http://example.com;/test"></animate>
4167 <animate attributeName="href" values="http://example.com;/test;javascript:xss"></animate>
4168 <animate attributeName="href" from="http://example.com" to="http://example.com"></animate>
4169 <animate attributeName="href" from="javascript:xss" to="http://example.com"></animate>
4170 <animate attributeName="href" from="http://example.com" to="javascript:xss"></animate>
4171 <animate attributeName="href" from="http://example.com" to="./test2"></animate>
4172 <animate attributeName="href" from="./test2" to="http://example.com"></animate>
4173 </a>
4174 </svg>
4175 "##;
4176 let filtered = r##"
4177 <svg>
4178 <a rel="noopener noreferrer">
4179
4180 <animate attributeName="href" values="http://example.com"></animate>
4181 <animate attributeName="href" values="http://example.com;http://notriddle.com/test"></animate>
4182
4183 <animate attributeName="href" from="http://example.com" to="http://example.com"></animate>
4184
4185
4186 <animate attributeName="href" from="http://example.com" to="http://notriddle.com/test2"></animate>
4187 <animate attributeName="href" from="http://notriddle.com/test2" to="http://example.com"></animate>
4188 </a>
4189 </svg>
4190 "##;
4191 let result = Builder::default()
4192 .add_tags(&["svg","a","animate"])
4193 .add_tag_attributes("animate", ["attributeName","values","from","to"])
4194 .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4195 .clean(fragment);
4196 assert_eq!(
4197 result.to_string(),
4198 filtered,
4199 );
4200 }
4201
4202 #[test]
4203 fn ns_svg_set_url_attr() {
4204 let fragment = r##"
4205 <svg>
4206 <a>
4207 <set attributeName="href" to="./test2"></set>
4208 <set attributeName="href" to="http://example.com"></set>
4209 <set attributeName="href" to="javascript:xss"></set>
4210 </a>
4211 </svg>
4212 "##;
4213 let filtered = r##"
4214 <svg>
4215 <a rel="noopener noreferrer">
4216 <set attributeName="href" to="http://notriddle.com/test2"></set>
4217 <set attributeName="href" to="http://example.com"></set>
4218
4219 </a>
4220 </svg>
4221 "##;
4222 let result = Builder::default()
4223 .add_tags(&["svg","a","set"])
4224 .add_tag_attributes("set", ["attributeName","values","from","to"])
4225 .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4226 .clean(fragment);
4227 assert_eq!(
4228 result.to_string(),
4229 filtered,
4230 );
4231 }
4232
4233 #[test]
4234 fn ns_svg_set_url_xlink_attr() {
4235 let fragment = r##"
4236 <svg>
4237 <a>
4238 <set attributeName="xlink:href" to="./test2"></set>
4239 <set attributeName="xlink:href" to="http://example.com"></set>
4240 <set attributeName="xlink:href" to="javascript:xss"></set>
4241 </a>
4242 </svg>
4243 "##;
4244 let filtered = r##"
4245 <svg>
4246 <a rel="noopener noreferrer">
4247 <set attributeName="xlink:href" to="http://notriddle.com/test2"></set>
4248 <set attributeName="xlink:href" to="http://example.com"></set>
4249
4250 </a>
4251 </svg>
4252 "##;
4253 let result = Builder::default()
4254 .add_tags(&["svg","a","set"])
4255 .add_tag_attributes("a", ["xlink:href"])
4256 .add_tag_attributes("set", ["attributeName","values","from","to"])
4257 .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4258 .clean(fragment);
4259 assert_eq!(
4260 result.to_string(),
4261 filtered,
4262 );
4263 }
4264
4265 #[test]
4266 fn ns_svg_set_url_attr_non_path() {
4267 let fragment = r##"
4268 <svg>
4269 <a>
4270 <set attributeName="href" to="./test2"></set>
4271 <set attributeName="href" to="http://example.com"></set>
4272 <set attributeName="href" to="javascript:xss"></set>
4273 </a>
4274 </svg>
4275 "##;
4276 let filtered = r##"
4277 <svg>
4278 <a rel="noopener noreferrer">
4279 <set attributeName="href"></set>
4280 <set attributeName="href" to="http://example.com"></set>
4281
4282 </a>
4283 </svg>
4284 "##;
4285 let result = Builder::default()
4286 .add_tags(&["svg","a","set"])
4287 .add_tag_attributes("set", ["attributeName","values","from","to"])
4288 .url_relative(UrlRelative::RewriteWithBase(Url::parse("magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709&xt=urn:btmh:1220e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855").unwrap()))
4289 .clean(fragment);
4290 assert_eq!(
4291 result.to_string(),
4292 filtered,
4293 );
4294 }
4295
4296 #[test]
4297 fn ns_svg_animate_set_attr() {
4298 let fragment = r##"
4299 <svg>
4300 <a>
4301 <animate attributeName="x" values="1;2;3"></animate>
4302 <animate attributeName="x" from="1" to="2"></animate>
4303 <animate attributeName="x" from="1"></animate>
4304 <animate attributeName="x" to="2"></animate>
4305 <animate attributeName="y" values="1;2;3"></animate>
4306 <animate attributeName="y" from="1" to="2"></animate>
4307 <animate attributeName="y" from="1"></animate>
4308 <animate attributeName="y" to="2"></animate>
4309 </a>
4310 </svg>
4311 "##;
4312 let filtered = r##"
4313 <svg>
4314 <a x="0" rel="noopener noreferrer">
4315
4316
4317
4318
4319 <animate attributeName="y" values="1;2;3"></animate>
4320 <animate attributeName="y" from="1" to="2"></animate>
4321 <animate attributeName="y" from="1"></animate>
4322 <animate attributeName="y" to="2"></animate>
4323 </a>
4324 </svg>
4325 "##;
4326 let result = Builder::default()
4327 .add_tags(&["svg","a","animate"])
4328 .add_tag_attributes("animate", ["attributeName","values","from","to"])
4329 .add_tag_attributes("a", ["x", "y"])
4330 .set_tag_attribute_value("a", "x", "0")
4331 .clean(fragment);
4332 assert_eq!(
4333 result.to_string(),
4334 filtered,
4335 );
4336 }
4337
4338 #[test]
4339 fn ns_svg_animate_attr_filter() {
4340 let fragment = r##"
4341 <svg>
4342 <a>
4343 <animate attributeName="x" values="1;2;3"></animate>
4344 <animate attributeName="x" from="1" to="2"></animate>
4345 <animate attributeName="x" from="1"></animate>
4346 <animate attributeName="x" to="2"></animate>
4347 <animate attributeName="y" values="1;2;3"></animate>
4348 <animate attributeName="y" from="1" to="2"></animate>
4349 <animate attributeName="y" from="1"></animate>
4350 <animate attributeName="y" to="2"></animate>
4351 </a>
4352 </svg>
4353 "##;
4354 let filtered = r##"
4355 <svg>
4356 <a rel="noopener noreferrer">
4357 <animate attributeName="x" values="0;0;0"></animate>
4358 <animate attributeName="x" from="0" to="0"></animate>
4359 <animate attributeName="x" from="0"></animate>
4360 <animate attributeName="x" to="0"></animate>
4361 <animate attributeName="y"></animate>
4362 <animate attributeName="y" to="2"></animate>
4363 <animate attributeName="y"></animate>
4364 <animate attributeName="y" to="2"></animate>
4365 </a>
4366 </svg>
4367 "##;
4368 let result = Builder::default()
4369 .add_tags(&["svg","a","animate"])
4370 .add_tag_attributes("animate", ["attributeName","values","from","to"])
4371 .add_tag_attributes("a", ["x", "y"])
4372 .attribute_filter(|_tag, key, value| Some(if key == "x" {
4373 "0".into()
4374 } else if key == "y" && value == "1" {
4375 return None;
4376 } else {
4377 value.into()
4378 }))
4379 .clean(fragment);
4380 assert_eq!(
4381 result.to_string(),
4382 filtered,
4383 );
4384 }
4385
4386 #[test]
4387 fn ns_svg_animate_allowed_classes() {
4388 let fragment = r##"
4389 <svg>
4390 <a>
4391 <animate attributeName="class" values="a b c;a b c d;a b"></animate>
4392 <animate attributeName="class" from="a b c" to="a b c d"></animate>
4393 <animate attributeName="class" from="a b c d"></animate>
4394 <animate attributeName="class" to="a d c"></animate>
4395 </a>
4396 </svg>
4397 "##;
4398 let filtered = r##"
4399 <svg>
4400 <a rel="noopener noreferrer">
4401 <animate attributeName="class" values="a b c;a b c;a b"></animate>
4402 <animate attributeName="class" from="a b c" to="a b c"></animate>
4403 <animate attributeName="class" from="a b c"></animate>
4404 <animate attributeName="class" to="a c"></animate>
4405 </a>
4406 </svg>
4407 "##;
4408 let result = Builder::default()
4409 .add_tags(&["svg","a","animate"])
4410 .add_tag_attributes("animate", ["attributeName","values","from","to"])
4411 .add_allowed_classes("a", ["a", "b", "c"])
4412 .clean(fragment);
4413 assert_eq!(
4414 result.to_string(),
4415 filtered,
4416 );
4417 }
4418
4419 #[test]
4420 fn ns_svg_animate_allowed_styles() {
4421 let fragment = r##"
4422 <svg>
4423 <a>
4424 <animate attributeName="style" values="background:red;color:blue"></animate>
4425 <animate attributeName="style" from="background:red;text-decoration:none" to="color: blue;background:red"></animate>
4426 <animate attributeName="style" from="background:red;text-decoration:none"></animate>
4427 <animate attributeName="style" to="text-decoration:none"></animate>
4428 </a>
4429 </svg>
4430 "##;
4431 let filtered = r##"
4432 <svg>
4433 <a rel="noopener noreferrer">
4434 <animate attributeName="style" values="background:red;"></animate>
4435 <animate attributeName="style" from="background:red" to="background:red"></animate>
4436 <animate attributeName="style" from="background:red"></animate>
4437 <animate attributeName="style" to=""></animate>
4438 </a>
4439 </svg>
4440 "##;
4441 let result = Builder::default()
4442 .add_tags(&["svg","a","animate"])
4443 .add_tag_attributes("animate", ["attributeName","values","from","to"])
4444 .add_tag_attributes("a", ["style"])
4445 .filter_style_properties(["background"].into())
4446 .clean(fragment);
4447 assert_eq!(
4448 result.to_string(),
4449 filtered,
4450 );
4451 }
4452
4453 #[test]
4454 fn ns_svg_animate_url_attr_href() {
4455 let fragment = r##"
4456 <svg>
4457 <a id="x1">
4458 </a>
4459 <animate href="#x1" attributeName="xss" values="http://example.com"></animate>
4460 <animate href="#x1" attributeName="href" values="http://example.com"></animate>
4461 <animate href="#x2" attributeName="href" values="http://example.com;/test"></animate>
4462 <animate href="#x2" attributeName="href" values="http://example.com;/test;javascript:xss"></animate>
4463 <animate href="#x2" attributeName="href" from="http://example.com" to="http://example.com"></animate>
4464 <animate href="#x2" attributeName="href" from="javascript:xss" to="http://example.com"></animate>
4465 <animate href="#x2" attributeName="href" from="http://example.com" to="javascript:xss"></animate>
4466 <animate href="#x2" attributeName="href" from="http://example.com" to="./test2"></animate>
4467 <animate href="#x2" attributeName="href" from="./test2" to="http://example.com"></animate>
4468 </svg>
4469 "##;
4470 let filtered = r##"
4471 <svg>
4472 <a rel="noopener noreferrer">
4473 </a>
4474
4475 <animate href="#x1" attributeName="href" values="http://example.com"></animate>
4476
4477
4478
4479
4480
4481
4482
4483 </svg>
4484 "##;
4485 let result = Builder::default()
4486 .add_tags(&["svg","a","animate"])
4487 .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4488 .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4489 .clean(fragment);
4490 assert_eq!(
4491 result.to_string(),
4492 filtered,
4493 );
4494 }
4495
4496 #[test]
4497 fn ns_svg_animate_url_attr_href_depends_on_tag_name() {
4498 let fragment = r##"
4499 <object id="x2"></object>
4500 <svg>
4501 <g id="x1">
4502 </g>
4503 <animate href="#x1" attributeName="data" values="javascript:xss"></animate>
4504 <animate href="#x2" attributeName="data" values="javascript:xss"></animate>
4505 </svg>
4506 "##;
4507 let filtered = r##"
4508 <object id="x2"></object>
4509 <svg>
4510 <g id="x1">
4511 </g>
4512 <animate href="#x1" attributeName="data" values="javascript:xss"></animate>
4513
4514 </svg>
4515 "##;
4516 let result = Builder::default()
4517 .add_tags(&["svg","g","animate","object"])
4518 .add_tag_attributes("animate", ["attributeName","values","href"])
4519 .add_tag_attributes("g", ["data","id"])
4520 .add_tag_attributes("object", ["data","id"])
4521 .clean(fragment);
4522 assert_eq!(
4523 result.to_string(),
4524 filtered,
4525 );
4526 }
4527
4528 #[test]
4529 fn ns_svg_animate_set_attr_href() {
4530 let fragment = r##"
4531 <svg>
4532 <a id="x1">
4533 </a>
4534 <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4535 <animate href="#x1" attributeName="x" from="1" to="2"></animate>
4536 <animate href="#x1" attributeName="x" from="1"></animate>
4537 <animate href="#x1" attributeName="x" to="2"></animate>
4538 <animate href="#x1" attributeName="y" values="1;2;3"></animate>
4539 <animate href="#x1" attributeName="y" from="1" to="2"></animate>
4540 <animate href="#x1" attributeName="y" from="1"></animate>
4541 <animate href="#x2" attributeName="y" to="2"></animate>
4542 </svg>
4543 "##;
4544 let filtered = r##"
4545 <svg>
4546 <a id="x1" x="0" rel="noopener noreferrer">
4547 </a>
4548
4549
4550
4551
4552 <animate href="#x1" attributeName="y" values="1;2;3"></animate>
4553 <animate href="#x1" attributeName="y" from="1" to="2"></animate>
4554 <animate href="#x1" attributeName="y" from="1"></animate>
4555
4556 </svg>
4557 "##;
4558 let result = Builder::default()
4559 .add_tags(&["svg","a","animate"])
4560 .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4561 .add_tag_attributes("a", ["id", "x", "y"])
4562 .set_tag_attribute_value("a", "x", "0")
4563 .clean(fragment);
4564 assert_eq!(
4565 result.to_string(),
4566 filtered,
4567 );
4568 }
4569
4570 #[test]
4571 fn ns_svg_animate_attr_filter_href() {
4572 let fragment = r##"
4573 <svg>
4574 <a id="x1">
4575 </a>
4576 <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4577 <animate href="#x1" attributeName="x" from="1" to="2"></animate>
4578 <animate href="#x1" attributeName="x" from="1"></animate>
4579 <animate href="#x1" attributeName="x" to="2"></animate>
4580 <animate href="#x1" attributeName="y" values="1;2;3"></animate>
4581 <animate href="#x1" attributeName="y" from="1" to="2"></animate>
4582 <animate href="x1" attributeName="y" from="1"></animate>
4583 <animate href="#x2" attributeName="y" to="2"></animate>
4584 </svg>
4585 "##;
4586 let filtered = r##"
4587 <svg>
4588 <a id="x1" rel="noopener noreferrer">
4589 </a>
4590 <animate href="#x1" attributeName="x" values="0;0;0"></animate>
4591 <animate href="#x1" attributeName="x" from="0" to="0"></animate>
4592 <animate href="#x1" attributeName="x" from="0"></animate>
4593 <animate href="#x1" attributeName="x" to="0"></animate>
4594 <animate href="#x1" attributeName="y"></animate>
4595 <animate href="#x1" attributeName="y" to="2"></animate>
4596
4597
4598 </svg>
4599 "##;
4600 let result = Builder::default()
4601 .add_tags(&["svg","a","animate"])
4602 .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4603 .add_tag_attributes("a", ["id", "x", "y"])
4604 .attribute_filter(|_tag, key, value| Some(if key == "x" {
4605 "0".into()
4606 } else if key == "y" && value == "1" {
4607 return None;
4608 } else {
4609 value.into()
4610 }))
4611 .clean(fragment);
4612 assert_eq!(
4613 result.to_string(),
4614 filtered,
4615 );
4616 }
4617
4618 #[test]
4619 fn ns_svg_animate_id_conflict() {
4620 let fragment = r##"
4621 <svg>
4622 <a id="x1">
4623 </a>
4624 <a id="x2">
4625 </a>
4626 <a id="x2">
4627 </a>
4628 <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4629 <animate href="#x2" attributeName="x" values="1;2;3"></animate>
4630 <animate href="#x3" attributeName="x" values="1;2;3"></animate>
4631 </svg>
4632 "##;
4633 let filtered = r##"
4634 <svg>
4635 <a id="x1" rel="noopener noreferrer">
4636 </a>
4637 <a id="x2" rel="noopener noreferrer">
4638 </a>
4639 <a id="x2" rel="noopener noreferrer">
4640 </a>
4641 <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4642
4643
4644 </svg>
4645 "##;
4646 let result = Builder::default()
4647 .add_tags(&["svg","a","animate"])
4648 .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4649 .add_tag_attributes("a", ["id", "x", "y"])
4650 .clean(fragment);
4651 assert_eq!(
4652 result.to_string(),
4653 filtered,
4654 );
4655 }
4656
4657 #[test]
4658 fn xml_processing_instruction() {
4659 // https://blog.slonser.info/posts/dompurify-node-type-confusion/
4660 let fragment = r##"<svg><?xml-stylesheet src='slonser' ?></svg>"##;
4661 let result = String::from(Builder::new().clean(fragment));
4662 assert_eq!(result.to_string(), "");
4663
4664 let fragment = r##"<svg><?xml-stylesheet src='slonser' ?></svg>"##;
4665 let result = String::from(Builder::new().add_tags(&["svg"]).clean(fragment));
4666 assert_eq!(result.to_string(), "<svg></svg>");
4667
4668 let fragment = r##"<svg><?xml-stylesheet ><img src=x onerror="alert('Ammonia bypassed!!!')"> ?></svg>"##;
4669 let result = String::from(Builder::new().add_tags(&["svg"]).clean(fragment));
4670 assert_eq!(result.to_string(), "<svg></svg><img src=\"x\"> ?>");
4671 }
4672
4673 #[test]
4674 fn generic_attribute_prefixes() {
4675 let prefix_data = ["data-"];
4676 let prefix_code = ["code-"];
4677 let mut b = Builder::new();
4678 let mut hs: HashSet<&'_ str> = HashSet::new();
4679 hs.insert("data-");
4680 assert!(b.generic_attribute_prefixes.is_none());
4681 b.generic_attribute_prefixes(hs);
4682 assert!(b.generic_attribute_prefixes.is_some());
4683 assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4684 b.add_generic_attribute_prefixes(&prefix_data);
4685 assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4686 b.add_generic_attribute_prefixes(&prefix_code);
4687 assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 2);
4688 b.rm_generic_attribute_prefixes(&prefix_code);
4689 assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4690 b.rm_generic_attribute_prefixes(&prefix_code);
4691 assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4692 b.rm_generic_attribute_prefixes(&prefix_data);
4693 assert!(b.generic_attribute_prefixes.is_none());
4694 }
4695
4696 #[test]
4697 fn selectedcontent() {
4698 // https://github.com/servo/html5ever/issues/712
4699 let fragment1 = r#"<select><selectedcontent></selectedcontent><option>X"#;
4700 let fragment2 = r#"<select><selectedcontent></selectedcontent><option>X</option></select>"#;
4701 let expected = r#"<select><selectedcontent></selectedcontent><option>X</option></select>"#;
4702 assert_eq!(String::from(Builder::new().add_tags(&["select", "selectedcontent", "option"]).clean(fragment1)), expected);
4703 assert_eq!(String::from(Builder::new().add_tags(&["select", "selectedcontent", "option"]).clean(fragment2)), expected);
4704 }
4705
4706 #[test]
4707 fn new_select_parse() {
4708 // https://github.com/whatwg/html/issues/10310#issuecomment-2304377029
4709 let fragment = r#"
4710<select><style></select><img src onerror=xss()></style></select>
4711 "#;
4712 let expected = r#"
4713<select></select>
4714 "#;
4715 assert_eq!(String::from(Builder::new().add_tags(&["select", "new-select"]).clean_content_tags(hashset!["style"]).clean(fragment)), expected);
4716 }
4717
4718 #[test]
4719 fn selectedcontent_not_in_select() {
4720 // https://github.com/whatwg/html/issues/10310#issuecomment-2304377029
4721 let fragment = r#"
4722<selectedcontent>first</selectedcontent>
4723<div><selectedcontent>second</selectedcontent></div>
4724<select><selectedcontent>third</selectedcontent></select>
4725 "#;
4726 let expected = r#"
4727<selectedcontent>first</selectedcontent>
4728<div><selectedcontent>second</selectedcontent></div>
4729<select><selectedcontent></selectedcontent></select>
4730 "#;
4731 assert_eq!(String::from(Builder::new().add_tags(&["select", "selectedcontent"]).clean(fragment)), expected);
4732 }
4733
4734 #[test]
4735 fn generic_attribute_prefixes_clean() {
4736 let fragment = r#"<a data-1 data-2 code-1 code-2><a>Hello!</a></a>"#;
4737 let result_cleaned = String::from(
4738 Builder::new()
4739 .add_tag_attributes("a", &["data-1"])
4740 .clean(fragment),
4741 );
4742 assert_eq!(
4743 result_cleaned,
4744 r#"<a data-1="" rel="noopener noreferrer"></a><a rel="noopener noreferrer">Hello!</a>"#
4745 );
4746 let result_allowed = String::from(
4747 Builder::new()
4748 .add_tag_attributes("a", &["data-1"])
4749 .add_generic_attribute_prefixes(&["data-"])
4750 .clean(fragment),
4751 );
4752 assert_eq!(
4753 result_allowed,
4754 r#"<a data-1="" data-2="" rel="noopener noreferrer"></a><a rel="noopener noreferrer">Hello!</a>"#
4755 );
4756 let result_allowed = String::from(
4757 Builder::new()
4758 .add_tag_attributes("a", &["data-1", "code-1"])
4759 .add_generic_attribute_prefixes(&["data-", "code-"])
4760 .clean(fragment),
4761 );
4762 assert_eq!(
4763 result_allowed,
4764 r#"<a data-1="" data-2="" code-1="" code-2="" rel="noopener noreferrer"></a><a rel="noopener noreferrer">Hello!</a>"#
4765 );
4766 }
4767 #[test]
4768 fn lesser_than_isnt_html() {
4769 let fragment = "1 < 2";
4770 assert!(!is_html(fragment));
4771 }
4772 #[test]
4773 fn dense_lesser_than_isnt_html() {
4774 let fragment = "1<2";
4775 assert!(!is_html(fragment));
4776 }
4777 #[test]
4778 fn what_about_number_elements() {
4779 let fragment = "foo<2>bar";
4780 assert!(!is_html(fragment));
4781 }
4782 #[test]
4783 fn turbofish_is_html_sadly() {
4784 let fragment = "Vec::<u8>::new()";
4785 assert!(is_html(fragment));
4786 }
4787 #[test]
4788 fn stop_grinning() {
4789 let fragment = "did you really believe me? <g>";
4790 assert!(is_html(fragment));
4791 }
4792 #[test]
4793 fn dont_be_bold() {
4794 let fragment = "<b>";
4795 assert!(is_html(fragment));
4796 }
4797
4798 #[test]
4799 fn rewrite_with_root() {
4800 let tests = [
4801 (
4802 "https://github.com/rust-ammonia/ammonia/blob/master/",
4803 "README.md",
4804 "",
4805 "https://github.com/rust-ammonia/ammonia/blob/master/README.md",
4806 ),
4807 (
4808 "https://github.com/rust-ammonia/ammonia/blob/master/",
4809 "README.md",
4810 "/",
4811 "https://github.com/rust-ammonia/ammonia/blob/master/",
4812 ),
4813 (
4814 "https://github.com/rust-ammonia/ammonia/blob/master/",
4815 "README.md",
4816 "/CONTRIBUTING.md",
4817 "https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md",
4818 ),
4819 (
4820 "https://github.com/rust-ammonia/ammonia/blob/master",
4821 "README.md",
4822 "",
4823 "https://github.com/rust-ammonia/ammonia/blob/README.md",
4824 ),
4825 (
4826 "https://github.com/rust-ammonia/ammonia/blob/master",
4827 "README.md",
4828 "/",
4829 "https://github.com/rust-ammonia/ammonia/blob/",
4830 ),
4831 (
4832 "https://github.com/rust-ammonia/ammonia/blob/master",
4833 "README.md",
4834 "/CONTRIBUTING.md",
4835 "https://github.com/rust-ammonia/ammonia/blob/CONTRIBUTING.md",
4836 ),
4837 (
4838 "https://github.com/rust-ammonia/ammonia/blob/master/",
4839 "",
4840 "",
4841 "https://github.com/rust-ammonia/ammonia/blob/master/",
4842 ),
4843 (
4844 "https://github.com/rust-ammonia/ammonia/blob/master/",
4845 "",
4846 "/",
4847 "https://github.com/rust-ammonia/ammonia/blob/master/",
4848 ),
4849 (
4850 "https://github.com/rust-ammonia/ammonia/blob/master/",
4851 "",
4852 "/CONTRIBUTING.md",
4853 "https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md",
4854 ),
4855 (
4856 "https://github.com/",
4857 "rust-ammonia/ammonia/blob/master/README.md",
4858 "",
4859 "https://github.com/rust-ammonia/ammonia/blob/master/README.md",
4860 ),
4861 (
4862 "https://github.com/",
4863 "rust-ammonia/ammonia/blob/master/README.md",
4864 "/",
4865 "https://github.com/",
4866 ),
4867 (
4868 "https://github.com/",
4869 "rust-ammonia/ammonia/blob/master/README.md",
4870 "CONTRIBUTING.md",
4871 "https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md",
4872 ),
4873 (
4874 "https://github.com/",
4875 "rust-ammonia/ammonia/blob/master/README.md",
4876 "/CONTRIBUTING.md",
4877 "https://github.com/CONTRIBUTING.md",
4878 ),
4879 ];
4880 for (root, path, url, result) in tests {
4881 let h = format!(r#"<a href="{url}">test</a>"#);
4882 let r = format!(r#"<a href="{result}" rel="noopener noreferrer">test</a>"#);
4883 let a = Builder::new()
4884 .url_relative(UrlRelative::RewriteWithRoot {
4885 root: Url::parse(root).unwrap(),
4886 path: path.to_string(),
4887 })
4888 .clean(&h)
4889 .to_string();
4890 if r != a {
4891 println!(
4892 "failed to check ({root}, {path}, {url}, {result})\n{r} != {a}",
4893 r = r
4894 );
4895 assert_eq!(r, a);
4896 }
4897 }
4898 }
4899}