UNPKG

25.6 kBMarkdownView Raw
1# DOMPurify
2
3[![npm version](https://badge.fury.io/js/dompurify.svg)](http://badge.fury.io/js/dompurify) ![Build and Test](https://github.com/cure53/DOMPurify/workflows/Build%20and%20Test/badge.svg?branch=main) [![Downloads](https://img.shields.io/npm/dm/dompurify.svg)](https://www.npmjs.com/package/dompurify) [![minified size](https://badgen.net/bundlephobia/min/dompurify?color=green&label=minified)](https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js) [![gzip size](https://badgen.net/bundlephobia/minzip/dompurify?color=green&label=gzipped)](https://packagephobia.now.sh/result?p=dompurify) [![dependents](https://badgen.net/github/dependents-repo/cure53/dompurify?color=green&label=dependents)](https://github.com/cure53/DOMPurify/network/dependents)
4
5[![NPM](https://nodei.co/npm/dompurify.png)](https://nodei.co/npm/dompurify/)
6
7DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG.
8
9It's also very simple to use and get started with. DOMPurify was [started in February 2014](https://github.com/cure53/DOMPurify/commit/a630922616927373485e0e787ab19e73e3691b2b) and, meanwhile, has reached version 3.0.0.
10
11DOMPurify is written in JavaScript and works in all modern browsers (Safari (10+), Opera (15+), Internet Explorer (10+), Edge, Firefox and Chrome - as well as almost anything else using Blink or WebKit). It doesn't break on MSIE6 or other legacy browsers. It either uses [a fall-back](#what-about-older-browsers-like-msie8) or simply does nothing.
12
13**Note that DOMPurify v2.4.5 is the final version supporting MSIE. For important security updates compatible with MSIE, please use the 2.x branch.**
14
15Our automated tests cover [19 different browsers](https://github.com/cure53/DOMPurify/blob/main/test/karma.custom-launchers.config.js#L5) right now, more to come. We also cover Node.js v14.x, v16.x, v17.x and v18.x, running DOMPurify on [jsdom](https://github.com/jsdom/jsdom). Older Node versions are known to work as well, but hey... no guarantees.
16
17DOMPurify is written by security people who have vast background in web attacks and XSS. Fear not. For more details please also read about our [Security Goals & Threat Model](https://github.com/cure53/DOMPurify/wiki/Security-Goals-&-Threat-Model). Please, read it. Like, really.
18
19## What does it do?
20
21DOMPurify sanitizes HTML and prevents XSS attacks. You can feed DOMPurify with string full of dirty HTML and it will return a string (unless configured otherwise) with clean HTML. DOMPurify will strip out everything that contains dangerous HTML and thereby prevent XSS attacks and other nastiness. It's also damn bloody fast. We use the technologies the browser provides and turn them into an XSS filter. The faster your browser, the faster DOMPurify will be.
22
23## How do I use it?
24
25It's easy. Just include DOMPurify on your website.
26
27### Using the unminified development version
28
29```html
30<script type="text/javascript" src="src/purify.js"></script>
31```
32
33### Using the minified and tested production version (source-map available)
34
35```html
36<script type="text/javascript" src="dist/purify.min.js"></script>
37```
38
39Afterwards you can sanitize strings by executing the following code:
40
41```js
42let clean = DOMPurify.sanitize(dirty);
43```
44
45Or maybe this, if you love working with Angular or alike:
46
47```js
48import * as DOMPurify from 'dompurify';
49
50let clean = DOMPurify.sanitize('<b>hello there</b>');
51```
52
53The resulting HTML can be written into a DOM element using `innerHTML` or the DOM using `document.write()`. That is fully up to you.
54Note that by default, we permit HTML, SVG **and** MathML. If you only need HTML, which might be a very common use-case, you can easily set that up as well:
55
56```js
57let clean = DOMPurify.sanitize(dirty, { USE_PROFILES: { html: true } });
58```
59
60### Where are the TypeScript type definitions?
61
62They can be found here: [@types/dompurify](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/dompurify)
63
64### Is there any foot-gun potential?
65
66Well, please note, if you _first_ sanitize HTML and then modify it _afterwards_, you might easily **void the effects of sanitization**. If you feed the sanitized markup to another library _after_ sanitization, please be certain that the library doesn't mess around with the HTML on its own.
67
68### Okay, makes sense, let's move on
69
70After sanitizing your markup, you can also have a look at the property `DOMPurify.removed` and find out, what elements and attributes were thrown out. Please **do not use** this property for making any security critical decisions. This is just a little helper for curious minds.
71
72### Running DOMPurify on the server
73
74DOMPurify technically also works server-side with Node.js. Our support strives to follow the [Node.js release cycle](https://nodejs.org/en/about/releases/).
75
76Running DOMPurify on the server requires a DOM to be present, which is probably no surprise. Usually, [jsdom](https://github.com/jsdom/jsdom) is the tool of choice and we **strongly recommend** to use the latest version of _jsdom_.
77
78Why? Because older versions of _jsdom_ are known to be buggy in ways that result in XSS _even if_ DOMPurify does everything 100% correctly. There are **known attack vectors** in, e.g. _jsdom v19.0.0_ that are fixed in _jsdom v20.0.0_ - and we really recommend to keep _jsdom_ up to date because of that.
79
80Other than that, you are fine to use DOMPurify on the server. Probably. This really depends on _jsdom_ or whatever DOM you utilize server-side. If you can live with that, this is how you get it to work:
81
82```bash
83npm install dompurify
84npm install jsdom
85```
86
87For _jsdom_ (please use an up-to-date version), this should do the trick:
88
89```js
90const createDOMPurify = require('dompurify');
91const { JSDOM } = require('jsdom');
92
93const window = new JSDOM('').window;
94const DOMPurify = createDOMPurify(window);
95const clean = DOMPurify.sanitize('<b>hello there</b>');
96```
97
98Or even this, if you prefer working with imports:
99
100```js
101import { JSDOM } from 'jsdom';
102import DOMPurify from 'dompurify';
103
104const window = new JSDOM('').window;
105const purify = DOMPurify(window);
106const clean = purify.sanitize('<b>hello there</b>');
107```
108
109If you have problems making it work in your specific setup, consider looking at the amazing [isomorphic-dompurify](https://github.com/kkomelin/isomorphic-dompurify) project which solves lots of problems people might run into.
110
111```bash
112npm install isomorphic-dompurify
113```
114
115```js
116import DOMPurify from 'isomorphic-dompurify';
117
118const clean = DOMPurify.sanitize('<s>hello</s>');
119```
120
121## Is there a demo?
122
123Of course there is a demo! [Play with DOMPurify](https://cure53.de/purify)
124
125## What if I find a _security_ bug?
126
127First of all, please immediately contact us via [email](mailto:mario@cure53.de) so we can work on a fix. [PGP key](https://keyserver.ubuntu.com/pks/lookup?op=vindex&search=0xC26C858090F70ADA)
128
129Also, you probably qualify for a bug bounty! The fine folks over at [Fastmail](https://www.fastmail.com/) use DOMPurify for their services and added our library to their bug bounty scope. So, if you find a way to bypass or weaken DOMPurify, please also have a look at their website and the [bug bounty info](https://www.fastmail.com/about/bugbounty/).
130
131## Some purification samples please?
132
133How does purified markup look like? Well, [the demo](https://cure53.de/purify) shows it for a big bunch of nasty elements. But let's also show some smaller examples!
134
135```js
136DOMPurify.sanitize('<img src=x onerror=alert(1)//>'); // becomes <img src="x">
137DOMPurify.sanitize('<svg><g/onload=alert(2)//<p>'); // becomes <svg><g></g></svg>
138DOMPurify.sanitize('<p>abc<iframe//src=jAva&Tab;script:alert(3)>def</p>'); // becomes <p>abc</p>
139DOMPurify.sanitize('<math><mi//xlink:href="data:x,<script>alert(4)</script>">'); // becomes <math><mi></mi></math>
140DOMPurify.sanitize('<TABLE><tr><td>HELLO</tr></TABL>'); // becomes <table><tbody><tr><td>HELLO</td></tr></tbody></table>
141DOMPurify.sanitize('<UL><li><A HREF=//google.com>click</UL>'); // becomes <ul><li><a href="//google.com">click</a></li></ul>
142```
143
144## What is supported?
145
146DOMPurify currently supports HTML5, SVG and MathML. DOMPurify per default allows CSS, HTML custom data attributes. DOMPurify also supports the Shadow DOM - and sanitizes DOM templates recursively. DOMPurify also allows you to sanitize HTML for being used with the jQuery `$()` and `elm.html()` API without any known problems.
147
148## What about older browsers like MSIE8?
149
150DOMPurify offers a fall-back behavior for older MSIE browsers. It uses the MSIE-only `toStaticHTML` feature to sanitize. Note however that in this fall-back mode, pretty much none of the configuration flags shown below have any effect. You need to handle that yourself.
151
152If not even `toStaticHTML` is supported, DOMPurify does nothing at all. It simply returns exactly the string that you fed it.
153
154DOMPurify also exposes a property called `isSupported`, which tells you whether DOMPurify will be able to do its job.
155
156## What about DOMPurify and Trusted Types?
157
158In version 1.0.9, support for [Trusted Types API](https://github.com/w3c/webappsec-trusted-types) was added to DOMPurify.
159In version 2.0.0, a config flag was added to control DOMPurify's behavior regarding this.
160
161When `DOMPurify.sanitize` is used in an environment where the Trusted Types API is available and `RETURN_TRUSTED_TYPE` is set to `true`, it tries to return a `TrustedHTML` value instead of a string (the behavior for `RETURN_DOM` and `RETURN_DOM_FRAGMENT` config options does not change).
162
163## Can I configure DOMPurify?
164
165Yes. The included default configuration values are pretty good already - but you can of course override them. Check out the [`/demos`](https://github.com/cure53/DOMPurify/tree/main/demos) folder to see a bunch of examples on how you can [customize DOMPurify](https://github.com/cure53/DOMPurify/tree/main/demos#what-is-this).
166
167```js
168/**
169 * General settings
170 */
171
172// strip {{ ... }}, ${ ... } and <% ... %> to make output safe for template systems
173// be careful please, this mode is not recommended for production usage.
174// allowing template parsing in user-controlled HTML is not advised at all.
175// only use this mode if there is really no alternative.
176var clean = DOMPurify.sanitize(dirty, {SAFE_FOR_TEMPLATES: true});
177
178/**
179 * Control our allow-lists and block-lists
180 */
181// allow only <b> elements, very strict
182var clean = DOMPurify.sanitize(dirty, {ALLOWED_TAGS: ['b']});
183
184// allow only <b> and <q> with style attributes
185var clean = DOMPurify.sanitize(dirty, {ALLOWED_TAGS: ['b', 'q'], ALLOWED_ATTR: ['style']});
186
187// allow all safe HTML elements but neither SVG nor MathML
188// note that the USE_PROFILES setting will override the ALLOWED_TAGS setting
189// so don't use them together
190var clean = DOMPurify.sanitize(dirty, {USE_PROFILES: {html: true}});
191
192// allow all safe SVG elements and SVG Filters, no HTML or MathML
193var clean = DOMPurify.sanitize(dirty, {USE_PROFILES: {svg: true, svgFilters: true}});
194
195// allow all safe MathML elements and SVG, but no SVG Filters
196var clean = DOMPurify.sanitize(dirty, {USE_PROFILES: {mathMl: true, svg: true}});
197
198// change the default namespace from HTML to something different
199var clean = DOMPurify.sanitize(dirty, {NAMESPACE: 'http://www.w3.org/2000/svg'});
200
201// leave all safe HTML as it is and add <style> elements to block-list
202var clean = DOMPurify.sanitize(dirty, {FORBID_TAGS: ['style']});
203
204// leave all safe HTML as it is and add style attributes to block-list
205var clean = DOMPurify.sanitize(dirty, {FORBID_ATTR: ['style']});
206
207// extend the existing array of allowed tags and add <my-tag> to allow-list
208var clean = DOMPurify.sanitize(dirty, {ADD_TAGS: ['my-tag']});
209
210// extend the existing array of allowed attributes and add my-attr to allow-list
211var clean = DOMPurify.sanitize(dirty, {ADD_ATTR: ['my-attr']});
212
213// prohibit ARIA attributes, leave other safe HTML as is (default is true)
214var clean = DOMPurify.sanitize(dirty, {ALLOW_ARIA_ATTR: false});
215
216// prohibit HTML5 data attributes, leave other safe HTML as is (default is true)
217var clean = DOMPurify.sanitize(dirty, {ALLOW_DATA_ATTR: false});
218
219/**
220 * Control behavior relating to Custom Elements
221 */
222
223// DOMPurify allows to define rules for Custom Elements. When using the CUSTOM_ELEMENT_HANDLING
224// literal, it is possible to define exactly what elements you wish to allow (by default, none are allowed).
225//
226// The same goes for their attributes. By default, the built-in or configured allow.list is used.
227//
228// You can use a RegExp literal to specify what is allowed or a predicate, examples for both can be seen below.
229// The default values are very restrictive to prevent accidental XSS bypasses. Handle with great care!
230
231
232var clean = DOMPurify.sanitize(
233 '<foo-bar baz="foobar" forbidden="true"></foo-bar><div is="foo-baz"></div>',
234 {
235 CUSTOM_ELEMENT_HANDLING: {
236 tagNameCheck: null, // no custom elements are allowed
237 attributeNameCheck: null, // default / standard attribute allow-list is used
238 allowCustomizedBuiltInElements: false, // no customized built-ins allowed
239 },
240 }
241); // <div is=""></div>
242
243var clean = DOMPurify.sanitize(
244 '<foo-bar baz="foobar" forbidden="true"></foo-bar><div is="foo-baz"></div>',
245 {
246 CUSTOM_ELEMENT_HANDLING: {
247 tagNameCheck: /^foo-/, // allow all tags starting with "foo-"
248 attributeNameCheck: /baz/, // allow all attributes containing "baz"
249 allowCustomizedBuiltInElements: true, // customized built-ins are allowed
250 },
251 }
252); // <foo-bar baz="foobar"></foo-bar><div is=""></div>
253
254var clean = DOMPurify.sanitize(
255 '<foo-bar baz="foobar" forbidden="true"></foo-bar><div is="foo-baz"></div>',
256 {
257 CUSTOM_ELEMENT_HANDLING: {
258 tagNameCheck: (tagName) => tagName.match(/^foo-/), // allow all tags starting with "foo-"
259 attributeNameCheck: (attr) => attr.match(/baz/), // allow all containing "baz"
260 allowCustomizedBuiltInElements: true, // allow customized built-ins
261 },
262 }
263); // <foo-bar baz="foobar"></foo-bar><div is="foo-baz"></div>
264
265/**
266 * Control behavior relating to URI values
267 */
268// extend the existing array of elements that can use Data URIs
269var clean = DOMPurify.sanitize(dirty, {ADD_DATA_URI_TAGS: ['a', 'area']});
270
271// extend the existing array of elements that are safe for URI-like values (be careful, XSS risk)
272var clean = DOMPurify.sanitize(dirty, {ADD_URI_SAFE_ATTR: ['my-attr']});
273
274/**
275 * Control permitted attribute values
276 */
277// allow external protocol handlers in URL attributes (default is false, be careful, XSS risk)
278// by default only http, https, ftp, ftps, tel, mailto, callto, cid and xmpp are allowed.
279var clean = DOMPurify.sanitize(dirty, {ALLOW_UNKNOWN_PROTOCOLS: true});
280
281// allow specific protocols handlers in URL attributes via regex (default is false, be careful, XSS risk)
282// by default only http, https, ftp, ftps, tel, mailto, callto, cid and xmpp are allowed.
283// Default RegExp: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
284var clean = DOMPurify.sanitize(dirty, {ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|xxx):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;});
285
286/**
287 * Influence the return-type
288 */
289// return a DOM HTMLBodyElement instead of an HTML string (default is false)
290var clean = DOMPurify.sanitize(dirty, {RETURN_DOM: true});
291
292// return a DOM DocumentFragment instead of an HTML string (default is false)
293var clean = DOMPurify.sanitize(dirty, {RETURN_DOM_FRAGMENT: true});
294
295// use the RETURN_TRUSTED_TYPE flag to turn on Trusted Types support if available
296var clean = DOMPurify.sanitize(dirty, {RETURN_TRUSTED_TYPE: true}); // will return a TrustedHTML object instead of a string if possible
297
298/**
299 * Influence how we sanitize
300 */
301// return entire document including <html> tags (default is false)
302var clean = DOMPurify.sanitize(dirty, {WHOLE_DOCUMENT: true});
303
304// disable DOM Clobbering protection on output (default is true, handle with care, minor XSS risks here)
305var clean = DOMPurify.sanitize(dirty, {SANITIZE_DOM: false});
306
307// enforce strict DOM Clobbering protection via namespace isolation (default is false)
308// when enabled, isolates the namespace of named properties (i.e., `id` and `name` attributes)
309// from JS variables by prefixing them with the string `user-content-`
310var clean = DOMPurify.sanitize(dirty, {SANITIZE_NAMED_PROPS: true});
311
312// keep an element's content when the element is removed (default is true)
313var clean = DOMPurify.sanitize(dirty, {KEEP_CONTENT: false});
314
315// glue elements like style, script or others to document.body and prevent unintuitive browser behavior in several edge-cases (default is false)
316var clean = DOMPurify.sanitize(dirty, {FORCE_BODY: true});
317
318// remove all <a> elements under <p> elements that are removed
319var clean = DOMPurify.sanitize(dirty, {FORBID_CONTENTS: ['a'], FORBID_TAGS: ['p']});
320
321// change the parser type so sanitized data is treated as XML and not as HTML, which is the default
322var clean = DOMPurify.sanitize(dirty, {PARSER_MEDIA_TYPE: 'application/xhtml+xml'});
323
324/**
325 * Influence where we sanitize
326 */
327// use the IN_PLACE mode to sanitize a node "in place", which is much faster depending on how you use DOMPurify
328var dirty = document.createElement('a');
329dirty.setAttribute('href', 'javascript:alert(1)');
330var clean = DOMPurify.sanitize(dirty, {IN_PLACE: true}); // see https://github.com/cure53/DOMPurify/issues/288 for more info
331```
332
333There is even [more examples here](https://github.com/cure53/DOMPurify/tree/main/demos#what-is-this), showing how you can run, customize and configure DOMPurify to fit your needs.
334
335## Persistent Configuration
336
337Instead of repeatedly passing the same configuration to `DOMPurify.sanitize`, you can use the `DOMPurify.setConfig` method. Your configuration will persist until your next call to `DOMPurify.setConfig`, or until you invoke `DOMPurify.clearConfig` to reset it. Remember that there is only one active configuration, which means once it is set, all extra configuration parameters passed to `DOMPurify.sanitize` are ignored.
338
339## Hooks
340
341DOMPurify allows you to augment its functionality by attaching one or more functions with the `DOMPurify.addHook` method to one of the following hooks:
342
343- `beforeSanitizeElements`
344- `uponSanitizeElement` (No 's' - called for every element)
345- `afterSanitizeElements`
346- `beforeSanitizeAttributes`
347- `uponSanitizeAttribute`
348- `afterSanitizeAttributes`
349- `beforeSanitizeShadowDOM`
350- `uponSanitizeShadowNode`
351- `afterSanitizeShadowDOM`
352
353It passes the currently processed DOM node, when needed a literal with verified node and attribute data and the DOMPurify configuration to the callback. Check out the [MentalJS hook demo](https://github.com/cure53/DOMPurify/blob/main/demos/hooks-mentaljs-demo.html) to see how the API can be used nicely.
354
355_Example_:
356
357```js
358DOMPurify.addHook(
359 'beforeSanitizeElements',
360 function (currentNode, hookEvent, config) {
361 // Do something with the current node and return it
362 // You can also mutate hookEvent (i.e. set hookEvent.forceKeepAttr = true)
363 return currentNode;
364 }
365);
366```
367
368## Continuous Integration
369
370We are currently using Github Actions in combination with BrowserStack. This gives us the possibility to confirm for each and every commit that all is going according to plan in all supported browsers. Check out the build logs here: https://github.com/cure53/DOMPurify/actions
371
372You can further run local tests by executing `npm test`. The tests work fine with Node.js v0.6.2 and jsdom@8.5.0.
373
374All relevant commits will be signed with the key `0x24BB6BF4` for additional security (since 8th of April 2016).
375
376### Development and contributing
377
378#### Installation (`npm i`)
379
380We support `npm` officially. GitHub Actions workflow is configured to install dependencies using `npm`. When using deprecated version of `npm` we can not fully ensure the versions of installed dependencies which might lead to unanticipated problems.
381
382#### Scripts
383
384We rely on npm run-scripts for integrating with our tooling infrastructure. We use ESLint as a pre-commit hook to ensure code consistency. Moreover, to ease formatting we use [prettier](https://github.com/prettier/prettier) while building the `/dist` assets happens through `rollup`.
385
386These are our npm scripts:
387
388- `npm run dev` to start building while watching sources for changes
389- `npm run test` to run our test suite via jsdom and karma
390 - `test:jsdom` to only run tests through jsdom
391 - `test:karma` to only run tests through karma
392- `npm run lint` to lint the sources using ESLint (via xo)
393- `npm run format` to format our sources using prettier to ease to pass ESLint
394- `npm run build` to build our distribution assets minified and unminified as a UMD module
395 - `npm run build:umd` to only build an unminified UMD module
396 - `npm run build:umd:min` to only build a minified UMD module
397
398Note: all run scripts triggered via `npm run <script>`.
399
400There are more npm scripts but they are mainly to integrate with CI or are meant to be "private" for instance to amend build distribution files with every commit.
401
402## Security Mailing List
403
404We maintain a mailing list that notifies whenever a security-critical release of DOMPurify was published. This means, if someone found a bypass and we fixed it with a release (which always happens when a bypass was found) a mail will go out to that list. This usually happens within minutes or few hours after learning about a bypass. The list can be subscribed to here:
405
406[https://lists.ruhr-uni-bochum.de/mailman/listinfo/dompurify-security](https://lists.ruhr-uni-bochum.de/mailman/listinfo/dompurify-security)
407
408Feature releases will not be announced to this list.
409
410## Who contributed?
411
412Many people helped and help DOMPurify become what it is and need to be acknowledged here!
413
414[JGraph 💸](https://github.com/jgraph), [GitHub 💸](https://github.com/github), [CynegeticIO 💸](https://github.com/CynegeticIO), [Sentry 💸](https://github.com/getsentry), [jarrodldavis 💸](https://github.com/jarrodldavis), [kevin_mizu](https://twitter.com/kevin_mizu), [GrantGryczan](https://github.com/GrantGryczan), [Lowdefy 💸](https://twitter.com/lowdefy), [granlem ](https://twitter.com/MaximeVeit), [oreoshake ](https://github.com/oreoshake), [dcramer 💸](https://github.com/dcramer),[tdeekens ❤️](https://github.com/tdeekens), [peernohell ❤️](https://github.com/peernohell), [is2ei](https://github.com/is2ei), [SoheilKhodayari](https://github.com/SoheilKhodayari), [franktopel](https://github.com/franktopel), [NateScarlet](https://github.com/NateScarlet), [neilj](https://github.com/neilj), [fhemberger](https://github.com/fhemberger), [Joris-van-der-Wel](https://github.com/Joris-van-der-Wel), [ydaniv](https://github.com/ydaniv), [terjanq](https://twitter.com/terjanq), [filedescriptor](https://github.com/filedescriptor), [ConradIrwin](https://github.com/ConradIrwin), [gibson042](https://github.com/gibson042), [choumx](https://github.com/choumx), [0xSobky](https://github.com/0xSobky), [styfle](https://github.com/styfle), [koto](https://github.com/koto), [tlau88](https://github.com/tlau88), [strugee](https://github.com/strugee), [oparoz](https://github.com/oparoz), [mathiasbynens](https://github.com/mathiasbynens), [edg2s](https://github.com/edg2s), [dnkolegov](https://github.com/dnkolegov), [dhardtke](https://github.com/dhardtke), [wirehead](https://github.com/wirehead), [thorn0](https://github.com/thorn0), [styu](https://github.com/styu), [mozfreddyb](https://github.com/mozfreddyb), [mikesamuel](https://github.com/mikesamuel), [jorangreef](https://github.com/jorangreef), [jimmyhchan](https://github.com/jimmyhchan), [jameydeorio](https://github.com/jameydeorio), [jameskraus](https://github.com/jameskraus), [hyderali](https://github.com/hyderali), [hansottowirtz](https://github.com/hansottowirtz), [hackvertor](https://github.com/hackvertor), [freddyb](https://github.com/freddyb), [flavorjones](https://github.com/flavorjones), [djfarrelly](https://github.com/djfarrelly), [devd](https://github.com/devd), [camerondunford](https://github.com/camerondunford), [buu700](https://github.com/buu700), [buildog](https://github.com/buildog), [alabiaga](https://github.com/alabiaga), [Vector919](https://github.com/Vector919), [Robbert](https://github.com/Robbert), [GreLI](https://github.com/GreLI), [FuzzySockets](https://github.com/FuzzySockets), [ArtemBernatskyy](https://github.com/ArtemBernatskyy), [@garethheyes](https://twitter.com/garethheyes), [@shafigullin](https://twitter.com/shafigullin), [@mmrupp](https://twitter.com/mmrupp), [@irsdl](https://twitter.com/irsdl),[ShikariSenpai](https://github.com/ShikariSenpai), [ansjdnakjdnajkd](https://github.com/ansjdnakjdnajkd), [@asutherland](https://twitter.com/asutherland), [@mathias](https://twitter.com/mathias), [@cgvwzq](https://twitter.com/cgvwzq), [@robbertatwork](https://twitter.com/robbertatwork), [@giutro](https://twitter.com/giutro), [@CmdEngineer\_](https://twitter.com/CmdEngineer_), [@avr4mit](https://twitter.com/avr4mit) and especially [@securitymb ❤️](https://twitter.com/securitymb) & [@masatokinugawa ❤️](https://twitter.com/masatokinugawa)
415
416## Testing powered by
417
418<a target="_blank" href="https://www.browserstack.com/"><img width="200" src="https://www.browserstack.com/images/layout/browserstack-logo-600x315.png"></a><br>
419
420And last but not least, thanks to [BrowserStack Open-Source Program](https://www.browserstack.com/open-source) for supporting this project with their services for free and delivering excellent, dedicated and very professional support on top of that.