Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2a8a283

Browse files
authored
Merge pull request #4946 from erik-krogh/libRedos
JS: Add library input as source for `js/polynomial-redos`
2 parents 24947f2 + 01900d7 commit 2a8a283

18 files changed

Lines changed: 354 additions & 255 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* The `js/polynomial-redos` query now flags uses of expensive regular expressions where the source is library input.

javascript/ql/src/Performance/PolynomialReDoS.ql

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/**
22
* @name Polynomial regular expression used on uncontrolled data
33
* @description A regular expression that can require polynomial time
4-
* to match user-provided values may be
5-
* vulnerable to denial-of-service attacks.
4+
* to match may be vulnerable to denial-of-service attacks.
65
* @kind path-problem
76
* @problem.severity warning
87
* @precision high
@@ -17,12 +16,18 @@ import semmle.javascript.security.performance.PolynomialReDoS::PolynomialReDoS
1716
import semmle.javascript.security.performance.SuperlinearBackTracking
1817
import DataFlow::PathGraph
1918

20-
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
19+
from
20+
Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, Sink sinkNode,
21+
PolynomialBackTrackingTerm regexp
2122
where
2223
cfg.hasFlowPath(source, sink) and
24+
sinkNode = sink.getNode() and
25+
regexp = sinkNode.getRegExp() and
2326
not (
2427
source.getNode().(Source).getKind() = "url" and
25-
sink.getNode().(Sink).getRegExp().(PolynomialBackTrackingTerm).isAtEndLine()
28+
regexp.isAtEndLine()
2629
)
27-
select sink.getNode(), source, sink, "This expensive $@ use depends on $@.",
28-
sink.getNode().(Sink).getRegExp(), "regular expression", source.getNode(), "a user-provided value"
30+
select sinkNode.getHighlight(), source, sink,
31+
"This $@ that depends on $@ may run slow on strings " + regexp.getPrefixMessage() +
32+
"with many repetitions of '" + regexp.getPumpString() + "'.", regexp, "regular expression",
33+
source.getNode(), source.getNode().(Source).describe()

javascript/ql/src/semmle/javascript/PackageExports.qll

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@
66

77
import javascript
88

9+
/**
10+
* Gets a parameter that is a library input to a top-level package.
11+
*/
12+
DataFlow::ParameterNode getALibraryInputParameter() {
13+
exists(int bound, DataFlow::FunctionNode func |
14+
func = getAValueExportedByPackage().getABoundFunctionValue(bound) and
15+
result = func.getParameter(any(int arg | arg >= bound))
16+
)
17+
}
18+
919
/**
1020
* Gets the number of occurrences of "/" in `path`.
1121
*/
@@ -29,33 +39,33 @@ PackageJSON getTopmostPackageJSON() {
2939
}
3040

3141
/**
32-
* Gets a value exported by the main module from the package.json `packageJSON`.
42+
* Gets a value exported by the main module from one of the topmost `package.json` files (see `getTopmostPackageJSON`).
3343
* The value is either directly the `module.exports` value, a nested property of `module.exports`, or a method on an exported class.
3444
*/
35-
DataFlow::Node getAValueExportedBy(PackageJSON packageJSON) {
36-
result = getAnExportFromModule(packageJSON.getMainModule())
45+
private DataFlow::Node getAValueExportedByPackage() {
46+
result = getAnExportFromModule(getTopmostPackageJSON().getMainModule())
3747
or
38-
result = getAValueExportedBy(packageJSON).(DataFlow::PropWrite).getRhs()
48+
result = getAValueExportedByPackage().(DataFlow::PropWrite).getRhs()
3949
or
4050
exists(DataFlow::SourceNode callee |
41-
callee = getAValueExportedBy(packageJSON).(DataFlow::NewNode).getCalleeNode().getALocalSource()
51+
callee = getAValueExportedByPackage().(DataFlow::NewNode).getCalleeNode().getALocalSource()
4252
|
4353
result = callee.getAPropertyRead("prototype").getAPropertyWrite().getRhs()
4454
or
4555
result = callee.(DataFlow::ClassNode).getAnInstanceMethod()
4656
)
4757
or
48-
result = getAValueExportedBy(packageJSON).getALocalSource()
58+
result = getAValueExportedByPackage().getALocalSource()
4959
or
50-
result = getAValueExportedBy(packageJSON).(DataFlow::SourceNode).getAPropertyReference()
60+
result = getAValueExportedByPackage().(DataFlow::SourceNode).getAPropertyReference()
5161
or
5262
exists(Module mod |
53-
mod = getAValueExportedBy(packageJSON).getEnclosingExpr().(Import).getImportedModule()
63+
mod = getAValueExportedByPackage().getEnclosingExpr().(Import).getImportedModule()
5464
|
5565
result = getAnExportFromModule(mod)
5666
)
5767
or
58-
exists(DataFlow::ClassNode cla | cla = getAValueExportedBy(packageJSON) |
68+
exists(DataFlow::ClassNode cla | cla = getAValueExportedByPackage() |
5969
result = cla.getAnInstanceMethod() or
6070
result = cla.getAStaticMethod() or
6171
result = cla.getConstructor()

javascript/ql/src/semmle/javascript/security/dataflow/UnsafeShellCommandConstructionCustomizations.qll

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,7 @@ module UnsafeShellCommandConstruction {
5252
*/
5353
class ExternalInputSource extends Source, DataFlow::ParameterNode {
5454
ExternalInputSource() {
55-
exists(int bound, DataFlow::FunctionNode func |
56-
func =
57-
Exports::getAValueExportedBy(Exports::getTopmostPackageJSON())
58-
.getABoundFunctionValue(bound) and
59-
this = func.getParameter(any(int arg | arg >= bound))
60-
) and
55+
this = Exports::getALibraryInputParameter() and
6156
not this.getName() = ["cmd", "command"] // looks to be on purpose.
6257
}
6358
}

javascript/ql/src/semmle/javascript/security/performance/PolynomialReDoSCustomizations.qll

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,29 @@ module PolynomialReDoS {
1313
*/
1414
abstract class Source extends DataFlow::Node {
1515
/**
16-
* Gets the kind of source that is being accesed. See `HTTP::RequestInputAccess::getKind()`.
17-
* Can be one of "parameter", "header", "body", "url", "cookie".
16+
* Gets the kind of source that is being accesed.
17+
*
18+
* Is either a kind from `HTTP::RequestInputAccess::getKind()`, or "library".
1819
*/
1920
abstract string getKind();
21+
22+
/**
23+
* Gets a string that describes the source.
24+
* For use in the alert message.
25+
*/
26+
string describe() { result = "a user-provided value" }
2027
}
2128

2229
/**
2330
* A data flow sink node for polynomial regular expression denial-of-service vulnerabilities.
2431
*/
2532
abstract class Sink extends DataFlow::Node {
2633
abstract RegExpTerm getRegExp();
34+
35+
/**
36+
* Gets the node to highlight in the alert message.
37+
*/
38+
DataFlow::Node getHighlight() { result = this }
2739
}
2840

2941
/**
@@ -47,9 +59,10 @@ module PolynomialReDoS {
4759
*/
4860
class PolynomialBackTrackingTermUse extends Sink {
4961
PolynomialBackTrackingTerm term;
62+
DataFlow::MethodCallNode mcn;
5063

5164
PolynomialBackTrackingTermUse() {
52-
exists(DataFlow::MethodCallNode mcn, DataFlow::Node regexp, string name |
65+
exists(DataFlow::Node regexp, string name |
5366
term.getRootTerm() = RegExp::getRegExpFromNode(regexp)
5467
|
5568
this = mcn.getArgument(0) and
@@ -70,14 +83,22 @@ module PolynomialReDoS {
7083
}
7184

7285
override RegExpTerm getRegExp() { result = term }
86+
87+
override DataFlow::Node getHighlight() { result = mcn }
7388
}
7489

7590
/**
7691
* An operation that limits the length of a string, seen as a sanitizer.
7792
*/
7893
class StringLengthLimiter extends Sanitizer {
7994
StringLengthLimiter() {
80-
this.(StringReplaceCall).isGlobal()
95+
this.(StringReplaceCall).isGlobal() and
96+
// not lone char classes - they don't remove any repeated pattern.
97+
not exists(RegExpTerm root | root = this.(StringReplaceCall).getRegExp().getRoot() |
98+
root instanceof RegExpCharacterClass
99+
or
100+
root instanceof RegExpCharacterClassEscape
101+
)
81102
or
82103
exists(string name | name = "slice" or name = "substring" or name = "substr" |
83104
this.(DataFlow::MethodCallNode).getMethodName() = name
@@ -108,4 +129,17 @@ module PolynomialReDoS {
108129
e = input.asExpr()
109130
}
110131
}
132+
133+
private import semmle.javascript.PackageExports as Exports
134+
135+
/**
136+
* A parameter of an exported function, seen as a source for polynomial-redos.
137+
*/
138+
class ExternalInputSource extends Source, DataFlow::ParameterNode {
139+
ExternalInputSource() { this = Exports::getALibraryInputParameter() }
140+
141+
override string getKind() { result = "library" }
142+
143+
override string describe() { result = "library input" }
144+
}
111145
}

javascript/ql/src/semmle/javascript/security/performance/ReDoSUtil.qll

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,10 @@ private module SuffixConstruction {
905905
exists(ascii(result))
906906
or
907907
exists(InputSymbol s | belongsTo(s, root) | result = intersect(s, _))
908+
or
909+
// The characters from `hasSimpleRejectEdge`. Only `\n` is really needed (as `\n` is not in the `ascii` relation).
910+
// The three chars must be kept in sync with `hasSimpleRejectEdge`.
911+
result = ["|", "\n", "Z"]
908912
}
909913

910914
/**
@@ -923,7 +927,7 @@ private module SuffixConstruction {
923927
* This predicate is used as a cheap pre-processing to speed up `hasRejectEdge`.
924928
*/
925929
private predicate hasSimpleRejectEdge(State s) {
926-
// The three chars were chosen arbitrarily.
930+
// The three chars were chosen arbitrarily. The three chars must be kept in sync with `relevant`.
927931
exists(string char | char = ["|", "\n", "Z"] | not deltaClosedChar(s, char, _))
928932
}
929933

javascript/ql/src/semmle/javascript/security/performance/SuperlinearBackTracking.qll

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,20 @@ predicate step(
209209
*/
210210
pragma[noinline]
211211
string getAThreewayIntersect(InputSymbol s1, InputSymbol s2, InputSymbol s3) {
212-
result = intersect(s1, s2) and result = [intersect(s2, s3), intersect(s1, s3)]
212+
result = minAndMaxIntersect(s1, s2) and result = [intersect(s2, s3), intersect(s1, s3)]
213213
or
214-
result = intersect(s1, s3) and result = [intersect(s2, s3), intersect(s1, s2)]
214+
result = minAndMaxIntersect(s1, s3) and result = [intersect(s2, s3), intersect(s1, s2)]
215215
or
216-
result = intersect(s2, s3) and result = [intersect(s1, s2), intersect(s1, s3)]
216+
result = minAndMaxIntersect(s2, s3) and result = [intersect(s1, s2), intersect(s1, s3)]
217+
}
218+
219+
/**
220+
* Gets the minimum and maximum characters that intersect between `a` and `b`.
221+
* This predicate is used to limit the size of `getAThreewayIntersect`.
222+
*/
223+
pragma[noinline]
224+
string minAndMaxIntersect(InputSymbol a, InputSymbol b) {
225+
result = [min(intersect(a, b)), max(intersect(a, b))]
217226
}
218227

219228
private newtype TTrace =
@@ -347,15 +356,11 @@ predicate isPumpable(State pivot, State succ, string pump) {
347356
/**
348357
* Holds if repetitions of `pump` at `t` will cause polynomial backtracking.
349358
*/
350-
predicate polynimalReDoS(RegExpTerm t, string msg) {
351-
exists(string pump, State s, string prefixMsg |
359+
predicate polynimalReDoS(RegExpTerm t, string pump, string prefixMsg, RegExpTerm prev) {
360+
exists(State s, State pivot |
352361
hasReDoSResult(t, pump, s, prefixMsg) and
353-
exists(State pivot |
354-
isPumpable(pivot, s, _) and
355-
msg =
356-
"Strings " + prefixMsg + "with many repetitions of '" + pump +
357-
"' can start matching anywhere after the start of the preceeding " + pivot.getRepr()
358-
)
362+
isPumpable(pivot, s, _) and
363+
prev = pivot.getRepr()
359364
)
360365
}
361366

@@ -388,17 +393,30 @@ private predicate matchesEpsilon(RegExpTerm t) {
388393
forex(RegExpTerm child | child = t.(RegExpSequence).getAChild() | matchesEpsilon(child))
389394
}
390395

396+
/**
397+
* Gets a message for why `term` can cause polynomial backtracking.
398+
*/
399+
string getReasonString(RegExpTerm term, string pump, string prefixMsg, RegExpTerm prev) {
400+
polynimalReDoS(term, pump, prefixMsg, prev) and
401+
result =
402+
"Strings " + prefixMsg + "with many repetitions of '" + pump +
403+
"' can start matching anywhere after the start of the preceeding " + prev
404+
}
405+
391406
/**
392407
* A term that may cause a regular expression engine to perform a
393408
* polynomial number of match attempts, relative to the input length.
394409
*/
395410
class PolynomialBackTrackingTerm extends InfiniteRepetitionQuantifier {
396411
string reason;
412+
string pump;
413+
string prefixMsg;
414+
RegExpTerm prev;
397415

398416
PolynomialBackTrackingTerm() {
399-
polynimalReDoS(this, _) and
400-
// there might be many reasons for this term to have polynomial backtracking - we pick an arbitary one.
401-
reason = min(string msg | polynimalReDoS(this, msg))
417+
reason = getReasonString(this, pump, prefixMsg, prev) and
418+
// there might be many reasons for this term to have polynomial backtracking - we pick the shortest one.
419+
reason = min(string msg | msg = getReasonString(this, _, _, _) | msg order by msg.length(), msg)
402420
}
403421

404422
/**
@@ -410,6 +428,21 @@ class PolynomialBackTrackingTerm extends InfiniteRepetitionQuantifier {
410428
)
411429
}
412430

431+
/**
432+
* Gets the string that should be repeated to cause this regular expression to perform polynomially.
433+
*/
434+
string getPumpString() { result = pump }
435+
436+
/**
437+
* Gets a message for which prefix a matching string must start with for this term to cause polynomial backtracking.
438+
*/
439+
string getPrefixMessage() { result = prefixMsg }
440+
441+
/**
442+
* Gets a predecessor to `this`, which also loops on the pump string, and thereby causes polynomial backtracking.
443+
*/
444+
RegExpTerm getPreviousLoop() { result = prev }
445+
413446
/**
414447
* Gets the reason for the number of match attempts.
415448
*/
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2+
"name": "foo",
23
"main": "dist/does-not-exist.js"
34
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module.exports = function thisIsRequiredFromMain() {}
22

3-
module.exports.foo = function alsoExported() {}
3+
module.exports.foo = function alsoExported(d) {}

javascript/ql/test/library-tests/PackageExports/lib1/main.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ module.exports = function isExported() {}
33
module.exports.foo = require("./foo.js")
44

55
module.exports.bar = class Bar {
6-
constructor() {} // all are exported
7-
static staticMethod() {}
8-
instanceMethod() {}
6+
constructor(a) {} // all are exported
7+
static staticMethod(b) {}
8+
instanceMethod(c) {}
99
}
1010

1111
class Baz {

0 commit comments

Comments
 (0)