754a014ca6268dc3b2020204f3dad54c66552ed6
[yaffs-website] / web / core / modules / editor / src / EditorXssFilter / Standard.php
1 <?php
2
3 namespace Drupal\editor\EditorXssFilter;
4
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Utility\Xss;
7 use Drupal\filter\FilterFormatInterface;
8 use Drupal\editor\EditorXssFilterInterface;
9
10 /**
11  * Defines the standard text editor XSS filter.
12  */
13 class Standard extends Xss implements EditorXssFilterInterface {
14
15   /**
16    * {@inheritdoc}
17    */
18   public static function filterXss($html, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL) {
19     // Apply XSS filtering, but blacklist the <script>, <style>, <link>, <embed>
20     // and <object> tags.
21     // The <script> and <style> tags are blacklisted because their contents
22     // can be malicious (and therefore they are inherently unsafe), whereas for
23     // all other tags, only their attributes can make them malicious. Since
24     // \Drupal\Component\Utility\Xss::filter() protects against malicious
25     // attributes, we take no blacklisting action.
26     // The exceptions to the above rule are <link>, <embed> and <object>:
27     // - <link> because the href attribute allows the attacker to import CSS
28     //   using the HTTP(S) protocols which Xss::filter() considers safe by
29     //   default. The imported remote CSS is applied to the main document, thus
30     //   allowing for the same XSS attacks as a regular <style> tag.
31     // - <embed> and <object> because these tags allow non-HTML applications or
32     //   content to be embedded using the src or data attributes, respectively.
33     //   This is safe in the case of HTML documents, but not in the case of
34     //   Flash objects for example, that may access/modify the main document
35     //   directly.
36     // <iframe> is considered safe because it only allows HTML content to be
37     // embedded, hence ensuring the same origin policy always applies.
38     $dangerous_tags = ['script', 'style', 'link', 'embed', 'object'];
39
40     // Simply blacklisting these five dangerous tags would bring safety, but
41     // also user frustration: what if a text format is configured to allow
42     // <embed>, for example? Then we would strip that tag, even though it is
43     // allowed, thereby causing data loss!
44     // Therefore, we want to be smarter still. We want to take into account
45     // which HTML tags are allowed and forbidden by the text format we're
46     // filtering for, and if we're switching from another text format, we want
47     // to take that format's allowed and forbidden tags into account as well.
48     // In other words: we only expect markup allowed in both the original and
49     // the new format to continue to exist.
50     $format_restrictions = $format->getHtmlRestrictions();
51     if ($original_format !== NULL) {
52       $original_format_restrictions = $original_format->getHtmlRestrictions();
53     }
54
55     // Any tags that are explicitly blacklisted by the text format must be
56     // appended to the list of default dangerous tags: if they're explicitly
57     // forbidden, then we must respect that configuration.
58     // When switching from another text format, we must use the union of
59     // forbidden tags: if either text format is more restrictive, then the
60     // safety expectations of *both* text formats apply.
61     $forbidden_tags = self::getForbiddenTags($format_restrictions);
62     if ($original_format !== NULL) {
63       $forbidden_tags = array_merge($forbidden_tags, self::getForbiddenTags($original_format_restrictions));
64     }
65
66     // Any tags that are explicitly whitelisted by the text format must be
67     // removed from the list of default dangerous tags: if they're explicitly
68     // allowed, then we must respect that configuration.
69     // When switching from another format, we must use the intersection of
70     // allowed tags: if either format is more restrictive, then the safety
71     // expectations of *both* formats apply.
72     $allowed_tags = self::getAllowedTags($format_restrictions);
73     if ($original_format !== NULL) {
74       $allowed_tags = array_intersect($allowed_tags, self::getAllowedTags($original_format_restrictions));
75     }
76
77     // Don't blacklist dangerous tags that are explicitly allowed in both text
78     // formats.
79     $blacklisted_tags = array_diff($dangerous_tags, $allowed_tags);
80
81     // Also blacklist tags that are explicitly forbidden in either text format.
82     $blacklisted_tags = array_merge($blacklisted_tags, $forbidden_tags);
83
84     $output = static::filter($html, $blacklisted_tags);
85
86     // Since data-attributes can contain encoded HTML markup that could be
87     // decoded and interpreted by editors, we need to apply XSS filtering to
88     // their contents.
89     return static::filterXssDataAttributes($output);
90   }
91
92   /**
93    * Applies a very permissive XSS/HTML filter to data-attributes.
94    *
95    * @param string $html
96    *   The string to apply the data-attributes filtering to.
97    *
98    * @return string
99    *   The filtered string.
100    */
101   protected static function filterXssDataAttributes($html) {
102     if (stristr($html, 'data-') !== FALSE) {
103       $dom = Html::load($html);
104       $xpath = new \DOMXPath($dom);
105       foreach ($xpath->query('//@*[starts-with(name(.), "data-")]') as $node) {
106         // The data-attributes contain an HTML-encoded value, so we need to
107         // decode the value, apply XSS filtering and then re-save as encoded
108         // value. There is no need to explicitly decode $node->value, since the
109         // DOMAttr::value getter returns the decoded value.
110         $value = Xss::filterAdmin($node->value);
111         $node->value = Html::escape($value);
112       }
113       $html = Html::serialize($dom);
114     }
115
116     return $html;
117   }
118
119   /**
120    * Get all allowed tags from a restrictions data structure.
121    *
122    * @param array|false $restrictions
123    *   Restrictions as returned by FilterInterface::getHTMLRestrictions().
124    *
125    * @return array
126    *   An array of allowed HTML tags.
127    *
128    * @see \Drupal\filter\Plugin\Filter\FilterInterface::getHTMLRestrictions()
129    */
130   protected static function getAllowedTags($restrictions) {
131     if ($restrictions === FALSE || !isset($restrictions['allowed'])) {
132       return [];
133     }
134
135     $allowed_tags = array_keys($restrictions['allowed']);
136     // Exclude the wildcard tag, which is used to set attribute restrictions on
137     // all tags simultaneously.
138     $allowed_tags = array_diff($allowed_tags, ['*']);
139
140     return $allowed_tags;
141   }
142
143   /**
144    * Get all forbidden tags from a restrictions data structure.
145    *
146    * @param array|false $restrictions
147    *   Restrictions as returned by FilterInterface::getHTMLRestrictions().
148    *
149    * @return array
150    *   An array of forbidden HTML tags.
151    *
152    * @see \Drupal\filter\Plugin\Filter\FilterInterface::getHTMLRestrictions()
153    */
154   protected static function getForbiddenTags($restrictions) {
155     if ($restrictions === FALSE || !isset($restrictions['forbidden_tags'])) {
156       return [];
157     }
158     else {
159       return $restrictions['forbidden_tags'];
160     }
161   }
162
163   /**
164    * {@inheritdoc}
165    */
166   protected static function needsRemoval($html_tags, $elem) {
167     // See static::filterXss() about how this class uses blacklisting instead
168     // of the normal whitelisting.
169     return !parent::needsRemoval($html_tags, $elem);
170   }
171
172 }