SafeMethods.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2020-2022 The MathJax Consortium
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * @fileoverview Support functions for the safe extension
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {length2em} from '../../util/lengths.js';
  23. import {Safe, FilterFunction} from './safe.js';
  24. /**
  25. * The default attribute-filtering functions
  26. */
  27. export const SafeMethods: {[name: string]: FilterFunction<any, any, any>} = {
  28. /**
  29. * Filter HREF URL's
  30. *
  31. * @param {Safe<N,T,D>} safe The Safe object being used
  32. * @param {string} url The URL being tested
  33. * @return {string|null} The URL if OK and null if not
  34. *
  35. * @template N The HTMLElement node class
  36. * @template T The Text node class
  37. * @template D The Document class
  38. */
  39. filterURL<N, T, D>(safe: Safe<N, T, D>, url: string): string | null {
  40. const protocol = (url.match(/^\s*([a-z]+):/i) || [null, ''])[1].toLowerCase();
  41. const allow = safe.allow.URLs;
  42. return (allow === 'all' || (allow === 'safe' &&
  43. (safe.options.safeProtocols[protocol] || !protocol))) ? url : null;
  44. },
  45. /**
  46. * Filter a class list
  47. *
  48. * @param {Safe<N,T,D>} safe The Safe object being used
  49. * @param {string} list The class list being tested
  50. * @return {string|null} The class list if OK and null if not
  51. *
  52. * @template N The HTMLElement node class
  53. * @template T The Text node class
  54. * @template D The Document class
  55. */
  56. filterClassList<N, T, D>(safe: Safe<N, T, D>, list: string): string | null {
  57. const classes = list.trim().replace(/\s\s+/g, ' ').split(/ /);
  58. return classes.map((name) => this.filterClass(safe, name) || '').join(' ').trim().replace(/\s\s+/g, '');
  59. },
  60. /**
  61. * Filter a class name
  62. *
  63. * @param {Safe<N,T,D>} safe The Safe object being used
  64. * @param {string} CLASS The class being tested
  65. * @return {string|null} The class if OK and null if not
  66. *
  67. * @template N The HTMLElement node class
  68. * @template T The Text node class
  69. * @template D The Document class
  70. */
  71. filterClass<N, T, D>(safe: Safe<N, T, D>, CLASS: string): string | null {
  72. const allow = safe.allow.classes;
  73. return (allow === 'all' || (allow === 'safe' && CLASS.match(safe.options.classPattern))) ? CLASS : null;
  74. },
  75. /**
  76. * Filter ids
  77. *
  78. * @param {Safe<N,T,D>} safe The Safe object being used
  79. * @param {string} id The id being tested
  80. * @return {string|null} The id if OK and null if not
  81. *
  82. * @template N The HTMLElement node class
  83. * @template T The Text node class
  84. * @template D The Document class
  85. */
  86. filterID<N, T, D>(safe: Safe<N, T, D>, id: string): string | null {
  87. const allow = safe.allow.cssIDs;
  88. return (allow === 'all' || (allow === 'safe' && id.match(safe.options.idPattern))) ? id : null;
  89. },
  90. /**
  91. * Filter style strings
  92. *
  93. * @param {Safe<N,T,D>} safe The Safe object being used
  94. * @param {string} styles The style string being tested
  95. * @return {string} The sanitized style string
  96. *
  97. * @template N The HTMLElement node class
  98. * @template T The Text node class
  99. * @template D The Document class
  100. */
  101. filterStyles<N, T, D>(safe: Safe<N, T, D>, styles: string): string {
  102. if (safe.allow.styles === 'all') return styles;
  103. if (safe.allow.styles !== 'safe') return null;
  104. const adaptor = safe.adaptor;
  105. const options = safe.options;
  106. try {
  107. //
  108. // Create div1 with styles set to the given styles, and div2 with blank styles
  109. //
  110. const div1 = adaptor.node('div', {style: styles});
  111. const div2 = adaptor.node('div');
  112. //
  113. // Check each allowed style and transfer OK ones to div2
  114. // If the style has Top/Right/Bottom/Left, look at all four separately
  115. //
  116. for (const style of Object.keys(options.safeStyles)) {
  117. if (options.styleParts[style]) {
  118. for (const sufix of ['Top', 'Right', 'Bottom', 'Left']) {
  119. const name = style + sufix;
  120. const value = this.filterStyle(safe, name, div1);
  121. if (value) {
  122. adaptor.setStyle(div2, name, value);
  123. }
  124. }
  125. } else {
  126. const value = this.filterStyle(safe, style, div1);
  127. if (value) {
  128. adaptor.setStyle(div2, style, value);
  129. }
  130. }
  131. }
  132. //
  133. // Return the div2 style string
  134. //
  135. styles = adaptor.allStyles(div2);
  136. } catch (err) {
  137. styles = '';
  138. }
  139. return styles;
  140. },
  141. /**
  142. * Filter an individual name:value style pair
  143. *
  144. * @param {Safe<N,T,D>} safe The Safe object being used
  145. * @param {string} style The style name being tested
  146. * @param {N} div The temp DIV node containing the style object to be tested
  147. * @return {string|null} The sanitized style string or null if invalid
  148. *
  149. * @template N The HTMLElement node class
  150. * @template T The Text node class
  151. * @template D The Document class
  152. */
  153. filterStyle<N, T, D>(safe: Safe<N, T, D>, style: string, div: N): string | null {
  154. const value = safe.adaptor.getStyle(div, style);
  155. if (typeof value !== 'string' || value === '' || value.match(/^\s*calc/) ||
  156. (value.match(/javascript:/) && !safe.options.safeProtocols.javascript) ||
  157. (value.match(/data:/) && !safe.options.safeProtocols.data)) {
  158. return null;
  159. }
  160. const name = style.replace(/Top|Right|Left|Bottom/, '');
  161. if (!safe.options.safeStyles[style] && !safe.options.safeStyles[name]) {
  162. return null;
  163. }
  164. return this.filterStyleValue(safe, style, value, div);
  165. },
  166. /**
  167. * Filter a style's value, handling compound values (e.g., borders that have widths as well as styles and colors)
  168. *
  169. * @param {Safe<N,T,D>} safe The Safe object being used
  170. * @param {string} style The style name being tested
  171. * @param {string} value The value of the style to test
  172. * @param {N} div The temp DIV node containing the style object to be tested
  173. * @return {string|null} The sanitized style string or null if invalid
  174. *
  175. * @template N The HTMLElement node class
  176. * @template T The Text node class
  177. * @template D The Document class
  178. */
  179. filterStyleValue<N, T, D>(safe: Safe<N, T, D>, style: string, value: string, div: N): string | null {
  180. const name = safe.options.styleLengths[style];
  181. if (!name) {
  182. return value;
  183. }
  184. if (typeof name !== 'string') {
  185. return this.filterStyleLength(safe, style, value);
  186. }
  187. const length = this.filterStyleLength(safe, name, safe.adaptor.getStyle(div, name));
  188. if (!length) {
  189. return null;
  190. }
  191. safe.adaptor.setStyle(div, name, length);
  192. return safe.adaptor.getStyle(div, style);
  193. },
  194. /**
  195. * Filter a length value
  196. *
  197. * @param {Safe<N,T,D>} safe The Safe object being used
  198. * @param {string} style The style name being tested
  199. * @param {string} value The value of the style to test
  200. * @return {string|null} The sanitized length value
  201. *
  202. * @template N The HTMLElement node class
  203. * @template T The Text node class
  204. * @template D The Document class
  205. */
  206. filterStyleLength<N, T, D>(safe: Safe<N, T, D>, style: string, value: string): string | null {
  207. if (!value.match(/^(.+)(em|ex|ch|rem|px|mm|cm|in|pt|pc|%)$/)) return null;
  208. const em = length2em(value, 1);
  209. const lengths = safe.options.styleLengths[style];
  210. const [m, M] = (Array.isArray(lengths) ? lengths : [-safe.options.lengthMax, safe.options.lengthMax]);
  211. return (m <= em && em <= M ? value : (em < m ? m : M).toFixed(3).replace(/\.?0+$/, '') + 'em');
  212. },
  213. /**
  214. * Filter a font size
  215. *
  216. * @param {Safe<N,T,D>} safe The Safe object being used
  217. * @param {string} size The font size to test
  218. * @return {string|null} The sanitized style string or null if invalid
  219. *
  220. * @template N The HTMLElement node class
  221. * @template T The Text node class
  222. * @template D The Document class
  223. */
  224. filterFontSize<N, T, D>(safe: Safe<N, T, D>, size: string): string | null {
  225. return this.filterStyleLength(safe, 'fontSize', size);
  226. },
  227. /**
  228. * Filter scriptsizemultiplier
  229. *
  230. * @param {Safe<N,T,D>} safe The Safe object being used
  231. * @param {string} size The script size multiplier to test
  232. * @return {string} The sanitized size
  233. *
  234. * @template N The HTMLElement node class
  235. * @template T The Text node class
  236. * @template D The Document class
  237. */
  238. filterSizeMultiplier<N, T, D>(safe: Safe<N, T, D>, size: string): string {
  239. const [m, M] = safe.options.scriptsizemultiplierRange || [-Infinity, Infinity];
  240. return Math.min(M, Math.max(m, parseFloat(size))).toString();
  241. },
  242. /**
  243. * Filter scriptLevel
  244. *
  245. * @param {Safe<N,T,D>} safe The Safe object being used
  246. * @param {string} size The scriptlevel to test
  247. * @return {string|null} The sanitized scriptlevel or null
  248. *
  249. * @template N The HTMLElement node class
  250. * @template T The Text node class
  251. * @template D The Document class
  252. */
  253. filterScriptLevel<N, T, D>(safe: Safe<N, T, D>, level: string): string | null {
  254. const [m, M] = safe.options.scriptlevelRange || [-Infinity, Infinity];
  255. return Math.min(M, Math.max(m, parseInt(level))).toString();
  256. },
  257. /**
  258. * Filter a data-* attribute
  259. *
  260. * @param {Safe<N,T,D>} safe The Safe object being used
  261. * @param {string} value The attribute's value
  262. * @param {string} id The attribute's id (e.g., data-mjx-variant)
  263. * @return {number|null} The sanitized value or null
  264. *
  265. * @template N The HTMLElement node class
  266. * @template T The Text node class
  267. * @template D The Document class
  268. */
  269. filterData<N, T, D>(safe: Safe<N, T, D>, value: string, id: string): string | null {
  270. return (id.match(safe.options.dataPattern) ? value : null);
  271. }
  272. };