Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS: RegExp unknown flags support and enhanced compatibility with RegExp objects #18089

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
178da21
JS: Added test case for CWE-178 RegExp with unknown flags
Napalys Nov 25, 2024
e38b63e
JS: previously js/case-sensitive-middleware-path was not taking into …
Napalys Nov 25, 2024
d6372ae
Update javascript/ql/src/Security/CWE-178/CaseSensitiveMiddlewarePath.ql
Napalys Nov 25, 2024
41f21d4
JS: Added test case which is not flagged but should be abusing new Re…
Napalys Nov 26, 2024
38be0e4
JS: Now BadHtmlSanitizers also flags new RegExp as potential issue
Napalys Nov 26, 2024
89f3b6f
JS: Added test case for bad sanitizer with unknown flags, currently n…
Napalys Nov 26, 2024
18c7b18
JS: Now BadHtmlSanitizers new RegExp with unknown flags is also flagged.
Napalys Nov 26, 2024
41fef0f
JS: Added test cases which cover new RegExp creation with replace on …
Napalys Nov 26, 2024
faef9dd
JS: protyte poluting now treats unknownFlags as potentially good sani…
Napalys Nov 26, 2024
7db6f7c
JS: Added test cases with new RegExp for Tainted paths, currently wor…
Napalys Nov 26, 2024
eca7a88
JS: Fixed docs description
Napalys Nov 26, 2024
23b18ae
JS: Now unknown flags are not flagged in taint paths
Napalys Nov 26, 2024
155f1fc
JS: Added test cases for unsafe shell command sanitization with RegEx…
Napalys Nov 26, 2024
a0df33c
JS: UnsafeShellCommand Using unknown flags in the RegExp object is no…
Napalys Nov 26, 2024
aa557cf
JS: Added tests for DotRemovingReplaceCall with RegExp Object.
Napalys Nov 27, 2024
875478c
JS: Fixed path query not flagging new RegExp with DotRemovingReplaceCall
Napalys Nov 27, 2024
9c2366a
JS: Added tests for ReDos with unknownFlags, everything seems to be good
Napalys Nov 27, 2024
7631803
JS: Add test cases for RegExp object usage in replace within incomple…
Napalys Nov 27, 2024
1ae1748
JS: incomplete sanitization now also works with RegExp objects
Napalys Nov 27, 2024
98fd977
JS: imcomplete sanization now handles properly maybe global
Napalys Nov 27, 2024
fe28657
JS: add test cases with unknown flags for double escaping, works as e…
Napalys Nov 27, 2024
dbae553
JS: add xss test cases with unknownflags for replace using RegExp
Napalys Nov 27, 2024
c71778f
JS: xss does not flag anymore replace with RegExp unknown flags
Napalys Nov 27, 2024
1ca57cf
JS: add test cases with RegExp object for MaskingReplacer, currently …
Napalys Nov 27, 2024
a2c4674
JS: fixed issue where MaskingReplacer would work only with regexp lit…
Napalys Nov 27, 2024
e673348
JS: now RegExp with unknown flags is not flagged as an issue within p…
Napalys Nov 27, 2024
62194f5
JS: add test cases RegExp with unknown flags
Napalys Nov 27, 2024
1d2e08a
JS: now Reg Exp injection treats unknownFlags as sanitization, Metach…
Napalys Nov 27, 2024
9a1c1f4
JS: Added in RegExpCreationNode maybeGlobal predicate for more conven…
Napalys Nov 27, 2024
fd77360
Added change notes
Napalys Nov 28, 2024
9ca0fe4
Update RegExp handling and add test case
Napalys Nov 28, 2024
d2de9a2
Fixed change notes
Napalys Nov 28, 2024
13afd63
Update javascript/ql/lib/change-notes/2024-11-28-regexp-unknown-flags.md
Napalys Nov 29, 2024
3171f38
JS: fixed bad alert messages when it came to incomplete sanitization …
Napalys Nov 29, 2024
9d4e737
JS: follow proper code standards for get predicates
Napalys Nov 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
category: majorAnalysis
---
* Queries working with regular expressions now also handle regular expressions constructed using `new RegExp(..)`. Previously some queries didn't handle such regular expressions.
* Regular expression related queries now account for unknown flags.
6 changes: 6 additions & 0 deletions javascript/ql/lib/semmle/javascript/StandardLibrary.qll
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ class StringReplaceCall extends DataFlow::MethodCallNode {
*/
predicate isGlobal() { this.getRegExp().isGlobal() or this.getMethodName() = "replaceAll" }

/**
* Holds if this is a global replacement, that is, the first argument is a regular expression
* with the `g` flag or unknown flags, or this is a call to `.replaceAll()`.
*/
predicate maybeGlobal() { this.getRegExp().maybeGlobal() or this.getMethodName() = "replaceAll" }

/**
* Holds if this call to `replace` replaces `old` with `new`.
*/
Expand Down
3 changes: 3 additions & 0 deletions javascript/ql/lib/semmle/javascript/dataflow/Nodes.qll
Original file line number Diff line number Diff line change
Expand Up @@ -1685,6 +1685,9 @@ class RegExpCreationNode extends DataFlow::SourceNode {
/** Holds if the constructed predicate has the `g` flag. */
predicate isGlobal() { RegExp::isGlobal(this.getFlags()) }

/** Holds if the constructed predicate has the `g` flag or unknown flags. */
predicate maybeGlobal() { RegExp::maybeGlobal(this.tryGetFlags()) }

/** Gets a data flow node referring to this regular expression. */
private DataFlow::SourceNode getAReference(DataFlow::TypeTracker t) {
t.start() and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private StringReplaceCall getAStringReplaceMethodCall(StringReplaceCall n) {
module HtmlSanitization {
private predicate fixedGlobalReplacement(StringReplaceCallSequence chain) {
forall(StringReplaceCall member | member = chain.getAMember() |
member.isGlobal() and member.getArgument(0) instanceof DataFlow::RegExpLiteralNode
member.maybeGlobal() and member.getArgument(0) instanceof DataFlow::RegExpCreationNode
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ module CleartextLogging {
*/
class MaskingReplacer extends Barrier, StringReplaceCall {
MaskingReplacer() {
this.isGlobal() and
this.maybeGlobal() and
exists(this.getRawReplacement().getStringValue()) and
any(RegExpDot term).getLiteral() = this.getRegExp().asExpr()
exists(DataFlow::RegExpCreationNode regexpObj |
this.(StringReplaceCall).getRegExp() = regexpObj and
regexpObj.getRoot() = any(RegExpDot term).getRootTerm()
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class Configuration extends TaintTracking::Configuration {
// Replacing with "_" is likely to be exploitable
not replace.getRawReplacement().getStringValue() = "_" and
(
replace.isGlobal()
replace.maybeGlobal()
or
// Non-global replace with a non-empty string can also prevent __proto__ by
// inserting a chunk of text that doesn't fit anywhere in __proto__
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ module RegExpInjection {
*/
class MetacharEscapeSanitizer extends Sanitizer, StringReplaceCall {
MetacharEscapeSanitizer() {
this.isGlobal() and
this.maybeGlobal() and
(
RegExp::alwaysMatchesMetaCharacter(this.getRegExp().getRoot(), ["{", "[", "+"])
or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,10 @@ module TaintedPath {
this instanceof StringReplaceCall and
input = this.getReceiver() and
output = this and
not exists(RegExpLiteral literal, RegExpTerm term |
this.(StringReplaceCall).getRegExp().asExpr() = literal and
this.(StringReplaceCall).isGlobal() and
literal.getRoot() = term
not exists(DataFlow::RegExpCreationNode regexp, RegExpTerm term |
this.(StringReplaceCall).getRegExp() = regexp and
this.(StringReplaceCall).maybeGlobal() and
regexp.getRoot() = term
|
term.getAMatchedString() = "/" or
term.getAMatchedString() = "." or
Expand Down Expand Up @@ -305,9 +305,9 @@ module TaintedPath {
input = this.getReceiver() and
output = this and
this.isGlobal() and
exists(RegExpLiteral literal, RegExpTerm term |
this.getRegExp().asExpr() = literal and
literal.getRoot() = term and
exists(DataFlow::RegExpCreationNode regexp, RegExpTerm term |
this.getRegExp() = regexp and
regexp.getRoot() = term and
not term.getAMatchedString() = "/"
|
term.getAMatchedString() = "." or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ module UnsafeShellCommandConstruction {
class ReplaceQuotesSanitizer extends Sanitizer, StringReplaceCall {
ReplaceQuotesSanitizer() {
this.getAReplacedString() = "'" and
this.isGlobal() and
this.maybeGlobal() and
this.getRawReplacement().mayHaveStringValue(["'\\''", ""])
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ module Shared {
*/
class MetacharEscapeSanitizer extends Sanitizer, StringReplaceCall {
MetacharEscapeSanitizer() {
this.isGlobal() and
this.maybeGlobal() and
(
RegExp::alwaysMatchesMetaCharacter(this.getRegExp().getRoot(), ["<", "'", "\""])
or
Expand Down
21 changes: 15 additions & 6 deletions javascript/ql/src/Security/CWE-116/IncompleteSanitization.ql
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

/** Gets a string matched by `e` in a `replace` call. */
string getAMatchedString(DataFlow::Node e) {
result = e.(DataFlow::RegExpLiteralNode).getRoot().getAMatchedString()
result = e.(DataFlow::RegExpCreationNode).getRoot().getAMatchedString()
or
result = e.getStringValue()
}
Expand Down Expand Up @@ -52,8 +52,8 @@
* Holds if `mce` is of the form `x.replace(re, new)`, where `re` is a global
* regular expression and `new` prefixes the matched string with a backslash.
*/
predicate isBackslashEscape(StringReplaceCall mce, DataFlow::RegExpLiteralNode re) {
mce.isGlobal() and
predicate isBackslashEscape(StringReplaceCall mce, DataFlow::RegExpCreationNode re) {
mce.maybeGlobal() and
re = mce.getRegExp() and
(
// replacement with `\$&`, `\$1` or similar
Expand All @@ -72,7 +72,7 @@
nd instanceof JsonStringifyCall
or
// check whether `nd` itself escapes backslashes
exists(DataFlow::RegExpLiteralNode rel | isBackslashEscape(nd, rel) |
exists(DataFlow::RegExpCreationNode rel | isBackslashEscape(nd, rel) |
// if it's a complex regexp, we conservatively assume that it probably escapes backslashes
not isSimple(rel.getRoot()) or
getAMatchedString(rel) = "\\"
Expand Down Expand Up @@ -143,12 +143,21 @@
)
}

/**
* Gets a nice string representation of the pattern or value of the node.
*/
string getPatternOrValueString(DataFlow::Node node) {
if node instanceof DataFlow::RegExpConstructorInvokeNode
then result = "/" + node.(DataFlow::RegExpConstructorInvokeNode).getRoot() + "/"
else result = node.toString()
Napalys marked this conversation as resolved.
Show resolved Hide resolved
}

from StringReplaceCall repl, DataFlow::Node old, string msg
where
(old = repl.getArgument(0) or old = repl.getRegExp()) and
(
not repl.isGlobal() and
msg = "This replaces only the first occurrence of " + old + "." and
not repl.maybeGlobal() and
msg = "This replaces only the first occurrence of " + getPatternOrValueString(old) + "." and
// only flag if this is likely to be a sanitizer or URL encoder or decoder
exists(string m | m = getAMatchedString(old) |
// sanitizer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ predicate isCaseSensitiveMiddleware(
arg = call.getArgument(0) and
regexp.getAReference().flowsTo(arg) and
exists(string flags |
flags = regexp.getFlags() and
not RegExp::isIgnoreCase(flags)
flags = regexp.tryGetFlags() and
not RegExp::maybeIgnoreCase(flags)
)
)
}
Expand Down
Loading