]> BookStack Code Mirror - bookstack/blob - app/Util/HtmlContentFilter.php
HTML: Aligned and standardised DOMDocument usage
[bookstack] / app / Util / HtmlContentFilter.php
1 <?php
2
3 namespace BookStack\Util;
4
5 use DOMAttr;
6 use DOMElement;
7 use DOMNodeList;
8
9 class HtmlContentFilter
10 {
11     /**
12      * Remove all the script elements from the given HTML.
13      */
14     public static function removeScripts(string $html): string
15     {
16         if (empty($html)) {
17             return $html;
18         }
19
20         $doc = new HtmlDocument($html);
21
22         // Remove standard script tags
23         $scriptElems = $doc->queryXPath('//script');
24         static::removeNodes($scriptElems);
25
26         // Remove clickable links to JavaScript URI
27         $badLinks = $doc->queryXPath('//*[' . static::xpathContains('@href', 'javascript:') . ']');
28         static::removeNodes($badLinks);
29
30         // Remove forms with calls to JavaScript URI
31         $badForms = $doc->queryXPath('//*[' . static::xpathContains('@action', 'javascript:') . '] | //*[' . static::xpathContains('@formaction', 'javascript:') . ']');
32         static::removeNodes($badForms);
33
34         // Remove meta tag to prevent external redirects
35         $metaTags = $doc->queryXPath('//meta[' . static::xpathContains('@content', 'url') . ']');
36         static::removeNodes($metaTags);
37
38         // Remove data or JavaScript iFrames
39         $badIframes = $doc->queryXPath('//*[' . static::xpathContains('@src', 'data:') . '] | //*[' . static::xpathContains('@src', 'javascript:') . '] | //*[@srcdoc]');
40         static::removeNodes($badIframes);
41
42         // Remove attributes, within svg children, hiding JavaScript or data uris.
43         // A bunch of svg element and attribute combinations expose xss possibilities.
44         // For example, SVG animate tag can exploit javascript in values.
45         $badValuesAttrs = $doc->queryXPath('//svg//@*[' . static::xpathContains('.', 'data:') . '] | //svg//@*[' . static::xpathContains('.', 'javascript:') . ']');
46         static::removeAttributes($badValuesAttrs);
47
48         // Remove elements with a xlink:href attribute
49         // Used in SVG but deprecated anyway, so we'll be a bit more heavy-handed here.
50         $xlinkHrefAttributes = $doc->queryXPath('//@*[contains(name(), \'xlink:href\')]');
51         static::removeAttributes($xlinkHrefAttributes);
52
53         // Remove 'on*' attributes
54         $onAttributes = $doc->queryXPath('//@*[starts-with(name(), \'on\')]');
55         static::removeAttributes($onAttributes);
56
57         return $doc->getBodyInnerHtml();
58     }
59
60     /**
61      * Create a xpath contains statement with a translation automatically built within
62      * to affectively search in a cases-insensitive manner.
63      */
64     protected static function xpathContains(string $property, string $value): string
65     {
66         $value = strtolower($value);
67         $upperVal = strtoupper($value);
68
69         return 'contains(translate(' . $property . ', \'' . $upperVal . '\', \'' . $value . '\'), \'' . $value . '\')';
70     }
71
72     /**
73      * Remove all the given DOMNodes.
74      */
75     protected static function removeNodes(DOMNodeList $nodes): void
76     {
77         foreach ($nodes as $node) {
78             $node->parentNode->removeChild($node);
79         }
80     }
81
82     /**
83      * Remove all the given attribute nodes.
84      */
85     protected static function removeAttributes(DOMNodeList $attrs): void
86     {
87         /** @var DOMAttr $attr */
88         foreach ($attrs as $attr) {
89             $attrName = $attr->nodeName;
90             /** @var DOMElement $parentNode */
91             $parentNode = $attr->parentNode;
92             $parentNode->removeAttribute($attrName);
93         }
94     }
95 }