UNPKG

25.4 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 2.4.1.
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
13Our 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.
14
15DOMPurify 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.
16
17## What does it do?
18
19DOMPurify 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.
20
21## How do I use it?
22
23It's easy. Just include DOMPurify on your website.
24
25### Using the unminified development version
26
27```html
28<script type="text/javascript" src="src/purify.js"></script>
29```
30
31### Using the minified and tested production version (source-map available)
32
33```html
34<script type="text/javascript" src="dist/purify.min.js"></script>
35```
36
37Afterwards you can sanitize strings by executing the following code:
38
39```js
40let clean = DOMPurify.sanitize(dirty);
41```
42
43Or maybe this, if you love working with Angular or alike:
44
45```js
46import * as DOMPurify from 'dompurify';
47
48let clean = DOMPurify.sanitize('<b>hello there</b>');
49```
50
51The resulting HTML can be written into a DOM element using `innerHTML` or the DOM using `document.write()`. That is fully up to you.
52Note 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:
53
54```js
55let clean = DOMPurify.sanitize(dirty, { USE_PROFILES: { html: true } });
56```
57
58### Where are the TypeScript type definitions?
59
60They can be found here: [@types/dompurify](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/dompurify)
61
62### Is there any foot-gun potential?
63
64Well, 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.
65
66### Okay, makes sense, let's move on
67
68After 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.
69
70### Running DOMPurify on the server
71
72DOMPurify technically also works server-side with Node.js. Our support strives to follow the [Node.js release cycle](https://nodejs.org/en/about/releases/).
73
74Running 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_.
75
76Why? 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.
77
78Other 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:
79
80```bash
81npm install dompurify
82npm install jsdom
83```
84
85For _jsdom_ (please use an up-to-date version), this should do the trick:
86
87```js
88const createDOMPurify = require('dompurify');
89const { JSDOM } = require('jsdom');
90
91const window = new JSDOM('').window;
92const DOMPurify = createDOMPurify(window);
93const clean = DOMPurify.sanitize('<b>hello there</b>');
94```
95
96Or even this, if you prefer working with imports:
97
98```js
99import { JSDOM } from 'jsdom';
100import DOMPurify from 'dompurify';
101
102const window = new JSDOM('').window;
103const purify = DOMPurify(window);
104const clean = purify.sanitize('<b>hello there</b>');
105```
106
107If 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.
108
109```bash
110npm install isomorphic-dompurify
111```
112
113```js
114import DOMPurify from 'isomorphic-dompurify';
115
116const clean = DOMPurify.sanitize('<s>hello</s>');
117```
118
119## Is there a demo?
120
121Of course there is a demo! [Play with DOMPurify](https://cure53.de/purify)
122
123## What if I find a _security_ bug?
124
125First 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)
126
127Also, 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/).
128
129## Some purification samples please?
130
131How 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!
132
133```js
134DOMPurify.sanitize('<img src=x onerror=alert(1)//>'); // becomes <img src="x">
135DOMPurify.sanitize('<svg><g/onload=alert(2)//<p>'); // becomes <svg><g></g></svg>
136DOMPurify.sanitize('<p>abc<iframe//src=jAva&Tab;script:alert(3)>def</p>'); // becomes <p>abc</p>
137DOMPurify.sanitize('<math><mi//xlink:href="data:x,<script>alert(4)</script>">'); // becomes <math><mi></mi></math>
138DOMPurify.sanitize('<TABLE><tr><td>HELLO</tr></TABL>'); // becomes <table><tbody><tr><td>HELLO</td></tr></tbody></table>
139DOMPurify.sanitize('<UL><li><A HREF=//google.com>click</UL>'); // becomes <ul><li><a href="//google.com">click</a></li></ul>
140```
141
142## What is supported?
143
144DOMPurify 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.
145
146## What about older browsers like MSIE8?
147
148DOMPurify 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.
149
150If not even `toStaticHTML` is supported, DOMPurify does nothing at all. It simply returns exactly the string that you fed it.
151
152DOMPurify also exposes a property called `isSupported`, which tells you whether DOMPurify will be able to do its job.
153
154## What about DOMPurify and Trusted Types?
155
156In version 1.0.9, support for [Trusted Types API](https://github.com/w3c/webappsec-trusted-types) was added to DOMPurify.
157In version 2.0.0, a config flag was added to control DOMPurify's behavior regarding this.
158
159When `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).
160
161## Can I configure DOMPurify?
162
163Yes. 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).
164
165```js
166/**
167 * General settings
168 */
169
170// strip {{ ... }}, ${ ... } and <% ... %> to make output safe for template systems
171// be careful please, this mode is not recommended for production usage.
172// allowing template parsing in user-controlled HTML is not advised at all.
173// only use this mode if there is really no alternative.
174var clean = DOMPurify.sanitize(dirty, {SAFE_FOR_TEMPLATES: true});
175
176/**
177 * Control our allow-lists and block-lists
178 */
179// allow only <b> elements, very strict
180var clean = DOMPurify.sanitize(dirty, {ALLOWED_TAGS: ['b']});
181
182// allow only <b> and <q> with style attributes
183var clean = DOMPurify.sanitize(dirty, {ALLOWED_TAGS: ['b', 'q'], ALLOWED_ATTR: ['style']});
184
185// allow all safe HTML elements but neither SVG nor MathML
186// note that the USE_PROFILES setting will override the ALLOWED_TAGS setting
187// so don't use them together
188var clean = DOMPurify.sanitize(dirty, {USE_PROFILES: {html: true}});
189
190// allow all safe SVG elements and SVG Filters, no HTML or MathML
191var clean = DOMPurify.sanitize(dirty, {USE_PROFILES: {svg: true, svgFilters: true}});
192
193// allow all safe MathML elements and SVG, but no SVG Filters
194var clean = DOMPurify.sanitize(dirty, {USE_PROFILES: {mathMl: true, svg: true}});
195
196// change the default namespace from HTML to something different
197var clean = DOMPurify.sanitize(dirty, {NAMESPACE: 'http://www.w3.org/2000/svg'});
198
199// leave all safe HTML as it is and add <style> elements to block-list
200var clean = DOMPurify.sanitize(dirty, {FORBID_TAGS: ['style']});
201
202// leave all safe HTML as it is and add style attributes to block-list
203var clean = DOMPurify.sanitize(dirty, {FORBID_ATTR: ['style']});
204
205// extend the existing array of allowed tags and add <my-tag> to allow-list
206var clean = DOMPurify.sanitize(dirty, {ADD_TAGS: ['my-tag']});
207
208// extend the existing array of allowed attributes and add my-attr to allow-list
209var clean = DOMPurify.sanitize(dirty, {ADD_ATTR: ['my-attr']});
210
211// prohibit ARIA attributes, leave other safe HTML as is (default is true)
212var clean = DOMPurify.sanitize(dirty, {ALLOW_ARIA_ATTR: false});
213
214// prohibit HTML5 data attributes, leave other safe HTML as is (default is true)
215var clean = DOMPurify.sanitize(dirty, {ALLOW_DATA_ATTR: false});
216
217/**
218 * Control behavior relating to Custom Elements
219 */
220
221// DOMPurify allows to define rules for Custom Elements. When using the CUSTOM_ELEMENT_HANDLING
222// literal, it is possible to define exactly what elements you wish to allow (by default, none are allowed).
223//
224// The same goes for their attributes. By default, the built-in or configured allow.list is used.
225//
226// You can use a RegExp literal to specify what is allowed or a predicate, examples for both can be seen below.
227// The default values are very restrictive to prevent accidental XSS bypasses. Handle with great care!
228
229
230var clean = DOMPurify.sanitize(
231 '<foo-bar baz="foobar" forbidden="true"></foo-bar><div is="foo-baz"></div>',
232 {
233 CUSTOM_ELEMENT_HANDLING: {
234 tagNameCheck: null, // no custom elements are allowed
235 attributeNameCheck: null, // default / standard attribute allow-list is used
236 allowCustomizedBuiltInElements: false, // no customized built-ins allowed
237 },
238 }
239); // <div is=""></div>
240
241var clean = DOMPurify.sanitize(
242 '<foo-bar baz="foobar" forbidden="true"></foo-bar><div is="foo-baz"></div>',
243 {
244 CUSTOM_ELEMENT_HANDLING: {
245 tagNameCheck: /^foo-/, // allow all tags starting with "foo-"
246 attributeNameCheck: /baz/, // allow all attributes containing "baz"
247 allowCustomizedBuiltInElements: false, // customized built-ins are allowed
248 },
249 }
250); // <foo-bar baz="foobar"></foo-bar><div is=""></div>
251
252var clean = DOMPurify.sanitize(
253 '<foo-bar baz="foobar" forbidden="true"></foo-bar><div is="foo-baz"></div>',
254 {
255 CUSTOM_ELEMENT_HANDLING: {
256 tagNameCheck: (tagName) => tagName.match(/^foo-/), // allow all tags starting with "foo-"
257 attributeNameCheck: (attr) => attr.match(/baz/), // allow all containing "baz"
258 allowCustomizedBuiltInElements: true, // allow customized built-ins
259 },
260 }
261); // <foo-bar baz="foobar"></foo-bar><div is="foo-baz"></div>
262
263/**
264 * Control behavior relating to URI values
265 */
266// extend the existing array of elements that can use Data URIs
267var clean = DOMPurify.sanitize(dirty, {ADD_DATA_URI_TAGS: ['a', 'area']});
268
269// extend the existing array of elements that are safe for URI-like values (be careful, XSS risk)
270var clean = DOMPurify.sanitize(dirty, {ADD_URI_SAFE_ATTR: ['my-attr']});
271
272/**
273 * Control permitted attribute values
274 */
275// allow external protocol handlers in URL attributes (default is false, be careful, XSS risk)
276// by default only http, https, ftp, ftps, tel, mailto, callto, cid and xmpp are allowed.
277var clean = DOMPurify.sanitize(dirty, {ALLOW_UNKNOWN_PROTOCOLS: true});
278
279// allow specific protocols handlers in URL attributes via regex (default is false, be careful, XSS risk)
280// by default only http, https, ftp, ftps, tel, mailto, callto, cid and xmpp are allowed.
281// Default RegExp: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
282var clean = DOMPurify.sanitize(dirty, {ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|xxx):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;});
283
284/**
285 * Influence the return-type
286 */
287// return a DOM HTMLBodyElement instead of an HTML string (default is false)
288var clean = DOMPurify.sanitize(dirty, {RETURN_DOM: true});
289
290// return a DOM DocumentFragment instead of an HTML string (default is false)
291var clean = DOMPurify.sanitize(dirty, {RETURN_DOM_FRAGMENT: true});
292
293// use the RETURN_TRUSTED_TYPE flag to turn on Trusted Types support if available
294var clean = DOMPurify.sanitize(dirty, {RETURN_TRUSTED_TYPE: true}); // will return a TrustedHTML object instead of a string if possible
295
296/**
297 * Influence how we sanitize
298 */
299// return entire document including <html> tags (default is false)
300var clean = DOMPurify.sanitize(dirty, {WHOLE_DOCUMENT: true});
301
302// disable DOM Clobbering protection on output (default is true, handle with care, minor XSS risks here)
303var clean = DOMPurify.sanitize(dirty, {SANITIZE_DOM: false});
304
305// enforce strict DOM Clobbering protection via namespace isolation (default is false)
306// when enabled, isolates the namespace of named properties (i.e., `id` and `name` attributes)
307// from JS variables by prefixing them with the string `user-content-`
308var clean = DOMPurify.sanitize(dirty, {SANITIZE_NAMED_PROPS: true});
309
310// keep an element's content when the element is removed (default is true)
311var clean = DOMPurify.sanitize(dirty, {KEEP_CONTENT: false});
312
313// glue elements like style, script or others to document.body and prevent unintuitive browser behavior in several edge-cases (default is false)
314var clean = DOMPurify.sanitize(dirty, {FORCE_BODY: true});
315
316// remove all <a> elements under <p> elements that are removed
317var clean = DOMPurify.sanitize(dirty, {FORBID_CONTENTS: ['a'], FORBID_TAGS: ['p']});
318
319// change the parser type so sanitized data is treated as XML and not as HTML, which is the default
320var clean = DOMPurify.sanitize(dirty, {PARSER_MEDIA_TYPE: 'application/xhtml+xml'});
321
322/**
323 * Influence where we sanitize
324 */
325// use the IN_PLACE mode to sanitize a node "in place", which is much faster depending on how you use DOMPurify
326var dirty = document.createElement('a');
327dirty.setAttribute('href', 'javascript:alert(1)');
328var clean = DOMPurify.sanitize(dirty, {IN_PLACE: true}); // see https://github.com/cure53/DOMPurify/issues/288 for more info
329```
330
331There 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.
332
333## Persistent Configuration
334
335Instead 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.
336
337## Hooks
338
339DOMPurify allows you to augment its functionality by attaching one or more functions with the `DOMPurify.addHook` method to one of the following hooks:
340
341- `beforeSanitizeElements`
342- `uponSanitizeElement` (No 's' - called for every element)
343- `afterSanitizeElements`
344- `beforeSanitizeAttributes`
345- `uponSanitizeAttribute`
346- `afterSanitizeAttributes`
347- `beforeSanitizeShadowDOM`
348- `uponSanitizeShadowNode`
349- `afterSanitizeShadowDOM`
350
351It 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.
352
353_Example_:
354
355```js
356DOMPurify.addHook(
357 'beforeSanitizeElements',
358 function (currentNode, hookEvent, config) {
359 // Do something with the current node and return it
360 // You can also mutate hookEvent (i.e. set hookEvent.forceKeepAttr = true)
361 return currentNode;
362 }
363);
364```
365
366## Continuous Integration
367
368We 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
369
370You can further run local tests by executing `npm test`. The tests work fine with Node.js v0.6.2 and jsdom@8.5.0.
371
372All relevant commits will be signed with the key `0x24BB6BF4` for additional security (since 8th of April 2016).
373
374### Development and contributing
375
376#### Installation (`npm i`)
377
378We 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.
379
380#### Scripts
381
382We 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`.
383
384These are our npm scripts:
385
386- `npm run dev` to start building while watching sources for changes
387- `npm run test` to run our test suite via jsdom and karma
388 - `test:jsdom` to only run tests through jsdom
389 - `test:karma` to only run tests through karma
390- `npm run lint` to lint the sources using ESLint (via xo)
391- `npm run format` to format our sources using prettier to ease to pass ESLint
392- `npm run build` to build our distribution assets minified and unminified as a UMD module
393 - `npm run build:umd` to only build an unminified UMD module
394 - `npm run build:umd:min` to only build a minified UMD module
395
396Note: all run scripts triggered via `npm run <script>`.
397
398There 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.
399
400## Security Mailing List
401
402We 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:
403
404[https://lists.ruhr-uni-bochum.de/mailman/listinfo/dompurify-security](https://lists.ruhr-uni-bochum.de/mailman/listinfo/dompurify-security)
405
406Feature releases will not be announced to this list.
407
408## Who contributed?
409
410Many people helped and help DOMPurify become what it is and need to be acknowledged here!
411
412[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), [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)
413
414## Testing powered by
415
416<a target="_blank" href="https://www.browserstack.com/"><img width="200" src="https://www.browserstack.com/images/layout/browserstack-logo-600x315.png"></a><br>
417
418And 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.