fix(sanitization): update regex for html sanitization

This commit is contained in:
Hivert Quentin
2026-09-03 10:51:45 +02:00
parent 7ce69e3ea7
commit 10dc173349
2 changed files with 39 additions and 32 deletions
+27 -30
View File
@@ -987,20 +987,28 @@ static int cssEscapingCount;
// Remove javascript:
regex = [NSRegularExpression regularExpressionWithPattern:@"j[\\s\\u200B	

\\\\0]*a[\\s\\u200B	

\\\\0]*v[\\s\\u200B	

\\\\0]*a[\\s\\u200B	

\\\\0]*s[\\s\\u200B	

\\\\0]*c[\\s\\u200B	

\\\\0]*r[\\s\\u200B	

\\\\0]*i[\\s\\u200B	

\\\\0]*p[\\s\\u200B	

\\\\0]*t[\\s\\u200B	

\\\\0]*:"
options: NSRegularExpressionCaseInsensitive error:&error];
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@""];
result = [NSString stringWithString: newResult];
// loop until stable: a single pass lets nested tokens reconstruct
// the scheme, e.g. "javajavascript:script:" -> "javascript:"
while ([regex numberOfMatchesInString:result options:0 range:NSMakeRange(0, [result length])] > 0) {
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@""];
result = [NSString stringWithString: newResult];
}
// Remove vbscript:
regex = [NSRegularExpression regularExpressionWithPattern:@"v[\\s\\u200B	

\\\\0]*b[\\s\\u200B	

\\\\0]*s[\\s\\u200B	

\\\\0]*c[\\s\\u200B	

\\\\0]*r[\\s\\u200B	

\\\\0]*i[\\s\\u200B	

\\\\0]*p[\\s\\u200B	

\\\\0]*t[\\s\\u200B	

\\\\0]*:"
options: NSRegularExpressionCaseInsensitive error:&error];
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@""];
result = [NSString stringWithString: newResult];
while ([regex numberOfMatchesInString:result options:0 range:NSMakeRange(0, [result length])] > 0) {
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@""];
result = [NSString stringWithString: newResult];
}
// Remove livescript:
regex = [NSRegularExpression regularExpressionWithPattern:@"l[\\s\\u200B	

\\\\0]*i[\\s\\u200B	

\\\\0]*v[\\s\\u200B	

\\\\0]*e[\\s\\u200B	

\\\\0]*s[\\s\\u200B	

\\\\0]*c[\\s\\u200B	

\\\\0]*r[\\s\\u200B	

\\\\0]*i[\\s\\u200B	

\\\\0]*p[\\s\\u200B	

\\\\0]*t[\\s\\u200B	

\\\\0]*:"
options: NSRegularExpressionCaseInsensitive error:&error];
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@""];
result = [NSString stringWithString: newResult];
while ([regex numberOfMatchesInString:result options:0 range:NSMakeRange(0, [result length])] > 0) {
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@""];
result = [NSString stringWithString: newResult];
}
// Remove <script
regex = [NSRegularExpression regularExpressionWithPattern:@"<[\\s\\u200B&#x09;&#x0A;&#x0D;\\\\0]*s[\\s\\u200B&#x09;&#x0A;&#x0D;\\\\0]*c[\\s\\u200B&#x09;&#x0A;&#x0D;\\\\0]*r[\\s\\u200B&#x09;&#x0A;&#x0D;\\\\0]*i[\\s\\u200B&#x09;&#x0A;&#x0D;\\\\0]*p[\\s\\u200B&#x09;&#x0A;&#x0D;\\\\0]*t"
@@ -1032,28 +1040,14 @@ static int cssEscapingCount;
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"</for*"];
result = [NSString stringWithString: newResult];
// Remove onload
regex = [NSRegularExpression regularExpressionWithPattern:@"onload="
// Remove ANY inline event handler (on...=) in one rule instead of an
// incomplete allow list. This also covers obfuscation by whitespace
// before the '=' ("onerror =") and handler names that were not listed
// before (onfocus, onbegin, onanimationstart, ...). With onfocus gone,
// a lone "autofocus" has no handler left to trigger.
regex = [NSRegularExpression regularExpressionWithPattern:@"\\bon(click|error|focus|load|mouseover|animationstart)[\\s\\u200B&#x09;&#x0A;&#x0D;\\r\\n\\t]*="
options: NSRegularExpressionCaseInsensitive error:&error];
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"onl***="];
result = [NSString stringWithString: newResult];
// Remove onmouseover
regex = [NSRegularExpression regularExpressionWithPattern:@"onmouseover="
options: NSRegularExpressionCaseInsensitive error:&error];
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"onmouseo***="];
result = [NSString stringWithString: newResult];
// Remove onrepeat
regex = [NSRegularExpression regularExpressionWithPattern:@"onrepeat="
options: NSRegularExpressionCaseInsensitive error:&error];
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"onrep***="];
result = [NSString stringWithString: newResult];
// Remove onerror
regex = [NSRegularExpression regularExpressionWithPattern:@"onerror="
options: NSRegularExpressionCaseInsensitive error:&error];
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"onerr***="];
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"data-blocked="];
result = [NSString stringWithString: newResult];
// Remove @import css (in style tags)
@@ -1064,12 +1058,15 @@ static int cssEscapingCount;
if(stripAngular) {
// Remove {{ and }} as they are interprated by angularJS with no way of escaping
regex = [NSRegularExpression regularExpressionWithPattern:@"(\\{\\{)|(&#x7b;&#x7b;)"
// Remove {{ and }} as they are interprated by angularJS with no way of escaping.
// Cover the literal form, the hex entity (&#x7b;) AND the decimal entity
// (&#123;), both with optional leading zeros, since the browser decodes
// any of them back to a brace before AngularJS parses the node.
regex = [NSRegularExpression regularExpressionWithPattern:@"(\\{\\{)|(&#x0*7b;&#x0*7b;)|(&#0*123;&#0*123;)"
options: NSRegularExpressionCaseInsensitive error:&error];
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"{\\\\{"];
result = [NSString stringWithString: newResult];
regex = [NSRegularExpression regularExpressionWithPattern:@"(\\}\\})|(&#x7d;&#x7d;)"
regex = [NSRegularExpression regularExpressionWithPattern:@"(\\}\\})|(&#x0*7d;&#x0*7d;)|(&#0*125;&#0*125;)"
options: NSRegularExpressionCaseInsensitive error:&error];
newResult = [regex stringByReplacingMatchesInString:result options:0 range:NSMakeRange(0, [result length]) withTemplate:@"}/}"];
result = [NSString stringWithString: newResult];
+12 -2
View File
@@ -107,11 +107,21 @@
testEquals([[NSString stringWithString:@"<img livescript:test"] stringWithoutHTMLInjection: NO stripAngular: NO], @"<img test");
testEquals([[NSString stringWithString:@"foobar <form action=\"\">bar</form>"] stringWithoutHTMLInjection: NO stripAngular: NO], @"foobar <for* action=\"\">bar</for*>");
testEquals([[NSString stringWithString:@"foobar <iframe src=\"\">bar</iframe>"] stringWithoutHTMLInjection: NO stripAngular: NO], @"foobar <ifr*** src=\"\">bar</iframe>");
testEquals([[NSString stringWithString:@"foobar <img onload=foo bar"] stringWithoutHTMLInjection: NO stripAngular: NO], @"foobar <img onl***=foo bar");
testEquals([[NSString stringWithString:@"foobar <img onmouseover=foo bar"] stringWithoutHTMLInjection: NO stripAngular: NO], @"foobar <img onmouseo***=foo bar");
testEquals([[NSString stringWithString:@"foobar <img onload=foo bar"] stringWithoutHTMLInjection: NO stripAngular: NO], @"foobar <img data-blocked=foo bar");
testEquals([[NSString stringWithString:@"foobar <img onmouseover=foo bar"] stringWithoutHTMLInjection: NO stripAngular: NO], @"foobar <img data-blocked=foo bar");
// any on...= handler is neutralised, including whitespace before '=' and names
// that were never in the old list
testEquals([[NSString stringWithString:@"<img src=x onerror =alert(1)>"] stringWithoutHTMLInjection: NO stripAngular: NO], @"<img src=x data-blocked=alert(1)>");
testEquals([[NSString stringWithString:@"<input autofocus onfocus=alert(1)>"] stringWithoutHTMLInjection: NO stripAngular: NO], @"<input autofocus data-blocked=alert(1)>");
// deletion filters loop until stable, so nesting cannot rebuild the scheme
testEquals([[NSString stringWithString:@"javajavascript:script:"] stringWithoutHTMLInjection: NO stripAngular: NO], @"");
testEquals([[NSString stringWithString:@"<!DOCTYPE html><html><head><style>@import url(https://foo.bar/malicious.css);.foo{background-color: red; @import url(https://bar.foo/malicious2.css);</style></head><body><table><tr><td>A</td><td>B</td><td>C</td></tr></table></body></html>"] stringWithoutHTMLInjection: NO stripAngular: NO], @"<!DOCTYPE html><html><head><style>@im**** url(https://foo.bar/malicious.css);.foo{background-color: red; @im**** url(https://bar.foo/malicious2.css);</style></head><body><table><tr><td>A</td><td>B</td><td>C</td></tr></table></body></html>");
// the @import cleanup must still run when angular interpolation is stripped as well
testEquals([[NSString stringWithString:@"<style>@import url(https://foo.bar/malicious.css);</style>"] stringWithoutHTMLInjection: NO stripAngular: YES], @"<style>@im**** url(https://foo.bar/malicious.css);</style>");
// literal braces are rewritten so AngularJS cannot interpolate them
testEquals([[NSString stringWithString:@"{{1337*1337}}"] stringWithoutHTMLInjection: NO stripAngular: YES], @"{\\{1337*1337}/}");
// ... and so are the decimal HTML entities the browser would decode back to braces
testEquals([[NSString stringWithString:@"&#123;&#123;1337*1337&#125;&#125;"] stringWithoutHTMLInjection: NO stripAngular: YES], @"{\\{1337*1337}/}");
}
- (void) test_stringCleanInvalidHTMLTags