|
| 1 | +/** |
| 2 | + * @name Case-sensitive middleware path |
| 3 | + * @description Middleware with case-sensitive paths do not protect endpoints with case-insensitive paths. |
| 4 | + * @kind problem |
| 5 | + * @problem.severity warning |
| 6 | + * @security-severity 7.3 |
| 7 | + * @precision high |
| 8 | + * @id js/case-sensitive-middleware-path |
| 9 | + * @tags security |
| 10 | + * external/cwe/cwe-178 |
| 11 | + */ |
| 12 | + |
| 13 | +import javascript |
| 14 | + |
| 15 | +/** |
| 16 | + * Converts `s` to upper case, or to lower-case if it was already upper case. |
| 17 | + */ |
| 18 | +bindingset[s] |
| 19 | +string toOtherCase(string s) { |
| 20 | + if s.regexpMatch(".*[a-z].*") then result = s.toUpperCase() else result = s.toLowerCase() |
| 21 | +} |
| 22 | + |
| 23 | +RegExpCharacterClass getEnclosingClass(RegExpTerm term) { |
| 24 | + term = result.getAChild() |
| 25 | + or |
| 26 | + term = result.getAChild().(RegExpRange).getAChild() |
| 27 | +} |
| 28 | + |
| 29 | +/** |
| 30 | + * Holds if `term` seems to distinguish between upper and lower case letters, assuming the `i` flag is not present. |
| 31 | + */ |
| 32 | +pragma[inline] |
| 33 | +predicate isLikelyCaseSensitiveRegExp(RegExpTerm term) { |
| 34 | + exists(RegExpConstant const | |
| 35 | + const = term.getAChild*() and |
| 36 | + const.getValue().regexpMatch(".*[a-zA-Z].*") and |
| 37 | + not getEnclosingClass(const).getAChild().(RegExpConstant).getValue() = |
| 38 | + toOtherCase(const.getValue()) and |
| 39 | + not const.getParent*() instanceof RegExpNegativeLookahead and |
| 40 | + not const.getParent*() instanceof RegExpNegativeLookbehind |
| 41 | + ) |
| 42 | +} |
| 43 | + |
| 44 | +/** |
| 45 | + * Gets a string matched by `term`, or part of such a string. |
| 46 | + */ |
| 47 | +string getExampleString(RegExpTerm term) { |
| 48 | + result = term.getAMatchedString() |
| 49 | + or |
| 50 | + // getAMatchedString does not recurse into sequences. Perform one step manually. |
| 51 | + exists(RegExpSequence seq | seq = term | |
| 52 | + result = |
| 53 | + strictconcat(RegExpTerm child, int i, string text | |
| 54 | + child = seq.getChild(i) and |
| 55 | + ( |
| 56 | + text = child.getAMatchedString() |
| 57 | + or |
| 58 | + not exists(child.getAMatchedString()) and |
| 59 | + text = "" |
| 60 | + ) |
| 61 | + | |
| 62 | + text order by i |
| 63 | + ) |
| 64 | + ) |
| 65 | +} |
| 66 | + |
| 67 | +string getCaseSensitiveBypassExample(RegExpTerm term) { |
| 68 | + exists(string example | |
| 69 | + example = getExampleString(term) and |
| 70 | + result = toOtherCase(example) and |
| 71 | + result != example // getting an example string is approximate; ensure we got a proper case-change example |
| 72 | + ) |
| 73 | +} |
| 74 | + |
| 75 | +/** |
| 76 | + * Holds if `setup` has a path-argument `arg` referring to the given case-sensitive `regexp`. |
| 77 | + */ |
| 78 | +predicate isCaseSensitiveMiddleware( |
| 79 | + Routing::RouteSetup setup, DataFlow::RegExpCreationNode regexp, DataFlow::Node arg |
| 80 | +) { |
| 81 | + exists(DataFlow::MethodCallNode call | |
| 82 | + setup = Routing::getRouteSetupNode(call) and |
| 83 | + ( |
| 84 | + setup.definitelyResumesDispatch() |
| 85 | + or |
| 86 | + // If applied to all HTTP methods, be a bit more lenient in detecting middleware |
| 87 | + setup.mayResumeDispatch() and |
| 88 | + not exists(setup.getOwnHttpMethod()) |
| 89 | + ) and |
| 90 | + arg = call.getArgument(0) and |
| 91 | + regexp.getAReference().flowsTo(arg) and |
| 92 | + isLikelyCaseSensitiveRegExp(regexp.getRoot()) and |
| 93 | + exists(string flags | |
| 94 | + flags = regexp.getFlags() and |
| 95 | + not RegExp::isIgnoreCase(flags) |
| 96 | + ) |
| 97 | + ) |
| 98 | +} |
| 99 | + |
| 100 | +predicate isGuardedCaseInsensitiveEndpoint( |
| 101 | + Routing::RouteSetup endpoint, Routing::RouteSetup middleware |
| 102 | +) { |
| 103 | + isCaseSensitiveMiddleware(middleware, _, _) and |
| 104 | + exists(DataFlow::MethodCallNode call | |
| 105 | + endpoint = Routing::getRouteSetupNode(call) and |
| 106 | + endpoint.isGuardedByNode(middleware) and |
| 107 | + call.getArgument(0).mayHaveStringValue(_) |
| 108 | + ) |
| 109 | +} |
| 110 | + |
| 111 | +from |
| 112 | + DataFlow::RegExpCreationNode regexp, Routing::RouteSetup middleware, Routing::RouteSetup endpoint, |
| 113 | + DataFlow::Node arg, string example |
| 114 | +where |
| 115 | + isCaseSensitiveMiddleware(middleware, regexp, arg) and |
| 116 | + example = getCaseSensitiveBypassExample(regexp.getRoot()) and |
| 117 | + isGuardedCaseInsensitiveEndpoint(endpoint, middleware) and |
| 118 | + exists(endpoint.getRelativePath().toLowerCase().indexOf(example.toLowerCase())) |
| 119 | +select arg, |
| 120 | + "This route uses a case-sensitive path $@, but is guarding a case-insensitive path $@. A path such as '" |
| 121 | + + example + "' will bypass the middleware.", regexp, "pattern", endpoint, "here" |
0 commit comments