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