Skip to content

Commit cbad705

Browse files
committed
general performance improvements in the ReDoS utility library
1 parent 3a43421 commit cbad705

4 files changed

Lines changed: 79 additions & 14 deletions

File tree

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

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -802,12 +802,18 @@ private module PrefixConstruction {
802802
result = prefix(prev) and delta(prev, Epsilon(), state)
803803
or
804804
not delta(prev, Epsilon(), state) and
805-
result =
806-
prefix(prev) +
807-
min(string c | delta(prev, any(InputSymbol symbol | c = intersect(Any(), symbol)), state))
805+
result = prefix(prev) + getMinimumEdgeChar(prev, state)
808806
)
809807
}
810808

809+
/**
810+
* Gets the minimum char for which there exists a transition from `prev` to `next` in the NFA.
811+
*/
812+
private string getMinimumEdgeChar(State prev, State next) {
813+
result =
814+
min(string c | delta(prev, any(InputSymbol symbol | c = intersect(Any(), symbol)), next))
815+
}
816+
811817
/**
812818
* A state within a regular expression that has a pumpable state.
813819
*/
@@ -871,15 +877,27 @@ private module SuffixConstruction {
871877
/**
872878
* Holds if there is likely a non-empty suffix leading to rejection starting in `s`.
873879
*/
874-
pragma[noinline]
880+
pragma[noopt]
875881
predicate hasEdgeToLikelyRejectable(StateInPumpableRegexp s) {
876882
// all edges (at least one) with some char leads to another state that is rejectable.
877883
// the `next` states might not share a common suffix, which can cause FPs.
878-
exists(string char | char = relevant(getRoot(s.getRepr())) |
879-
forex(State next | deltaClosedChar(s, char, next) | isLikelyRejectable(next))
884+
exists(string char | char = hasEdgeToLikelyRejectableHelper(s) |
885+
exists(State next | deltaClosedChar(s, char, next) | isLikelyRejectable(next)) and
886+
forall(State next | deltaClosedChar(s, char, next) | isLikelyRejectable(next))
880887
)
881888
}
882889

890+
/**
891+
* Gets a char for there exists a transition away from `s`,
892+
* and `s` has not been found to be rejectable by `hasRejectEdge` or `isRejectState`.
893+
*/
894+
pragma[noinline]
895+
private string hasEdgeToLikelyRejectableHelper(StateInPumpableRegexp s) {
896+
not hasRejectEdge(s) and
897+
not isRejectState(s) and
898+
deltaClosedChar(s, result, _)
899+
}
900+
883901
/**
884902
* Holds if there is a state `next` that can be reached from `prev`
885903
* along epsilon edges, such that there is a transition from
@@ -900,37 +918,76 @@ private module SuffixConstruction {
900918
*/
901919
pragma[noinline]
902920
private string relevant(RegExpRoot root) {
903-
result = ["a", "9", "|", "\n", " "]
921+
result = ["a", "9", "|", "\n", " ", "|", "\n", "Z"] // must include all the strings from `hasSimpleRejectEdge`.
904922
or
905923
exists(InputSymbol s | belongsTo(s, root) | result = intersect(s, _))
906924
}
907925

908926
/**
909-
* Holds if there is no edge from `s` labeled `char` in our NFA.
927+
* Holds if there exists a `char` such that there is no edge from `s` labeled `char` in our NFA.
910928
* The NFA does not model reject states, so the above is the same as saying there is a reject edge.
911929
*/
912930
private predicate hasRejectEdge(State s) {
931+
hasSimpleRejectEdge(s)
932+
or
933+
not hasSimpleRejectEdge(s) and
913934
exists(string char | char = relevant(getRoot(s.getRepr())) | not deltaClosedChar(s, char, _))
914935
}
915936

937+
/**
938+
* Holds if there is not edge from `s` labeled with "|", "\n", or "Z" in our NFA.
939+
* This predicate is used as a cheap pre-processing to speed up `hasRejectEdge`.
940+
*/
941+
private predicate hasSimpleRejectEdge(State s) {
942+
// The three chars were chosen arbitrarily.
943+
exists(string char | char = ["|", "\n", "Z"] | not deltaClosedChar(s, char, _))
944+
}
945+
916946
/**
917947
* Gets a state that can be reached from pumpable `fork` consuming all
918948
* chars in `w` any number of times followed by the first `i+1` characters of `w`.
919949
*/
950+
pragma[noopt]
920951
private State process(State fork, string w, int i) {
952+
exists(State prev | prev = getProcessPrevious(fork, i, w) |
953+
exists(string char, InputSymbol sym |
954+
char = w.charAt(i) and
955+
deltaClosed(prev, sym, result) and
956+
sym = getAProcessInputSymbol(char)
957+
)
958+
)
959+
}
960+
961+
/**
962+
* Gets a state that can be reached from pumpable `fork` consuming all
963+
* chars in `w` any number of times followed by the first `i` characters of `w`.
964+
*/
965+
private State getProcessPrevious(State fork, int i, string w) {
921966
isReDoSCandidate(fork, w) and
922-
exists(State prev |
923-
i = 0 and prev = fork
967+
(
968+
i = 0 and result = fork
924969
or
925-
prev = process(fork, w, i - 1)
970+
result = process(fork, w, i - 1)
926971
or
927972
// repeat until fixpoint
928973
i = 0 and
929-
prev = process(fork, w, w.length() - 1)
930-
|
931-
deltaClosed(prev, getAnInputSymbolMatching(w.charAt(i)), result)
974+
result = process(fork, w, w.length() - 1)
932975
)
933976
}
977+
978+
/**
979+
* Gets an InputSymbol that matches `char`.
980+
* The predicate is specialized to only have a result for the `char`s that are relevant for the `process` predicate.
981+
*/
982+
private InputSymbol getAProcessInputSymbol(string char) {
983+
char = getAProcessChar() and
984+
result = getAnInputSymbolMatching(char)
985+
}
986+
987+
/**
988+
* Gets a `char` that occurs in a `pump` string.
989+
*/
990+
private string getAProcessChar() { result = any(string s | isReDoSCandidate(_, s)).charAt(_) }
934991
}
935992

936993
/**

javascript/ql/test/query-tests/Performance/ReDoS/PolynomialBackTracking.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
| polynomial-redos.js:112:17:112:19 | \\s+ | Strings with many repetitions of ' ' can start matching anywhere after the start of the preceeding \\s+$ |
7777
| polynomial-redos.js:114:22:114:24 | \\w* | Strings starting with '5' and with many repetitions of '5' can start matching anywhere after the start of the preceeding \\d* |
7878
| polynomial-redos.js:116:21:116:28 | [\\d\\D]*? | Strings starting with '/*' and with many repetitions of 'a/*' can start matching anywhere after the start of the preceeding \\/\\*[\\d\\D]*?\\*\\/ |
79+
| polynomial-redos.js:118:17:118:23 | (#\\d+)+ | Strings with many repetitions of '9' can start matching anywhere after the start of the preceeding \\d+ |
7980
| regexplib/address.js:27:3:27:5 | \\s* | Strings with many repetitions of ' ' can start matching anywhere after the start of the preceeding (\\s*\\(?0\\d{4}\\)?(\\s*\|-)\\d{3}(\\s*\|-)\\d{3}\\s*) |
8081
| regexplib/address.js:27:48:27:50 | \\s* | Strings with many repetitions of ' ' can start matching anywhere after the start of the preceeding (\\s*\\(?0\\d{3}\\)?(\\s*\|-)\\d{3}(\\s*\|-)\\d{4}\\s*) |
8182
| regexplib/address.js:27:93:27:95 | \\s* | Strings with many repetitions of ' ' can start matching anywhere after the start of the preceeding (\\s*(7\|8)(\\d{7}\|\\d{3}(\\-\|\\s{1})\\d{4})\\s*) |

javascript/ql/test/query-tests/Performance/ReDoS/PolynomialReDoS.expected

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ nodes
132132
| polynomial-redos.js:114:2:114:8 | tainted |
133133
| polynomial-redos.js:116:2:116:8 | tainted |
134134
| polynomial-redos.js:116:2:116:8 | tainted |
135+
| polynomial-redos.js:118:2:118:8 | tainted |
136+
| polynomial-redos.js:118:2:118:8 | tainted |
135137
edges
136138
| polynomial-redos.js:5:6:5:32 | tainted | polynomial-redos.js:7:2:7:8 | tainted |
137139
| polynomial-redos.js:5:6:5:32 | tainted | polynomial-redos.js:7:2:7:8 | tainted |
@@ -257,6 +259,8 @@ edges
257259
| polynomial-redos.js:5:6:5:32 | tainted | polynomial-redos.js:114:2:114:8 | tainted |
258260
| polynomial-redos.js:5:6:5:32 | tainted | polynomial-redos.js:116:2:116:8 | tainted |
259261
| polynomial-redos.js:5:6:5:32 | tainted | polynomial-redos.js:116:2:116:8 | tainted |
262+
| polynomial-redos.js:5:6:5:32 | tainted | polynomial-redos.js:118:2:118:8 | tainted |
263+
| polynomial-redos.js:5:6:5:32 | tainted | polynomial-redos.js:118:2:118:8 | tainted |
260264
| polynomial-redos.js:5:16:5:32 | req.query.tainted | polynomial-redos.js:5:6:5:32 | tainted |
261265
| polynomial-redos.js:5:16:5:32 | req.query.tainted | polynomial-redos.js:5:6:5:32 | tainted |
262266
| polynomial-redos.js:68:18:68:24 | req.url | polynomial-redos.js:68:18:68:24 | req.url |
@@ -335,3 +339,4 @@ edges
335339
| polynomial-redos.js:112:2:112:8 | tainted | polynomial-redos.js:5:16:5:32 | req.query.tainted | polynomial-redos.js:112:2:112:8 | tainted | This expensive $@ use depends on $@. | polynomial-redos.js:112:17:112:19 | \\s+ | regular expression | polynomial-redos.js:5:16:5:32 | req.query.tainted | a user-provided value |
336340
| polynomial-redos.js:114:2:114:8 | tainted | polynomial-redos.js:5:16:5:32 | req.query.tainted | polynomial-redos.js:114:2:114:8 | tainted | This expensive $@ use depends on $@. | polynomial-redos.js:114:22:114:24 | \\w* | regular expression | polynomial-redos.js:5:16:5:32 | req.query.tainted | a user-provided value |
337341
| polynomial-redos.js:116:2:116:8 | tainted | polynomial-redos.js:5:16:5:32 | req.query.tainted | polynomial-redos.js:116:2:116:8 | tainted | This expensive $@ use depends on $@. | polynomial-redos.js:116:21:116:28 | [\\d\\D]*? | regular expression | polynomial-redos.js:5:16:5:32 | req.query.tainted | a user-provided value |
342+
| polynomial-redos.js:118:2:118:8 | tainted | polynomial-redos.js:5:16:5:32 | req.query.tainted | polynomial-redos.js:118:2:118:8 | tainted | This expensive $@ use depends on $@. | polynomial-redos.js:118:17:118:23 | (#\\d+)+ | regular expression | polynomial-redos.js:5:16:5:32 | req.query.tainted | a user-provided value |

javascript/ql/test/query-tests/Performance/ReDoS/polynomial-redos.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,6 @@ app.use(function(req, res) {
114114
tainted.match(/^\d*5\w*$/); // NOT OK
115115

116116
tainted.match(/\/\*[\d\D]*?\*\//g); // NOT OK
117+
118+
tainted.match(/(#\d+)+/); // OK - but still flagged due to insufficient suffix-checking.
117119
});

0 commit comments

Comments
 (0)