Merge branch 'main' into rb-redosMod

This commit is contained in:
erik-krogh
2022-11-15 17:14:19 +01:00
36 changed files with 1736 additions and 3385 deletions

View File

@@ -3,7 +3,7 @@
* @description Loading a .NET assembly based on a path constructed from user-controlled sources
* may allow a malicious user to load code which modifies the program in unintended
* ways.
* @kind problem
* @kind path-problem
* @id cs/assembly-path-injection
* @problem.severity error
* @security-severity 8.2
@@ -15,6 +15,7 @@
import csharp
import semmle.code.csharp.security.dataflow.flowsources.Remote
import semmle.code.csharp.commons.Util
import DataFlow::PathGraph
/**
* A taint-tracking configuration for untrusted user input used to load a DLL.
@@ -47,6 +48,7 @@ class TaintTrackingConfiguration extends TaintTracking::Configuration {
}
}
from TaintTrackingConfiguration c, DataFlow::Node source, DataFlow::Node sink
where c.hasFlow(source, sink)
select sink, "This assembly path depends on a $@.", source, "user-provided value"
from TaintTrackingConfiguration c, DataFlow::PathNode source, DataFlow::PathNode sink
where c.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This assembly path depends on a $@.", source,
"user-provided value"

View File

@@ -1,7 +1,7 @@
/**
* @name Hard-coded encryption key
* @description The .Key property or rgbKey parameter of a SymmetricAlgorithm should never be a hard-coded value.
* @kind problem
* @kind path-problem
* @id cs/hardcoded-key
* @problem.severity error
* @security-severity 8.1
@@ -15,6 +15,7 @@
import csharp
import semmle.code.csharp.security.cryptography.EncryptionKeyDataFlowQuery
import DataFlow::PathGraph
/**
* The creation of a literal byte array.
@@ -36,7 +37,13 @@ class StringLiteralSource extends KeySource {
StringLiteralSource() { this.asExpr() instanceof StringLiteral }
}
from SymmetricKeyTaintTrackingConfiguration keyFlow, KeySource src, SymmetricEncryptionKeySink sink
where keyFlow.hasFlow(src, sink)
select sink, "This hard-coded $@ is used in symmetric algorithm in " + sink.getDescription(), src,
from
SymmetricKeyTaintTrackingConfiguration keyFlow, DataFlow::PathNode source,
DataFlow::PathNode sink, KeySource srcNode, SymmetricEncryptionKeySink sinkNode
where
keyFlow.hasFlowPath(source, sink) and
source.getNode() = srcNode and
sink.getNode() = sinkNode
select sink.getNode(), source, sink,
"This hard-coded $@ is used in symmetric algorithm in " + sinkNode.getDescription(), srcNode,
"symmetric key"

View File

@@ -1 +1,11 @@
| Test.cs:10:36:10:46 | access to local variable libraryName | This assembly path depends on a $@. | Test.cs:7:26:7:48 | access to property QueryString | user-provided value |
edges
| Test.cs:7:26:7:48 | access to property QueryString : NameValueCollection | Test.cs:7:26:7:63 | access to indexer : String |
| Test.cs:7:26:7:48 | access to property QueryString : NameValueCollection | Test.cs:10:36:10:46 | access to local variable libraryName |
| Test.cs:7:26:7:63 | access to indexer : String | Test.cs:10:36:10:46 | access to local variable libraryName |
nodes
| Test.cs:7:26:7:48 | access to property QueryString : NameValueCollection | semmle.label | access to property QueryString : NameValueCollection |
| Test.cs:7:26:7:63 | access to indexer : String | semmle.label | access to indexer : String |
| Test.cs:10:36:10:46 | access to local variable libraryName | semmle.label | access to local variable libraryName |
subpaths
#select
| Test.cs:10:36:10:46 | access to local variable libraryName | Test.cs:7:26:7:48 | access to property QueryString : NameValueCollection | Test.cs:10:36:10:46 | access to local variable libraryName | This assembly path depends on a $@. | Test.cs:7:26:7:48 | access to property QueryString : NameValueCollection | user-provided value |

View File

@@ -1,7 +1,40 @@
| HardcodedSymmetricEncryptionKey.cs:17:21:17:97 | array creation of type Byte[] | This hard-coded $@ is used in symmetric algorithm in Key property assignment | HardcodedSymmetricEncryptionKey.cs:17:21:17:97 | array creation of type Byte[] | symmetric key |
| HardcodedSymmetricEncryptionKey.cs:22:23:22:99 | array creation of type Byte[] | This hard-coded $@ is used in symmetric algorithm in Key property assignment | HardcodedSymmetricEncryptionKey.cs:22:23:22:99 | array creation of type Byte[] | symmetric key |
| HardcodedSymmetricEncryptionKey.cs:31:21:31:21 | access to local variable d | This hard-coded $@ is used in symmetric algorithm in Key property assignment | HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] | symmetric key |
| HardcodedSymmetricEncryptionKey.cs:68:87:68:94 | access to parameter password | This hard-coded $@ is used in symmetric algorithm in Decryptor(rgbKey, IV) | HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] | symmetric key |
| HardcodedSymmetricEncryptionKey.cs:108:23:108:25 | access to parameter key | This hard-coded $@ is used in symmetric algorithm in Key property assignment | HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] | symmetric key |
| HardcodedSymmetricEncryptionKey.cs:121:87:121:89 | access to parameter key | This hard-coded $@ is used in symmetric algorithm in Encryptor(rgbKey, IV) | HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] | symmetric key |
| HardcodedSymmetricEncryptionKey.cs:121:87:121:89 | access to parameter key | This hard-coded $@ is used in symmetric algorithm in Encryptor(rgbKey, IV) | HardcodedSymmetricEncryptionKey.cs:28:62:28:115 | "Hello, world: here is a very bad way to create a key" | symmetric key |
edges
| HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] : Byte[] | HardcodedSymmetricEncryptionKey.cs:31:21:31:21 | access to local variable d |
| HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] : Byte[] | HardcodedSymmetricEncryptionKey.cs:36:37:36:37 | access to local variable d : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] : Byte[] | HardcodedSymmetricEncryptionKey.cs:41:50:41:50 | access to local variable c : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] : Byte[] | HardcodedSymmetricEncryptionKey.cs:50:35:50:35 | access to local variable c : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:28:39:28:116 | call to method GetBytes : Byte[] | HardcodedSymmetricEncryptionKey.cs:44:51:44:69 | access to local variable byteArrayFromString : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:28:62:28:115 | "Hello, world: here is a very bad way to create a key" : String | HardcodedSymmetricEncryptionKey.cs:28:39:28:116 | call to method GetBytes : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:36:37:36:37 | access to local variable d : Byte[] | HardcodedSymmetricEncryptionKey.cs:103:57:103:59 | key : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:41:50:41:50 | access to local variable c : Byte[] | HardcodedSymmetricEncryptionKey.cs:112:63:112:65 | key : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:44:51:44:69 | access to local variable byteArrayFromString : Byte[] | HardcodedSymmetricEncryptionKey.cs:112:63:112:65 | key : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:50:35:50:35 | access to local variable c : Byte[] | HardcodedSymmetricEncryptionKey.cs:59:64:59:71 | password : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:59:64:59:71 | password : Byte[] | HardcodedSymmetricEncryptionKey.cs:68:87:68:94 | access to parameter password |
| HardcodedSymmetricEncryptionKey.cs:103:57:103:59 | key : Byte[] | HardcodedSymmetricEncryptionKey.cs:108:23:108:25 | access to parameter key |
| HardcodedSymmetricEncryptionKey.cs:112:63:112:65 | key : Byte[] | HardcodedSymmetricEncryptionKey.cs:121:87:121:89 | access to parameter key |
nodes
| HardcodedSymmetricEncryptionKey.cs:17:21:17:97 | array creation of type Byte[] | semmle.label | array creation of type Byte[] |
| HardcodedSymmetricEncryptionKey.cs:22:23:22:99 | array creation of type Byte[] | semmle.label | array creation of type Byte[] |
| HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] : Byte[] | semmle.label | array creation of type Byte[] : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:28:39:28:116 | call to method GetBytes : Byte[] | semmle.label | call to method GetBytes : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:28:62:28:115 | "Hello, world: here is a very bad way to create a key" : String | semmle.label | "Hello, world: here is a very bad way to create a key" : String |
| HardcodedSymmetricEncryptionKey.cs:31:21:31:21 | access to local variable d | semmle.label | access to local variable d |
| HardcodedSymmetricEncryptionKey.cs:36:37:36:37 | access to local variable d : Byte[] | semmle.label | access to local variable d : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:41:50:41:50 | access to local variable c : Byte[] | semmle.label | access to local variable c : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:44:51:44:69 | access to local variable byteArrayFromString : Byte[] | semmle.label | access to local variable byteArrayFromString : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:50:35:50:35 | access to local variable c : Byte[] | semmle.label | access to local variable c : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:59:64:59:71 | password : Byte[] | semmle.label | password : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:68:87:68:94 | access to parameter password | semmle.label | access to parameter password |
| HardcodedSymmetricEncryptionKey.cs:103:57:103:59 | key : Byte[] | semmle.label | key : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:108:23:108:25 | access to parameter key | semmle.label | access to parameter key |
| HardcodedSymmetricEncryptionKey.cs:112:63:112:65 | key : Byte[] | semmle.label | key : Byte[] |
| HardcodedSymmetricEncryptionKey.cs:121:87:121:89 | access to parameter key | semmle.label | access to parameter key |
subpaths
#select
| HardcodedSymmetricEncryptionKey.cs:17:21:17:97 | array creation of type Byte[] | HardcodedSymmetricEncryptionKey.cs:17:21:17:97 | array creation of type Byte[] | HardcodedSymmetricEncryptionKey.cs:17:21:17:97 | array creation of type Byte[] | This hard-coded $@ is used in symmetric algorithm in Key property assignment | HardcodedSymmetricEncryptionKey.cs:17:21:17:97 | array creation of type Byte[] | symmetric key |
| HardcodedSymmetricEncryptionKey.cs:22:23:22:99 | array creation of type Byte[] | HardcodedSymmetricEncryptionKey.cs:22:23:22:99 | array creation of type Byte[] | HardcodedSymmetricEncryptionKey.cs:22:23:22:99 | array creation of type Byte[] | This hard-coded $@ is used in symmetric algorithm in Key property assignment | HardcodedSymmetricEncryptionKey.cs:22:23:22:99 | array creation of type Byte[] | symmetric key |
| HardcodedSymmetricEncryptionKey.cs:31:21:31:21 | access to local variable d | HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] : Byte[] | HardcodedSymmetricEncryptionKey.cs:31:21:31:21 | access to local variable d | This hard-coded $@ is used in symmetric algorithm in Key property assignment | HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] | symmetric key |
| HardcodedSymmetricEncryptionKey.cs:68:87:68:94 | access to parameter password | HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] : Byte[] | HardcodedSymmetricEncryptionKey.cs:68:87:68:94 | access to parameter password | This hard-coded $@ is used in symmetric algorithm in Decryptor(rgbKey, IV) | HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] | symmetric key |
| HardcodedSymmetricEncryptionKey.cs:108:23:108:25 | access to parameter key | HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] : Byte[] | HardcodedSymmetricEncryptionKey.cs:108:23:108:25 | access to parameter key | This hard-coded $@ is used in symmetric algorithm in Key property assignment | HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] | symmetric key |
| HardcodedSymmetricEncryptionKey.cs:121:87:121:89 | access to parameter key | HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] : Byte[] | HardcodedSymmetricEncryptionKey.cs:121:87:121:89 | access to parameter key | This hard-coded $@ is used in symmetric algorithm in Encryptor(rgbKey, IV) | HardcodedSymmetricEncryptionKey.cs:25:21:25:97 | array creation of type Byte[] | symmetric key |
| HardcodedSymmetricEncryptionKey.cs:121:87:121:89 | access to parameter key | HardcodedSymmetricEncryptionKey.cs:28:62:28:115 | "Hello, world: here is a very bad way to create a key" : String | HardcodedSymmetricEncryptionKey.cs:121:87:121:89 | access to parameter key | This hard-coded $@ is used in symmetric algorithm in Encryptor(rgbKey, IV) | HardcodedSymmetricEncryptionKey.cs:28:62:28:115 | "Hello, world: here is a very bad way to create a key" | symmetric key |

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The ReDoS libraries in `semmle.code.java.security.regexp` has been moved to a shared pack inside the `shared/` folder, and the previous location has been deprecated.

View File

@@ -5,3 +5,5 @@ dbscheme: config/semmlecode.dbscheme
extractor: java
library: true
upgrades: upgrades
dependencies:
codeql/regex: ${workspace}

View File

@@ -7,7 +7,7 @@
*/
import java
import semmle.code.java.regex.RegexTreeView
import semmle.code.java.regex.RegexTreeView as RegexTreeView
private newtype TPrintAstConfiguration = MkPrintAstConfiguration()
@@ -134,8 +134,10 @@ private newtype TPrintAstNode =
TImportsNode(CompilationUnit cu) {
shouldPrint(cu, _) and exists(Import i | i.getCompilationUnit() = cu)
} or
TRegExpTermNode(RegExpTerm term) {
exists(StringLiteral str | term.getRootTerm() = getParsedRegExp(str) and shouldPrint(str, _))
TRegExpTermNode(RegexTreeView::RegExpTerm term) {
exists(StringLiteral str |
term.getRootTerm() = RegexTreeView::getParsedRegExp(str) and shouldPrint(str, _)
)
}
/**
@@ -316,7 +318,7 @@ final class StringLiteralNode extends ExprStmtNode {
override PrintAstNode getChild(int childIndex) {
childIndex = 0 and
result.(RegExpTermNode).getTerm() = getParsedRegExp(element)
result.(RegExpTermNode).getTerm() = RegexTreeView::getParsedRegExp(element)
}
}
@@ -324,12 +326,12 @@ final class StringLiteralNode extends ExprStmtNode {
* A node representing a regular expression term.
*/
class RegExpTermNode extends TRegExpTermNode, PrintAstNode {
RegExpTerm term;
RegexTreeView::RegExpTerm term;
RegExpTermNode() { this = TRegExpTermNode(term) }
/** Gets the `RegExpTerm` for this node. */
RegExpTerm getTerm() { result = term }
RegexTreeView::RegExpTerm getTerm() { result = term }
override PrintAstNode getChild(int childIndex) {
result.(RegExpTermNode).getTerm() = term.getChild(childIndex)

File diff suppressed because it is too large Load Diff

View File

@@ -2,288 +2,7 @@
* Classes and predicates for working with suspicious character ranges.
*/
// We don't need the NFA utils, just the regexp tree.
// but the below is a nice shared library that exposes the API we need.
import regexp.NfaUtils
/**
* Gets a rank for `range` that is unique for ranges in the same file.
* Prioritizes ranges that match more characters.
*/
int rankRange(RegExpCharacterRange range) {
range =
rank[result](RegExpCharacterRange r, Location l, int low, int high |
r.getLocation() = l and
isRange(r, low, high)
|
r order by (high - low) desc, l.getStartLine(), l.getStartColumn()
)
}
/** Holds if `range` spans from the unicode code points `low` to `high` (both inclusive). */
predicate isRange(RegExpCharacterRange range, int low, int high) {
exists(string lowc, string highc |
range.isRange(lowc, highc) and
low.toUnicode() = lowc and
high.toUnicode() = highc
)
}
/** Holds if `char` is an alpha-numeric character. */
predicate isAlphanumeric(string char) {
// written like this to avoid having a bindingset for the predicate
char = [[48 .. 57], [65 .. 90], [97 .. 122]].toUnicode() // 0-9, A-Z, a-z
}
/**
* Holds if the given ranges are from the same character class
* and there exists at least one character matched by both ranges.
*/
predicate overlap(RegExpCharacterRange a, RegExpCharacterRange b) {
exists(RegExpCharacterClass clz |
a = clz.getAChild() and
b = clz.getAChild() and
a != b
|
exists(int alow, int ahigh, int blow, int bhigh |
isRange(a, alow, ahigh) and
isRange(b, blow, bhigh) and
alow <= bhigh and
blow <= ahigh
)
)
}
/**
* Holds if `range` overlaps with the char class `escape` from the same character class.
*/
predicate overlapsWithCharEscape(RegExpCharacterRange range, RegExpCharacterClassEscape escape) {
exists(RegExpCharacterClass clz, string low, string high |
range = clz.getAChild() and
escape = clz.getAChild() and
range.isRange(low, high)
|
escape.getValue() = "w" and
getInRange(low, high).regexpMatch("\\w")
or
escape.getValue() = "d" and
getInRange(low, high).regexpMatch("\\d")
or
escape.getValue() = "s" and
getInRange(low, high).regexpMatch("\\s")
)
}
/** Gets the unicode code point for a `char`. */
bindingset[char]
int toCodePoint(string char) { result.toUnicode() = char }
/** A character range that appears to be overly wide. */
class OverlyWideRange extends RegExpCharacterRange {
OverlyWideRange() {
exists(int low, int high, int numChars |
isRange(this, low, high) and
numChars = (1 + high - low) and
this.getRootTerm().isUsedAsRegExp() and
numChars >= 10
|
// across the Z-a range (which includes backticks)
toCodePoint("Z") >= low and
toCodePoint("a") <= high
or
// across the 9-A range (which includes e.g. ; and ?)
toCodePoint("9") >= low and
toCodePoint("A") <= high
or
// a non-alphanumeric char as part of the range boundaries
exists(int bound | bound = [low, high] | not isAlphanumeric(bound.toUnicode())) and
// while still being ascii
low < 128 and
high < 128
) and
// allowlist for known ranges
not this = allowedWideRanges()
}
/** Gets a string representation of a character class that matches the same chars as this range. */
string printEquivalent() { result = RangePrinter::printEquivalentCharClass(this) }
}
/** Gets a range that should not be reported as an overly wide range. */
RegExpCharacterRange allowedWideRanges() {
// ~ is the last printable ASCII character, it's used right in various wide ranges.
result.isRange(_, "~")
or
// the same with " " and "!". " " is the first printable character, and "!" is the first non-white-space printable character.
result.isRange([" ", "!"], _)
or
// the `[@-_]` range is intentional
result.isRange("@", "_")
or
// starting from the zero byte is a good indication that it's purposely matching a large range.
result.isRange(0.toUnicode(), _)
}
/** Gets a char between (and including) `low` and `high`. */
bindingset[low, high]
private string getInRange(string low, string high) {
result = [toCodePoint(low) .. toCodePoint(high)].toUnicode()
}
/** A module computing an equivalent character class for an overly wide range. */
module RangePrinter {
bindingset[char]
bindingset[result]
private string next(string char) {
exists(int prev, int next |
prev.toUnicode() = char and
next.toUnicode() = result and
next = prev + 1
)
}
/** Gets the points where the parts of the pretty printed range should be cut off. */
private string cutoffs() { result = ["A", "Z", "a", "z", "0", "9"] }
/** Gets the char to use in the low end of a range for a given `cut` */
private string lowCut(string cut) {
cut = ["A", "a", "0"] and
result = cut
or
cut = ["Z", "z", "9"] and
result = next(cut)
}
/** Gets the char to use in the high end of a range for a given `cut` */
private string highCut(string cut) {
cut = ["Z", "z", "9"] and
result = cut
or
cut = ["A", "a", "0"] and
next(result) = cut
}
/** Gets the cutoff char used for a given `part` of a range when pretty-printing it. */
private string cutoff(OverlyWideRange range, int part) {
exists(int low, int high | isRange(range, low, high) |
result =
rank[part + 1](string cut |
cut = cutoffs() and low < toCodePoint(cut) and toCodePoint(cut) < high
|
cut order by toCodePoint(cut)
)
)
}
/** Gets the number of parts we should print for a given `range`. */
private int parts(OverlyWideRange range) { result = 1 + count(cutoff(range, _)) }
/** Holds if the given part of a range should span from `low` to `high`. */
private predicate part(OverlyWideRange range, int part, string low, string high) {
// first part.
part = 0 and
(
range.isRange(low, high) and
parts(range) = 1
or
parts(range) >= 2 and
range.isRange(low, _) and
high = highCut(cutoff(range, part))
)
or
// middle
part >= 1 and
part < parts(range) - 1 and
low = lowCut(cutoff(range, part - 1)) and
high = highCut(cutoff(range, part))
or
// last.
part = parts(range) - 1 and
low = lowCut(cutoff(range, part - 1)) and
range.isRange(_, high)
}
/** Gets an escaped `char` for use in a character class. */
bindingset[char]
private string escape(string char) {
exists(string reg | reg = "(\\[|\\]|\\\\|-|/)" |
if char.regexpMatch(reg) then result = "\\" + char else result = char
)
}
/** Gets a part of the equivalent range. */
private string printEquivalentCharClass(OverlyWideRange range, int part) {
exists(string low, string high | part(range, part, low, high) |
if
isAlphanumeric(low) and
isAlphanumeric(high)
then result = low + "-" + high
else
result =
strictconcat(string char | char = getInRange(low, high) | escape(char) order by char)
)
}
/** Gets the entire pretty printed equivalent range. */
string printEquivalentCharClass(OverlyWideRange range) {
result =
strictconcat(string r, int part |
r = "[" and part = -1 and exists(range)
or
r = printEquivalentCharClass(range, part)
or
r = "]" and part = parts(range)
|
r order by part
)
}
}
/** Gets a char range that is overly large because of `reason`. */
RegExpCharacterRange getABadRange(string reason, int priority) {
result instanceof OverlyWideRange and
priority = 0 and
exists(string equiv | equiv = result.(OverlyWideRange).printEquivalent() |
if equiv.length() <= 50
then reason = "is equivalent to " + equiv
else reason = "is equivalent to " + equiv.substring(0, 50) + "..."
)
or
priority = 1 and
exists(RegExpCharacterRange other |
reason = "overlaps with " + other + " in the same character class" and
rankRange(result) < rankRange(other) and
overlap(result, other)
)
or
priority = 2 and
exists(RegExpCharacterClassEscape escape |
reason = "overlaps with " + escape + " in the same character class" and
overlapsWithCharEscape(result, escape)
)
or
reason = "is empty" and
priority = 3 and
exists(int low, int high |
isRange(result, low, high) and
low > high
)
}
/** Holds if `range` matches suspiciously many characters. */
predicate problem(RegExpCharacterRange range, string reason) {
reason =
strictconcat(string m, int priority |
range = getABadRange(m, priority)
|
m, ", and " order by priority desc
) and
// specifying a range using an escape is usually OK.
not range.getAChild() instanceof RegExpEscape and
// Unicode escapes in strings are interpreted before it turns into a regexp,
// so e.g. [\u0001-\uFFFF] will just turn up as a range between two constants.
// We therefore exclude these ranges.
range.getRootTerm().getParent() instanceof RegExpLiteral and
// is used as regexp (mostly for JS where regular expressions are parsed eagerly)
range.getRootTerm().isUsedAsRegExp()
}
private import semmle.code.java.regex.RegexTreeView::RegexTreeView as TreeView
// OverlyLargeRangeQuery should be used directly from the shared pack, and not from this file.
deprecated import codeql.regex.OverlyLargeRangeQuery::Make<TreeView> as Dep
import Dep

View File

@@ -62,284 +62,7 @@
* a suffix `x` (possible empty) that is most likely __not__ accepted.
*/
import NfaUtils
/**
* Holds if state `s` might be inside a backtracking repetition.
*/
pragma[noinline]
private predicate stateInsideBacktracking(State s) {
s.getRepr().getParent*() instanceof MaybeBacktrackingRepetition
}
/**
* A infinitely repeating quantifier that might backtrack.
*/
private class MaybeBacktrackingRepetition extends InfiniteRepetitionQuantifier {
MaybeBacktrackingRepetition() {
exists(RegExpTerm child |
child instanceof RegExpAlt or
child instanceof RegExpQuantifier
|
child.getParent+() = this
)
}
}
/**
* A state in the product automaton.
*/
private newtype TStatePair =
/**
* We lazily only construct those states that we are actually
* going to need: `(q, q)` for every fork state `q`, and any
* pair of states that can be reached from a pair that we have
* already constructed. To cut down on the number of states,
* we only represent states `(q1, q2)` where `q1` is lexicographically
* no bigger than `q2`.
*
* States are only constructed if both states in the pair are
* inside a repetition that might backtrack.
*/
MkStatePair(State q1, State q2) {
isFork(q1, _, _, _, _) and q2 = q1
or
(step(_, _, _, q1, q2) or step(_, _, _, q2, q1)) and
rankState(q1) <= rankState(q2)
}
/**
* Gets a unique number for a `state`.
* Is used to create an ordering of states, where states with the same `toString()` will be ordered differently.
*/
private int rankState(State state) {
state =
rank[result](State s, Location l |
stateInsideBacktracking(s) and
l = s.getRepr().getLocation()
|
s order by l.getStartLine(), l.getStartColumn(), s.toString()
)
}
/**
* A state in the product automaton.
*/
private class StatePair extends TStatePair {
State q1;
State q2;
StatePair() { this = MkStatePair(q1, q2) }
/** Gets a textual representation of this element. */
string toString() { result = "(" + q1 + ", " + q2 + ")" }
/** Gets the first component of the state pair. */
State getLeft() { result = q1 }
/** Gets the second component of the state pair. */
State getRight() { result = q2 }
}
/**
* Holds for `(fork, fork)` state pairs when `isFork(fork, _, _, _, _)` holds.
*
* Used in `statePairDistToFork`
*/
private predicate isStatePairFork(StatePair p) {
exists(State fork | p = MkStatePair(fork, fork) and isFork(fork, _, _, _, _))
}
/**
* Holds if there are transitions from the components of `q` to the corresponding
* components of `r`.
*
* Used in `statePairDistToFork`
*/
private predicate reverseStep(StatePair r, StatePair q) { step(q, _, _, r) }
/**
* Gets the minimum length of a path from `q` to `r` in the
* product automaton.
*/
private int statePairDistToFork(StatePair q, StatePair r) =
shortestDistances(isStatePairFork/1, reverseStep/2)(r, q, result)
/**
* Holds if there are transitions from `q` to `r1` and from `q` to `r2`
* labelled with `s1` and `s2`, respectively, where `s1` and `s2` do not
* trivially have an empty intersection.
*
* This predicate only holds for states associated with regular expressions
* that have at least one repetition quantifier in them (otherwise the
* expression cannot be vulnerable to ReDoS attacks anyway).
*/
pragma[noopt]
private predicate isFork(State q, InputSymbol s1, InputSymbol s2, State r1, State r2) {
stateInsideBacktracking(q) and
exists(State q1, State q2 |
q1 = epsilonSucc*(q) and
delta(q1, s1, r1) and
q2 = epsilonSucc*(q) and
delta(q2, s2, r2) and
// Use pragma[noopt] to prevent intersect(s1,s2) from being the starting point of the join.
// From (s1,s2) it would find a huge number of intermediate state pairs (q1,q2) originating from different literals,
// and discover at the end that no `q` can reach both `q1` and `q2` by epsilon transitions.
exists(intersect(s1, s2))
|
s1 != s2
or
r1 != r2
or
r1 = r2 and q1 != q2
or
// If q can reach itself by epsilon transitions, then there are two distinct paths to the q1/q2 state:
// one that uses the loop and one that doesn't. The engine will separately attempt to match with each path,
// despite ending in the same state. The "fork" thus arises from the choice of whether to use the loop or not.
// To avoid every state in the loop becoming a fork state,
// we arbitrarily pick the InfiniteRepetitionQuantifier state as the canonical fork state for the loop
// (every epsilon-loop must contain such a state).
//
// We additionally require that the there exists another InfiniteRepetitionQuantifier `mid` on the path from `q` to itself.
// This is done to avoid flagging regular expressions such as `/(a?)*b/` - that only has polynomial runtime, and is detected by `js/polynomial-redos`.
// The below code is therefore a heuristic, that only flags regular expressions such as `/(a*)*b/`,
// and does not flag regular expressions such as `/(a?b?)c/`, but the latter pattern is not used frequently.
r1 = r2 and
q1 = q2 and
epsilonSucc+(q) = q and
exists(RegExpTerm term | term = q.getRepr() | term instanceof InfiniteRepetitionQuantifier) and
// One of the mid states is an infinite quantifier itself
exists(State mid, RegExpTerm term |
mid = epsilonSucc+(q) and
term = mid.getRepr() and
term instanceof InfiniteRepetitionQuantifier and
q = epsilonSucc+(mid) and
not mid = q
)
) and
stateInsideBacktracking(r1) and
stateInsideBacktracking(r2)
}
/**
* Gets the state pair `(q1, q2)` or `(q2, q1)`; note that only
* one or the other is defined.
*/
private StatePair mkStatePair(State q1, State q2) {
result = MkStatePair(q1, q2) or result = MkStatePair(q2, q1)
}
/**
* Holds if there are transitions from the components of `q` to the corresponding
* components of `r` labelled with `s1` and `s2`, respectively.
*/
private predicate step(StatePair q, InputSymbol s1, InputSymbol s2, StatePair r) {
exists(State r1, State r2 | step(q, s1, s2, r1, r2) and r = mkStatePair(r1, r2))
}
/**
* Holds if there are transitions from the components of `q` to `r1` and `r2`
* labelled with `s1` and `s2`, respectively.
*
* We only consider transitions where the resulting states `(r1, r2)` are both
* inside a repetition that might backtrack.
*/
pragma[noopt]
private predicate step(StatePair q, InputSymbol s1, InputSymbol s2, State r1, State r2) {
exists(State q1, State q2 | q.getLeft() = q1 and q.getRight() = q2 |
deltaClosed(q1, s1, r1) and
deltaClosed(q2, s2, r2) and
// use noopt to force the join on `intersect` to happen last.
exists(intersect(s1, s2))
) and
stateInsideBacktracking(r1) and
stateInsideBacktracking(r2)
}
private newtype TTrace =
Nil() or
Step(InputSymbol s1, InputSymbol s2, TTrace t) { isReachableFromFork(_, _, s1, s2, t, _) }
/**
* A list of pairs of input symbols that describe a path in the product automaton
* starting from some fork state.
*/
private class Trace extends TTrace {
/** Gets a textual representation of this element. */
string toString() {
this = Nil() and result = "Nil()"
or
exists(InputSymbol s1, InputSymbol s2, Trace t | this = Step(s1, s2, t) |
result = "Step(" + s1 + ", " + s2 + ", " + t + ")"
)
}
}
/**
* Holds if `r` is reachable from `(fork, fork)` under input `w`, and there is
* a path from `r` back to `(fork, fork)` with `rem` steps.
*/
private predicate isReachableFromFork(State fork, StatePair r, Trace w, int rem) {
exists(InputSymbol s1, InputSymbol s2, Trace v |
isReachableFromFork(fork, r, s1, s2, v, rem) and
w = Step(s1, s2, v)
)
}
private predicate isReachableFromFork(
State fork, StatePair r, InputSymbol s1, InputSymbol s2, Trace v, int rem
) {
// base case
exists(State q1, State q2 |
isFork(fork, s1, s2, q1, q2) and
r = MkStatePair(q1, q2) and
v = Nil() and
rem = statePairDistToFork(r, MkStatePair(fork, fork))
)
or
// recursive case
exists(StatePair p |
isReachableFromFork(fork, p, v, rem + 1) and
step(p, s1, s2, r) and
rem = statePairDistToFork(r, MkStatePair(fork, fork))
)
}
/**
* Gets a state in the product automaton from which `(fork, fork)` is
* reachable in zero or more epsilon transitions.
*/
private StatePair getAForkPair(State fork) {
isFork(fork, _, _, _, _) and
result = MkStatePair(epsilonPred*(fork), epsilonPred*(fork))
}
/** An implementation of a chain containing chars for use by `Concretizer`. */
private module CharTreeImpl implements CharTree {
class CharNode = Trace;
CharNode getPrev(CharNode t) { t = Step(_, _, result) }
/** Holds if `n` is a trace that is used by `concretize` in `isPumpable`. */
predicate isARelevantEnd(CharNode n) {
exists(State f | isReachableFromFork(f, getAForkPair(f), n, _))
}
string getChar(CharNode t) {
exists(InputSymbol s1, InputSymbol s2 | t = Step(s1, s2, _) | result = intersect(s1, s2))
}
}
/**
* Holds if `fork` is a pumpable fork with word `w`.
*/
private predicate isPumpable(State fork, string w) {
exists(StatePair q, Trace t |
isReachableFromFork(fork, q, t, _) and
q = getAForkPair(fork) and
w = Concretizer<CharTreeImpl>::concretize(t)
)
}
/** Holds if `state` has exponential ReDoS */
predicate hasReDoSResult = ReDoSPruning<isPumpable/2>::hasReDoSResult/4;
private import semmle.code.java.regex.RegexTreeView::RegexTreeView as TreeView
// ExponentialBackTracking should be used directly from the shared pack, and not from this file.
deprecated private import codeql.regex.nfa.ExponentialBackTracking::Make<TreeView> as Dep
import Dep

File diff suppressed because it is too large Load Diff

View File

@@ -1,76 +0,0 @@
/**
* This module should provide a class hierarchy corresponding to a parse tree of regular expressions.
* This is the interface to the shared ReDoS library.
*/
private import java
import semmle.code.FileSystem
import semmle.code.java.regex.RegexTreeView
/**
* Holds if `term` is an escape class representing e.g. `\d`.
* `clazz` is which character class it represents, e.g. "d" for `\d`.
*/
predicate isEscapeClass(RegExpTerm term, string clazz) {
term.(RegExpCharacterClassEscape).getValue() = clazz
or
term.(RegExpNamedProperty).getBackslashEquivalent() = clazz
}
/**
* Holds if `term` is a possessive quantifier, e.g. `a*+`.
*/
predicate isPossessive(RegExpQuantifier term) { term.isPossessive() }
/**
* Holds if the regex that `term` is part of is used in a way that ignores any leading prefix of the input it's matched against.
*/
predicate matchesAnyPrefix(RegExpTerm term) { not term.getRegex().matchesFullString() }
/**
* Holds if the regex that `term` is part of is used in a way that ignores any trailing suffix of the input it's matched against.
*/
predicate matchesAnySuffix(RegExpTerm term) { not term.getRegex().matchesFullString() }
/**
* Holds if the regular expression should not be considered.
*
* We make the pragmatic performance optimization to ignore regular expressions in files
* that do not belong to the project code (such as installed dependencies).
*/
predicate isExcluded(RegExpParent parent) {
not exists(parent.getRegex().getLocation().getFile().getRelativePath())
or
// Regexes with many occurrences of ".*" may cause the polynomial ReDoS computation to explode, so
// we explicitly exclude these.
strictcount(int i | exists(parent.getRegex().getText().regexpFind("\\.\\*", i, _)) | i) > 10
}
/**
* A module containing predicates for determining which flags a regular expression have.
*/
module RegExpFlags {
/**
* Holds if `root` has the `i` flag for case-insensitive matching.
*/
predicate isIgnoreCase(RegExpTerm root) {
root.isRootTerm() and
root.getLiteral().isIgnoreCase()
}
/**
* Gets the flags for `root`, or the empty string if `root` has no flags.
*/
deprecated string getFlags(RegExpTerm root) {
root.isRootTerm() and
result = root.getLiteral().getFlags()
}
/**
* Holds if `root` has the `s` flag for multi-line matching.
*/
predicate isDotAll(RegExpTerm root) {
root.isRootTerm() and
root.getLiteral().isDotAll()
}
}

View File

@@ -1,19 +1,19 @@
/** Definitions and configurations for the Polynomial ReDoS query */
import semmle.code.java.security.regexp.SuperlinearBackTracking
private import semmle.code.java.regex.RegexTreeView::RegexTreeView as TreeView
import codeql.regex.nfa.SuperlinearBackTracking::Make<TreeView> as SuperlinearBackTracking
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.regex.RegexTreeView
import semmle.code.java.regex.RegexFlowConfigs
import semmle.code.java.dataflow.FlowSources
/** A sink for polynomial redos queries, where a regex is matched. */
class PolynomialRedosSink extends DataFlow::Node {
RegExpLiteral reg;
TreeView::RegExpLiteral reg;
PolynomialRedosSink() { regexMatchedAgainst(reg.getRegex(), this.asExpr()) }
/** Gets the regex that is matched against this node. */
RegExpTerm getRegExp() { result.getParent() = reg }
TreeView::RegExpTerm getRegExp() { result.getParent() = reg }
}
/**
@@ -49,7 +49,8 @@ class PolynomialRedosConfig extends TaintTracking::Configuration {
/** Holds if there is flow from `source` to `sink` that is matched against the regexp term `regexp` that is vulnerable to Polynomial ReDoS. */
predicate hasPolynomialReDoSResult(
DataFlow::PathNode source, DataFlow::PathNode sink, PolynomialBackTrackingTerm regexp
DataFlow::PathNode source, DataFlow::PathNode sink,
SuperlinearBackTracking::PolynomialBackTrackingTerm regexp
) {
any(PolynomialRedosConfig config).hasFlowPath(source, sink) and
regexp.getRootTerm() = sink.getNode().(PolynomialRedosSink).getRegExp()

View File

@@ -1,11 +1,4 @@
/**
* Provides classes for working with regular expressions that can
* perform backtracking in superlinear time.
*/
import NfaUtils
/*
* This module implements the analysis described in the paper:
* Valentin Wustholz, Oswaldo Olivo, Marijn J. H. Heule, and Isil Dillig:
* Static Detection of DoS Vulnerabilities in
@@ -42,377 +35,7 @@ import NfaUtils
* It also doesn't find all transitions in the product automaton, which can cause false negatives.
*/
/**
* Gets any root (start) state of a regular expression.
*/
private State getRootState() { result = mkMatch(any(RegExpRoot r)) }
private newtype TStateTuple =
MkStateTuple(State q1, State q2, State q3) {
// starts at (pivot, pivot, succ)
isStartLoops(q1, q3) and q1 = q2
or
step(_, _, _, _, q1, q2, q3) and FeasibleTuple::isFeasibleTuple(q1, q2, q3)
}
/**
* A state in the product automaton.
* The product automaton contains 3-tuples of states.
*
* We lazily only construct those states that we are actually
* going to need.
* Either a start state `(pivot, pivot, succ)`, or a state
* where there exists a transition from an already existing state.
*
* The exponential variant of this query (`js/redos`) uses an optimization
* trick where `q1 <= q2`. This trick cannot be used here as the order
* of the elements matter.
*/
class StateTuple extends TStateTuple {
State q1;
State q2;
State q3;
StateTuple() { this = MkStateTuple(q1, q2, q3) }
/**
* Gest a string representation of this tuple.
*/
string toString() { result = "(" + q1 + ", " + q2 + ", " + q3 + ")" }
/**
* Holds if this tuple is `(r1, r2, r3)`.
*/
pragma[noinline]
predicate isTuple(State r1, State r2, State r3) { r1 = q1 and r2 = q2 and r3 = q3 }
}
/**
* A module for determining feasible tuples for the product automaton.
*
* The implementation is split into many predicates for performance reasons.
*/
private module FeasibleTuple {
/**
* Holds if the tuple `(r1, r2, r3)` might be on path from a start-state to an end-state in the product automaton.
*/
pragma[inline]
predicate isFeasibleTuple(State r1, State r2, State r3) {
// The first element is either inside a repetition (or the start state itself)
isRepetitionOrStart(r1) and
// The last element is inside a repetition
stateInsideRepetition(r3) and
// The states are reachable in the NFA in the order r1 -> r2 -> r3
delta+(r1) = r2 and
delta+(r2) = r3 and
// The first element can reach a beginning (the "pivot" state in a `(pivot, succ)` pair).
canReachABeginning(r1) and
// The last element can reach a target (the "succ" state in a `(pivot, succ)` pair).
canReachATarget(r3)
}
/**
* Holds if `s` is either inside a repetition, or is the start state (which is a repetition).
*/
pragma[noinline]
private predicate isRepetitionOrStart(State s) { stateInsideRepetition(s) or s = getRootState() }
/**
* Holds if state `s` might be inside a backtracking repetition.
*/
pragma[noinline]
private predicate stateInsideRepetition(State s) {
s.getRepr().getParent*() instanceof InfiniteRepetitionQuantifier
}
/**
* Holds if there exists a path in the NFA from `s` to a "pivot" state
* (from a `(pivot, succ)` pair that starts the search).
*/
pragma[noinline]
private predicate canReachABeginning(State s) {
delta+(s) = any(State pivot | isStartLoops(pivot, _))
}
/**
* Holds if there exists a path in the NFA from `s` to a "succ" state
* (from a `(pivot, succ)` pair that starts the search).
*/
pragma[noinline]
private predicate canReachATarget(State s) { delta+(s) = any(State succ | isStartLoops(_, succ)) }
}
/**
* Holds if `pivot` and `succ` are a pair of loops that could be the beginning of a quadratic blowup.
*
* There is a slight implementation difference compared to the paper: this predicate requires that `pivot != succ`.
* The case where `pivot = succ` causes exponential backtracking and is handled by the `js/redos` query.
*/
predicate isStartLoops(State pivot, State succ) {
pivot != succ and
succ.getRepr() instanceof InfiniteRepetitionQuantifier and
delta+(pivot) = succ and
(
pivot.getRepr() instanceof InfiniteRepetitionQuantifier
or
pivot = mkMatch(any(RegExpRoot root))
)
}
/**
* Gets a state for which there exists a transition in the NFA from `s'.
*/
State delta(State s) { delta(s, _, result) }
/**
* Holds if there are transitions from the components of `q` to the corresponding
* components of `r` labelled with `s1`, `s2`, and `s3`, respectively.
*/
pragma[noinline]
predicate step(StateTuple q, InputSymbol s1, InputSymbol s2, InputSymbol s3, StateTuple r) {
exists(State r1, State r2, State r3 |
step(q, s1, s2, s3, r1, r2, r3) and r = MkStateTuple(r1, r2, r3)
)
}
/**
* Holds if there are transitions from the components of `q` to `r1`, `r2`, and `r3
* labelled with `s1`, `s2`, and `s3`, respectively.
*/
pragma[noopt]
predicate step(
StateTuple q, InputSymbol s1, InputSymbol s2, InputSymbol s3, State r1, State r2, State r3
) {
exists(State q1, State q2, State q3 | q.isTuple(q1, q2, q3) |
deltaClosed(q1, s1, r1) and
deltaClosed(q2, s2, r2) and
deltaClosed(q3, s3, r3) and
// use noopt to force the join on `getAThreewayIntersect` to happen last.
exists(getAThreewayIntersect(s1, s2, s3))
)
}
/**
* Gets a char that is matched by all the edges `s1`, `s2`, and `s3`.
*
* The result is not complete, and might miss some combination of edges that share some character.
*/
pragma[noinline]
string getAThreewayIntersect(InputSymbol s1, InputSymbol s2, InputSymbol s3) {
result = minAndMaxIntersect(s1, s2) and result = [intersect(s2, s3), intersect(s1, s3)]
or
result = minAndMaxIntersect(s1, s3) and result = [intersect(s2, s3), intersect(s1, s2)]
or
result = minAndMaxIntersect(s2, s3) and result = [intersect(s1, s2), intersect(s1, s3)]
}
/**
* Gets the minimum and maximum characters that intersect between `a` and `b`.
* This predicate is used to limit the size of `getAThreewayIntersect`.
*/
pragma[noinline]
string minAndMaxIntersect(InputSymbol a, InputSymbol b) {
result = [min(intersect(a, b)), max(intersect(a, b))]
}
private newtype TTrace =
Nil() or
Step(InputSymbol s1, InputSymbol s2, InputSymbol s3, TTrace t) {
isReachableFromStartTuple(_, _, t, s1, s2, s3, _, _)
}
/**
* A list of tuples of input symbols that describe a path in the product automaton
* starting from some start state.
*/
class Trace extends TTrace {
/**
* Gets a string representation of this Trace that can be used for debug purposes.
*/
string toString() {
this = Nil() and result = "Nil()"
or
exists(InputSymbol s1, InputSymbol s2, InputSymbol s3, Trace t | this = Step(s1, s2, s3, t) |
result = "Step(" + s1 + ", " + s2 + ", " + s3 + ", " + t + ")"
)
}
}
/**
* Holds if there exists a transition from `r` to `q` in the product automaton.
* Notice that the arguments are flipped, and thus the direction is backwards.
*/
pragma[noinline]
predicate tupleDeltaBackwards(StateTuple q, StateTuple r) { step(r, _, _, _, q) }
/**
* Holds if `tuple` is an end state in our search.
* That means there exists a pair of loops `(pivot, succ)` such that `tuple = (pivot, succ, succ)`.
*/
predicate isEndTuple(StateTuple tuple) { tuple = getAnEndTuple(_, _) }
/**
* Gets the minimum length of a path from `r` to some an end state `end`.
*
* The implementation searches backwards from the end-tuple.
* This approach was chosen because it is way more efficient if the first predicate given to `shortestDistances` is small.
* The `end` argument must always be an end state.
*/
int distBackFromEnd(StateTuple r, StateTuple end) =
shortestDistances(isEndTuple/1, tupleDeltaBackwards/2)(end, r, result)
/**
* Holds if there exists a pair of repetitions `(pivot, succ)` in the regular expression such that:
* `tuple` is reachable from `(pivot, pivot, succ)` in the product automaton,
* and there is a distance of `dist` from `tuple` to the nearest end-tuple `(pivot, succ, succ)`,
* and a path from a start-state to `tuple` follows the transitions in `trace`.
*/
private predicate isReachableFromStartTuple(
State pivot, State succ, StateTuple tuple, Trace trace, int dist
) {
exists(InputSymbol s1, InputSymbol s2, InputSymbol s3, Trace v |
isReachableFromStartTuple(pivot, succ, v, s1, s2, s3, tuple, dist) and
trace = Step(s1, s2, s3, v)
)
}
private predicate isReachableFromStartTuple(
State pivot, State succ, Trace trace, InputSymbol s1, InputSymbol s2, InputSymbol s3,
StateTuple tuple, int dist
) {
// base case.
exists(State q1, State q2, State q3 |
isStartLoops(pivot, succ) and
step(MkStateTuple(pivot, pivot, succ), s1, s2, s3, tuple) and
tuple = MkStateTuple(q1, q2, q3) and
trace = Nil() and
dist = distBackFromEnd(tuple, MkStateTuple(pivot, succ, succ))
)
or
// recursive case
exists(StateTuple p |
isReachableFromStartTuple(pivot, succ, p, trace, dist + 1) and
dist = distBackFromEnd(tuple, MkStateTuple(pivot, succ, succ)) and
step(p, s1, s2, s3, tuple)
)
}
/**
* Gets the tuple `(pivot, succ, succ)` from the product automaton.
*/
StateTuple getAnEndTuple(State pivot, State succ) {
isStartLoops(pivot, succ) and
result = MkStateTuple(pivot, succ, succ)
}
/** An implementation of a chain containing chars for use by `Concretizer`. */
private module CharTreeImpl implements CharTree {
class CharNode = Trace;
CharNode getPrev(CharNode t) { t = Step(_, _, _, result) }
/** Holds if `n` is used in `isPumpable`. */
predicate isARelevantEnd(CharNode n) {
exists(State pivot, State succ |
isReachableFromStartTuple(pivot, succ, getAnEndTuple(pivot, succ), n, _)
)
}
string getChar(CharNode t) {
exists(InputSymbol s1, InputSymbol s2, InputSymbol s3 | t = Step(s1, s2, s3, _) |
result = getAThreewayIntersect(s1, s2, s3)
)
}
}
/**
* Holds if matching repetitions of `pump` can:
* 1) Transition from `pivot` back to `pivot`.
* 2) Transition from `pivot` to `succ`.
* 3) Transition from `succ` to `succ`.
*
* From theorem 3 in the paper linked in the top of this file we can therefore conclude that
* the regular expression has polynomial backtracking - if a rejecting suffix exists.
*
* This predicate is used by `SuperLinearReDoSConfiguration`, and the final results are
* available in the `hasReDoSResult` predicate.
*/
predicate isPumpable(State pivot, State succ, string pump) {
exists(StateTuple q, Trace t |
isReachableFromStartTuple(pivot, succ, q, t, _) and
q = getAnEndTuple(pivot, succ) and
pump = Concretizer<CharTreeImpl>::concretize(t)
)
}
/**
* Holds if states starting in `state` can have polynomial backtracking with the string `pump`.
*/
predicate isReDoSCandidate(State state, string pump) { isPumpable(_, state, pump) }
/**
* Holds if repetitions of `pump` at `t` will cause polynomial backtracking.
*/
predicate polynomialReDoS(RegExpTerm t, string pump, string prefixMsg, RegExpTerm prev) {
exists(State s, State pivot |
ReDoSPruning<isReDoSCandidate/2>::hasReDoSResult(t, pump, s, prefixMsg) and
isPumpable(pivot, s, _) and
prev = pivot.getRepr()
)
}
/**
* Gets a message for why `term` can cause polynomial backtracking.
*/
string getReasonString(RegExpTerm term, string pump, string prefixMsg, RegExpTerm prev) {
polynomialReDoS(term, pump, prefixMsg, prev) and
result =
"Strings " + prefixMsg + "with many repetitions of '" + pump +
"' can start matching anywhere after the start of the preceeding " + prev
}
/**
* A term that may cause a regular expression engine to perform a
* polynomial number of match attempts, relative to the input length.
*/
class PolynomialBackTrackingTerm extends InfiniteRepetitionQuantifier {
string reason;
string pump;
string prefixMsg;
RegExpTerm prev;
PolynomialBackTrackingTerm() {
reason = getReasonString(this, pump, prefixMsg, prev) and
// there might be many reasons for this term to have polynomial backtracking - we pick the shortest one.
reason = min(string msg | msg = getReasonString(this, _, _, _) | msg order by msg.length(), msg)
}
/**
* Holds if all non-empty successors to the polynomial backtracking term matches the end of the line.
*/
predicate isAtEndLine() {
forall(RegExpTerm succ | this.getSuccessor+() = succ and not matchesEpsilon(succ) |
succ instanceof RegExpDollar
)
}
/**
* Gets the string that should be repeated to cause this regular expression to perform polynomially.
*/
string getPumpString() { result = pump }
/**
* Gets a message for which prefix a matching string must start with for this term to cause polynomial backtracking.
*/
string getPrefixMessage() { result = prefixMsg }
/**
* Gets a predecessor to `this`, which also loops on the pump string, and thereby causes polynomial backtracking.
*/
RegExpTerm getPreviousLoop() { result = prev }
/**
* Gets the reason for the number of match attempts.
*/
string getReason() { result = reason }
}
private import semmle.code.java.regex.RegexTreeView::RegexTreeView as TreeView
// SuperlinearBackTracking should be used directly from the shared pack, and not from this file.
deprecated private import codeql.regex.nfa.SuperlinearBackTracking::Make<TreeView> as Dep
import Dep

View File

@@ -12,14 +12,15 @@
* external/cwe/cwe-020
*/
import semmle.code.java.security.OverlyLargeRangeQuery
private import semmle.code.java.regex.RegexTreeView::RegexTreeView as TreeView
import codeql.regex.OverlyLargeRangeQuery::Make<TreeView>
RegExpCharacterClass potentialMisparsedCharClass() {
TreeView::RegExpCharacterClass potentialMisparsedCharClass() {
// nested char classes are currently misparsed
result.getAChild().(RegExpNormalChar).getValue() = "["
result.getAChild().(TreeView::RegExpNormalChar).getValue() = "["
}
from RegExpCharacterRange range, string reason
from TreeView::RegExpCharacterRange range, string reason
where
problem(range, reason) and
not range.getParent() = potentialMisparsedCharClass()

View File

@@ -17,7 +17,9 @@ import java
import semmle.code.java.security.regexp.PolynomialReDoSQuery
import DataFlow::PathGraph
from DataFlow::PathNode source, DataFlow::PathNode sink, PolynomialBackTrackingTerm regexp
from
DataFlow::PathNode source, DataFlow::PathNode sink,
SuperlinearBackTracking::PolynomialBackTrackingTerm regexp
where hasPolynomialReDoSResult(source, sink, regexp)
select sink, source, sink,
"This $@ that depends on a $@ may run slow on strings " + regexp.getPrefixMessage() +

View File

@@ -14,12 +14,12 @@
* external/cwe/cwe-400
*/
import java
import semmle.code.java.security.regexp.ExponentialBackTracking
private import semmle.code.java.regex.RegexTreeView::RegexTreeView as TreeView
import codeql.regex.nfa.ExponentialBackTracking::Make<TreeView> as ExponentialBackTracking
from RegExpTerm t, string pump, State s, string prefixMsg
from TreeView::RegExpTerm t, string pump, ExponentialBackTracking::State s, string prefixMsg
where
hasReDoSResult(t, pump, s, prefixMsg) and
ExponentialBackTracking::hasReDoSResult(t, pump, s, prefixMsg) and
// exclude verbose mode regexes for now
not t.getRegex().getAMode() = "VERBOSE"
select t,

View File

@@ -1,10 +1,12 @@
import java
import semmle.code.java.regex.RegexTreeView
import semmle.code.java.regex.regex
import semmle.code.java.regex.RegexTreeView as RegexTreeView
import semmle.code.java.regex.regex as Regex
string getQLClases(RegExpTerm t) { result = "[" + strictconcat(t.getPrimaryQLClass(), ",") + "]" }
string getQLClases(RegexTreeView::RegExpTerm t) {
result = "[" + strictconcat(t.getPrimaryQLClass(), ",") + "]"
}
query predicate parseFailures(Regex r, int i) { r.failedToParse(i) }
query predicate parseFailures(Regex::Regex r, int i) { r.failedToParse(i) }
from RegExpTerm t
from RegexTreeView::RegExpTerm t
select t, getQLClases(t)

View File

@@ -1,4 +1,3 @@
import java
import TestUtilities.InlineExpectationsTest
import semmle.code.java.security.regexp.PolynomialReDoSQuery
@@ -9,7 +8,10 @@ class HasPolyRedos extends InlineExpectationsTest {
override predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "hasPolyRedos" and
exists(DataFlow::PathNode source, DataFlow::PathNode sink, PolynomialBackTrackingTerm regexp |
exists(
DataFlow::PathNode source, DataFlow::PathNode sink,
SuperlinearBackTracking::PolynomialBackTrackingTerm regexp
|
hasPolynomialReDoSResult(source, sink, regexp) and
location = sink.getNode().getLocation() and
element = sink.getNode().toString() and

View File

@@ -1,6 +1,7 @@
import java
import TestUtilities.InlineExpectationsTest
import semmle.code.java.security.regexp.ExponentialBackTracking
private import semmle.code.java.regex.RegexTreeView::RegexTreeView as TreeView
import codeql.regex.nfa.ExponentialBackTracking::Make<TreeView> as ExponentialBackTracking
import semmle.code.java.regex.regex
class HasExpRedos extends InlineExpectationsTest {
@@ -10,8 +11,8 @@ class HasExpRedos extends InlineExpectationsTest {
override predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "hasExpRedos" and
exists(RegExpTerm t, string pump, State s, string prefixMsg |
hasReDoSResult(t, pump, s, prefixMsg) and
exists(TreeView::RegExpTerm t, string pump, ExponentialBackTracking::State s, string prefixMsg |
ExponentialBackTracking::hasReDoSResult(t, pump, s, prefixMsg) and
not t.getRegex().getAMode() = "VERBOSE" and
value = "" and
location = t.getLocation() and

View File

@@ -41,7 +41,7 @@ public class Main {
* A version identifier that should be updated every time the extractor changes in such a way that
* it may produce different tuples for the same file under the same {@link ExtractorConfig}.
*/
public static final String EXTRACTOR_VERSION = "2022-11-08";
public static final String EXTRACTOR_VERSION = "2022-11-14";
public static final Pattern NEWLINE = Pattern.compile("\n");

View File

@@ -10,6 +10,7 @@ import com.semmle.js.ast.TemplateElement;
import com.semmle.js.extractor.ASTExtractor.IdContext;
import com.semmle.ts.ast.ArrayTypeExpr;
import com.semmle.ts.ast.ConditionalTypeExpr;
import com.semmle.js.ast.DynamicImport;
import com.semmle.ts.ast.FunctionTypeExpr;
import com.semmle.ts.ast.GenericTypeExpr;
import com.semmle.ts.ast.ImportTypeExpr;
@@ -221,8 +222,7 @@ public class TypeExprKinds {
return inferTypeExpr;
}
@Override
public Integer visit(ImportTypeExpr nd, Void c) {
private Integer handleInlineImport() {
switch (idcontext) {
case NAMESPACE_BIND:
return importNamespaceAccess;
@@ -235,6 +235,17 @@ public class TypeExprKinds {
}
}
@Override
public Integer visit(ImportTypeExpr nd, Void c) {
return handleInlineImport();
}
@Override
public Integer visit(DynamicImport nd, Void c) {
// These may appear in interface 'extend' clauses
return handleInlineImport();
}
@Override
public Integer visit(OptionalTypeExpr nd, Void c) {
return optionalTypeExpr;

View File

@@ -0,0 +1 @@
interface Foo extends import("foo").Bar {}

View File

@@ -0,0 +1,139 @@
#10000=@"/dynamic-type.ts;sourcefile"
files(#10000,"/dynamic-type.ts")
#10001=@"/;folder"
folders(#10001,"/")
containerparent(#10001,#10000)
#10002=@"loc,{#10000},0,0,0,0"
locations_default(#10002,#10000,0,0,0,0)
hasLocation(#10000,#10002)
#20000=@"global_scope"
scopes(#20000,0)
#20001=@"script;{#10000},1,1"
#20002=*
lines(#20002,#20001,"interface Foo extends import(""foo"").Bar {}","
")
#20003=@"loc,{#10000},1,1,1,42"
locations_default(#20003,#10000,1,1,1,42)
hasLocation(#20002,#20003)
numlines(#20001,1,1,0)
#20004=*
tokeninfo(#20004,7,#20001,0,"interface")
#20005=@"loc,{#10000},1,1,1,9"
locations_default(#20005,#10000,1,1,1,9)
hasLocation(#20004,#20005)
#20006=*
tokeninfo(#20006,6,#20001,1,"Foo")
#20007=@"loc,{#10000},1,11,1,13"
locations_default(#20007,#10000,1,11,1,13)
hasLocation(#20006,#20007)
#20008=*
tokeninfo(#20008,7,#20001,2,"extends")
#20009=@"loc,{#10000},1,15,1,21"
locations_default(#20009,#10000,1,15,1,21)
hasLocation(#20008,#20009)
#20010=*
tokeninfo(#20010,7,#20001,3,"import")
#20011=@"loc,{#10000},1,23,1,28"
locations_default(#20011,#10000,1,23,1,28)
hasLocation(#20010,#20011)
#20012=*
tokeninfo(#20012,8,#20001,4,"(")
#20013=@"loc,{#10000},1,29,1,29"
locations_default(#20013,#10000,1,29,1,29)
hasLocation(#20012,#20013)
#20014=*
tokeninfo(#20014,4,#20001,5,"""foo""")
#20015=@"loc,{#10000},1,30,1,34"
locations_default(#20015,#10000,1,30,1,34)
hasLocation(#20014,#20015)
#20016=*
tokeninfo(#20016,8,#20001,6,")")
#20017=@"loc,{#10000},1,35,1,35"
locations_default(#20017,#10000,1,35,1,35)
hasLocation(#20016,#20017)
#20018=*
tokeninfo(#20018,8,#20001,7,".")
#20019=@"loc,{#10000},1,36,1,36"
locations_default(#20019,#10000,1,36,1,36)
hasLocation(#20018,#20019)
#20020=*
tokeninfo(#20020,6,#20001,8,"Bar")
#20021=@"loc,{#10000},1,37,1,39"
locations_default(#20021,#10000,1,37,1,39)
hasLocation(#20020,#20021)
#20022=*
tokeninfo(#20022,8,#20001,9,"{")
#20023=@"loc,{#10000},1,41,1,41"
locations_default(#20023,#10000,1,41,1,41)
hasLocation(#20022,#20023)
#20024=*
tokeninfo(#20024,8,#20001,10,"}")
#20025=@"loc,{#10000},1,42,1,42"
locations_default(#20025,#10000,1,42,1,42)
hasLocation(#20024,#20025)
#20026=*
tokeninfo(#20026,0,#20001,11,"")
#20027=@"loc,{#10000},2,1,2,0"
locations_default(#20027,#10000,2,1,2,0)
hasLocation(#20026,#20027)
toplevels(#20001,0)
#20028=@"loc,{#10000},1,1,2,0"
locations_default(#20028,#10000,1,1,2,0)
hasLocation(#20001,#20028)
#20029=@"local_type_name;{Foo};{#20000}"
local_type_names(#20029,"Foo",#20000)
#20030=*
stmts(#20030,34,#20001,0,"interfa ... .Bar {}")
hasLocation(#20030,#20003)
stmt_containers(#20030,#20001)
#20031=*
typeexprs(#20031,13,#20030,-1,"import(""foo"").Bar")
#20032=@"loc,{#10000},1,23,1,39"
locations_default(#20032,#10000,1,23,1,39)
hasLocation(#20031,#20032)
enclosing_stmt(#20031,#20030)
expr_containers(#20031,#20001)
#20033=*
typeexprs(#20033,31,#20031,0,"import(""foo"")")
#20034=@"loc,{#10000},1,23,1,35"
locations_default(#20034,#10000,1,23,1,35)
hasLocation(#20033,#20034)
enclosing_stmt(#20033,#20030)
expr_containers(#20033,#20001)
#20035=*
exprs(#20035,4,#20033,0,"""foo""")
hasLocation(#20035,#20015)
enclosing_stmt(#20035,#20030)
expr_containers(#20035,#20001)
literals("foo","""foo""",#20035)
#20036=*
regexpterm(#20036,14,#20035,0,"foo")
#20037=@"loc,{#10000},1,31,1,33"
locations_default(#20037,#10000,1,31,1,33)
hasLocation(#20036,#20037)
regexp_const_value(#20036,"foo")
#20038=*
typeexprs(#20038,15,#20031,1,"Bar")
hasLocation(#20038,#20021)
enclosing_stmt(#20038,#20030)
expr_containers(#20038,#20001)
literals("Bar","Bar",#20038)
#20039=*
typeexprs(#20039,1,#20030,0,"Foo")
hasLocation(#20039,#20007)
enclosing_stmt(#20039,#20030)
expr_containers(#20039,#20001)
literals("Foo","Foo",#20039)
typedecl(#20039,#20029)
#20040=*
entry_cfg_node(#20040,#20001)
#20041=@"loc,{#10000},1,1,1,0"
locations_default(#20041,#10000,1,1,1,0)
hasLocation(#20040,#20041)
#20042=*
exit_cfg_node(#20042,#20001)
hasLocation(#20042,#20027)
successor(#20030,#20042)
successor(#20040,#20030)
numlines(#10000,1,1,0)
filetype(#10000,"typescript")

View File

@@ -135,20 +135,11 @@ private class NosqlInjectionSinkCharacteristic extends EndpointCharacteristic {
* Characteristics that are indicative of not being a sink of any type.
*/
/**
* A characteristic that is an indicator of not being a sink of any type, because it's an argument that has a manual
* model.
*/
abstract private class OtherModeledArgumentCharacteristic extends EndpointCharacteristic {
bindingset[this]
OtherModeledArgumentCharacteristic() { any() }
}
/**
* A characteristic that is an indicator of not being a sink of any type, because it's an argument to a function of a
* builtin object.
*/
abstract private class ArgumentToBuiltinFunctionCharacteristic extends OtherModeledArgumentCharacteristic {
abstract private class ArgumentToBuiltinFunctionCharacteristic extends EndpointCharacteristic {
bindingset[this]
ArgumentToBuiltinFunctionCharacteristic() { any() }
}
@@ -156,7 +147,7 @@ abstract private class ArgumentToBuiltinFunctionCharacteristic extends OtherMode
/**
* A high-confidence characteristic that indicates that an endpoint is not a sink of any type.
*/
abstract private class NotASinkCharacteristic extends OtherModeledArgumentCharacteristic {
abstract private class NotASinkCharacteristic extends EndpointCharacteristic {
bindingset[this]
NotASinkCharacteristic() { any() }
@@ -175,7 +166,7 @@ abstract private class NotASinkCharacteristic extends OtherModeledArgumentCharac
* TODO: This class is currently not private, because the current extraction logic explicitly avoids including these
* endpoints in the training data. We might want to change this in the future.
*/
abstract class LikelyNotASinkCharacteristic extends OtherModeledArgumentCharacteristic {
abstract class LikelyNotASinkCharacteristic extends EndpointCharacteristic {
bindingset[this]
LikelyNotASinkCharacteristic() { any() }

View File

@@ -0,0 +1,5 @@
---
category: fix
---
* Fixed a bug that would cause the extractor to crash when an `import` type is used in
the `extends` clause of an `interface`.

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Data flow through the `ActiveSupport` extension `Enumerable#index_by` is now modeled.

View File

@@ -284,7 +284,17 @@ module ActiveSupport {
preservesValue = true
}
}
// TODO: index_by, index_with, pick, pluck (they require Hash dataflow)
private class IndexBySummary extends SimpleSummarizedCallable {
IndexBySummary() { this = "index_by" }
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Argument[self].Element[any]" and
output = ["Argument[block].Parameter[0]", "ReturnValue.Element[?]"] and
preservesValue = true
}
}
// TODO: index_with, pick, pluck (they require Hash dataflow)
}
}

View File

@@ -258,6 +258,34 @@ edges
| hash_extensions.rb:58:10:58:10 | x [element :a] : | hash_extensions.rb:58:10:58:14 | ...[...] |
| hash_extensions.rb:59:10:59:10 | x [element :b] : | hash_extensions.rb:59:10:59:14 | ...[...] |
| hash_extensions.rb:59:10:59:10 | x [element :b] : | hash_extensions.rb:59:10:59:14 | ...[...] |
| hash_extensions.rb:67:15:67:25 | call to source : | hash_extensions.rb:68:9:68:14 | values [element 0] : |
| hash_extensions.rb:67:15:67:25 | call to source : | hash_extensions.rb:68:9:68:14 | values [element 0] : |
| hash_extensions.rb:67:28:67:38 | call to source : | hash_extensions.rb:68:9:68:14 | values [element 1] : |
| hash_extensions.rb:67:28:67:38 | call to source : | hash_extensions.rb:68:9:68:14 | values [element 1] : |
| hash_extensions.rb:67:41:67:51 | call to source : | hash_extensions.rb:68:9:68:14 | values [element 2] : |
| hash_extensions.rb:67:41:67:51 | call to source : | hash_extensions.rb:68:9:68:14 | values [element 2] : |
| hash_extensions.rb:68:9:68:14 | values [element 0] : | hash_extensions.rb:68:9:71:7 | call to index_by [element] : |
| hash_extensions.rb:68:9:68:14 | values [element 0] : | hash_extensions.rb:68:9:71:7 | call to index_by [element] : |
| hash_extensions.rb:68:9:68:14 | values [element 0] : | hash_extensions.rb:68:29:68:33 | value : |
| hash_extensions.rb:68:9:68:14 | values [element 0] : | hash_extensions.rb:68:29:68:33 | value : |
| hash_extensions.rb:68:9:68:14 | values [element 1] : | hash_extensions.rb:68:9:71:7 | call to index_by [element] : |
| hash_extensions.rb:68:9:68:14 | values [element 1] : | hash_extensions.rb:68:9:71:7 | call to index_by [element] : |
| hash_extensions.rb:68:9:68:14 | values [element 1] : | hash_extensions.rb:68:29:68:33 | value : |
| hash_extensions.rb:68:9:68:14 | values [element 1] : | hash_extensions.rb:68:29:68:33 | value : |
| hash_extensions.rb:68:9:68:14 | values [element 2] : | hash_extensions.rb:68:9:71:7 | call to index_by [element] : |
| hash_extensions.rb:68:9:68:14 | values [element 2] : | hash_extensions.rb:68:9:71:7 | call to index_by [element] : |
| hash_extensions.rb:68:9:68:14 | values [element 2] : | hash_extensions.rb:68:29:68:33 | value : |
| hash_extensions.rb:68:9:68:14 | values [element 2] : | hash_extensions.rb:68:29:68:33 | value : |
| hash_extensions.rb:68:9:71:7 | call to index_by [element] : | hash_extensions.rb:73:10:73:10 | h [element] : |
| hash_extensions.rb:68:9:71:7 | call to index_by [element] : | hash_extensions.rb:73:10:73:10 | h [element] : |
| hash_extensions.rb:68:9:71:7 | call to index_by [element] : | hash_extensions.rb:74:10:74:10 | h [element] : |
| hash_extensions.rb:68:9:71:7 | call to index_by [element] : | hash_extensions.rb:74:10:74:10 | h [element] : |
| hash_extensions.rb:68:29:68:33 | value : | hash_extensions.rb:69:14:69:18 | value |
| hash_extensions.rb:68:29:68:33 | value : | hash_extensions.rb:69:14:69:18 | value |
| hash_extensions.rb:73:10:73:10 | h [element] : | hash_extensions.rb:73:10:73:16 | ...[...] |
| hash_extensions.rb:73:10:73:10 | h [element] : | hash_extensions.rb:73:10:73:16 | ...[...] |
| hash_extensions.rb:74:10:74:10 | h [element] : | hash_extensions.rb:74:10:74:16 | ...[...] |
| hash_extensions.rb:74:10:74:10 | h [element] : | hash_extensions.rb:74:10:74:16 | ...[...] |
nodes
| active_support.rb:10:9:10:18 | call to source : | semmle.label | call to source : |
| active_support.rb:11:10:11:10 | x : | semmle.label | x : |
@@ -594,6 +622,32 @@ nodes
| hash_extensions.rb:59:10:59:10 | x [element :b] : | semmle.label | x [element :b] : |
| hash_extensions.rb:59:10:59:14 | ...[...] | semmle.label | ...[...] |
| hash_extensions.rb:59:10:59:14 | ...[...] | semmle.label | ...[...] |
| hash_extensions.rb:67:15:67:25 | call to source : | semmle.label | call to source : |
| hash_extensions.rb:67:15:67:25 | call to source : | semmle.label | call to source : |
| hash_extensions.rb:67:28:67:38 | call to source : | semmle.label | call to source : |
| hash_extensions.rb:67:28:67:38 | call to source : | semmle.label | call to source : |
| hash_extensions.rb:67:41:67:51 | call to source : | semmle.label | call to source : |
| hash_extensions.rb:67:41:67:51 | call to source : | semmle.label | call to source : |
| hash_extensions.rb:68:9:68:14 | values [element 0] : | semmle.label | values [element 0] : |
| hash_extensions.rb:68:9:68:14 | values [element 0] : | semmle.label | values [element 0] : |
| hash_extensions.rb:68:9:68:14 | values [element 1] : | semmle.label | values [element 1] : |
| hash_extensions.rb:68:9:68:14 | values [element 1] : | semmle.label | values [element 1] : |
| hash_extensions.rb:68:9:68:14 | values [element 2] : | semmle.label | values [element 2] : |
| hash_extensions.rb:68:9:68:14 | values [element 2] : | semmle.label | values [element 2] : |
| hash_extensions.rb:68:9:71:7 | call to index_by [element] : | semmle.label | call to index_by [element] : |
| hash_extensions.rb:68:9:71:7 | call to index_by [element] : | semmle.label | call to index_by [element] : |
| hash_extensions.rb:68:29:68:33 | value : | semmle.label | value : |
| hash_extensions.rb:68:29:68:33 | value : | semmle.label | value : |
| hash_extensions.rb:69:14:69:18 | value | semmle.label | value |
| hash_extensions.rb:69:14:69:18 | value | semmle.label | value |
| hash_extensions.rb:73:10:73:10 | h [element] : | semmle.label | h [element] : |
| hash_extensions.rb:73:10:73:10 | h [element] : | semmle.label | h [element] : |
| hash_extensions.rb:73:10:73:16 | ...[...] | semmle.label | ...[...] |
| hash_extensions.rb:73:10:73:16 | ...[...] | semmle.label | ...[...] |
| hash_extensions.rb:74:10:74:10 | h [element] : | semmle.label | h [element] : |
| hash_extensions.rb:74:10:74:10 | h [element] : | semmle.label | h [element] : |
| hash_extensions.rb:74:10:74:16 | ...[...] | semmle.label | ...[...] |
| hash_extensions.rb:74:10:74:16 | ...[...] | semmle.label | ...[...] |
subpaths
#select
| active_support.rb:182:10:182:13 | ...[...] | active_support.rb:180:10:180:17 | call to source : | active_support.rb:182:10:182:13 | ...[...] | $@ | active_support.rb:180:10:180:17 | call to source : | call to source : |
@@ -622,3 +676,12 @@ subpaths
| hash_extensions.rb:56:10:56:14 | ...[...] | hash_extensions.rb:50:52:50:61 | call to taint : | hash_extensions.rb:56:10:56:14 | ...[...] | $@ | hash_extensions.rb:50:52:50:61 | call to taint : | call to taint : |
| hash_extensions.rb:58:10:58:14 | ...[...] | hash_extensions.rb:50:14:50:23 | call to taint : | hash_extensions.rb:58:10:58:14 | ...[...] | $@ | hash_extensions.rb:50:14:50:23 | call to taint : | call to taint : |
| hash_extensions.rb:59:10:59:14 | ...[...] | hash_extensions.rb:50:29:50:38 | call to taint : | hash_extensions.rb:59:10:59:14 | ...[...] | $@ | hash_extensions.rb:50:29:50:38 | call to taint : | call to taint : |
| hash_extensions.rb:69:14:69:18 | value | hash_extensions.rb:67:15:67:25 | call to source : | hash_extensions.rb:69:14:69:18 | value | $@ | hash_extensions.rb:67:15:67:25 | call to source : | call to source : |
| hash_extensions.rb:69:14:69:18 | value | hash_extensions.rb:67:28:67:38 | call to source : | hash_extensions.rb:69:14:69:18 | value | $@ | hash_extensions.rb:67:28:67:38 | call to source : | call to source : |
| hash_extensions.rb:69:14:69:18 | value | hash_extensions.rb:67:41:67:51 | call to source : | hash_extensions.rb:69:14:69:18 | value | $@ | hash_extensions.rb:67:41:67:51 | call to source : | call to source : |
| hash_extensions.rb:73:10:73:16 | ...[...] | hash_extensions.rb:67:15:67:25 | call to source : | hash_extensions.rb:73:10:73:16 | ...[...] | $@ | hash_extensions.rb:67:15:67:25 | call to source : | call to source : |
| hash_extensions.rb:73:10:73:16 | ...[...] | hash_extensions.rb:67:28:67:38 | call to source : | hash_extensions.rb:73:10:73:16 | ...[...] | $@ | hash_extensions.rb:67:28:67:38 | call to source : | call to source : |
| hash_extensions.rb:73:10:73:16 | ...[...] | hash_extensions.rb:67:41:67:51 | call to source : | hash_extensions.rb:73:10:73:16 | ...[...] | $@ | hash_extensions.rb:67:41:67:51 | call to source : | call to source : |
| hash_extensions.rb:74:10:74:16 | ...[...] | hash_extensions.rb:67:15:67:25 | call to source : | hash_extensions.rb:74:10:74:16 | ...[...] | $@ | hash_extensions.rb:67:15:67:25 | call to source : | call to source : |
| hash_extensions.rb:74:10:74:16 | ...[...] | hash_extensions.rb:67:28:67:38 | call to source : | hash_extensions.rb:74:10:74:16 | ...[...] | $@ | hash_extensions.rb:67:28:67:38 | call to source : | call to source : |
| hash_extensions.rb:74:10:74:16 | ...[...] | hash_extensions.rb:67:41:67:51 | call to source : | hash_extensions.rb:74:10:74:16 | ...[...] | $@ | hash_extensions.rb:67:41:67:51 | call to source : | call to source : |

View File

@@ -62,3 +62,16 @@ def m_extract!(x)
end
m_extract!(:c)
def m_index_by
values = [source("a"), source("b"), source("c")]
h = values.index_by do |value|
sink value # $ hasValueFlow=a $ hasValueFlow=b $ hasValueFlow=c
make_key(value)
end
sink h[:foo] # $ hasValueFlow=a $ hasValueFlow=b $ hasValueFlow=c
sink h[:bar] # $ hasValueFlow=a $ hasValueFlow=b $ hasValueFlow=c
end
m_index_by()

View File

@@ -1,4 +1,4 @@
/*
/**
* This module implements the analysis described in the paper:
* Valentin Wustholz, Oswaldo Olivo, Marijn J. H. Heule, and Isil Dillig:
* Static Detection of DoS Vulnerabilities in

View File

@@ -1,6 +1,20 @@
import swift
private import codeql.swift.dataflow.DataFlow
/**
* A unit class for adding additional taint steps.
*
* Extend this class to add additional taint steps that should apply to all
* taint configurations.
*/
class AdditionalTaintStep extends Unit {
/**
* Holds if the step from `node1` to `node2` should be considered a taint
* step for all configurations.
*/
abstract predicate step(DataFlow::Node node1, DataFlow::Node node2);
}
/**
* A `Content` that should be implicitly regarded as tainted whenever an object with such `Content`
* is itself tainted.

View File

@@ -64,6 +64,8 @@ private module Cached {
or
// flow through a flow summary (extension of `SummaryModelCsv`)
FlowSummaryImpl::Private::Steps::summaryLocalStep(nodeFrom, nodeTo, false)
or
any(AdditionalTaintStep a).step(nodeFrom, nodeTo)
}
/**

View File

@@ -0,0 +1,56 @@
import swift
/** The creation of an `AEXMLParser`. */
class AexmlParser extends ApplyExpr {
AexmlParser() {
this.getStaticTarget().(ConstructorDecl).getEnclosingDecl() instanceof AexmlParserDecl
}
}
/** The creation of an `AEXMLDocument`. */
class AexmlDocument extends ApplyExpr {
AexmlDocument() {
this.getStaticTarget().(ConstructorDecl).getEnclosingDecl() instanceof AexmlDocumentDecl
}
}
/** A call to `AEXMLDocument.loadXML(_:)`. */
class AexmlDocumentLoadXml extends MethodApplyExpr {
AexmlDocumentLoadXml() {
exists(MethodDecl f |
this.getStaticTarget() = f and
f.hasName("loadXML(_:)") and
f.getEnclosingDecl() instanceof AexmlDocumentDecl
)
}
}
/** The class `AEXMLParser`. */
class AexmlParserDecl extends ClassDecl {
AexmlParserDecl() { this.getFullName() = "AEXMLParser" }
}
/** The class `AEXMLDocument`. */
class AexmlDocumentDecl extends ClassDecl {
AexmlDocumentDecl() { this.getFullName() = "AEXMLDocument" }
}
/** A reference to the field `AEXMLOptions.ParserSettings.shouldResolveExternalEntities`. */
class AexmlShouldResolveExternalEntities extends MemberRefExpr {
AexmlShouldResolveExternalEntities() {
exists(FieldDecl f | this.getMember() = f |
f.getName() = "shouldResolveExternalEntities" and
f.getEnclosingDecl().(NominalTypeDecl).getType() instanceof AexmlOptionsParserSettingsType
)
}
}
/** The type `AEXMLOptions`. */
class AexmlOptionsType extends StructType {
AexmlOptionsType() { this.getFullName() = "AEXMLOptions" }
}
/** The type `AEXMLOptions.ParserSettings`. */
class AexmlOptionsParserSettingsType extends StructType {
AexmlOptionsParserSettingsType() { this.getFullName() = "AEXMLOptions.ParserSettings" }
}

View File

@@ -2,6 +2,7 @@
import swift
private import codeql.swift.dataflow.DataFlow
private import codeql.swift.frameworks.AEXML
/** A data flow sink for XML external entities (XXE) vulnerabilities. */
abstract class XxeSink extends DataFlow::Node { }
@@ -90,3 +91,75 @@ private class NodeLoadExternalEntitiesAlways extends VarDecl {
this.getEnclosingDecl().(StructDecl).getFullName() = "XMLNode.Options"
}
}
/** The XML argument of an `AEXMLDocument` vulnerable to XXE. */
private class AexmlDocumentSink extends XxeSink {
AexmlDocumentSink() {
// `AEXMLDocument` initialized with vulnerable options.
exists(ApplyExpr call | this.asExpr() = call.getArgument(0).getExpr() |
call.(VulnerableAexmlDocument)
.getStaticTarget()
.hasName(["init(xml:options:)", "init(xml:encoding:options:)"])
or
// `loadXML` called on a vulnerable AEXMLDocument.
DataFlow::localExprFlow(any(VulnerableAexmlDocument v),
call.(AexmlDocumentLoadXml).getQualifier())
)
}
}
/** The XML argument of an `AEXMLParser` initialized with an `AEXMLDocument` vulnerable to XXE. */
private class AexmlParserSink extends XxeSink {
AexmlParserSink() {
exists(AexmlParser parser | this.asExpr() = parser.getArgument(1).getExpr() |
DataFlow::localExprFlow(any(VulnerableAexmlDocument v), parser.getArgument(0).getExpr())
)
}
}
/** The creation of an `AEXMLDocument` that receives a vulnerable `AEXMLOptions` argument. */
private class VulnerableAexmlDocument extends AexmlDocument {
VulnerableAexmlDocument() {
exists(AexmlOptions optionsArgument, VulnerableAexmlOptions vulnOpts |
this.getAnArgument().getExpr() = optionsArgument and
DataFlow::localExprFlow(vulnOpts, optionsArgument)
)
}
}
/**
* An `AEXMLOptions` object which contains a `parserSettings` with `shouldResolveExternalEntities`
* set to `true`.
*/
private class VulnerableAexmlOptions extends AexmlOptions {
VulnerableAexmlOptions() {
exists(
AexmlParserSettings parserSettings, AexmlShouldResolveExternalEntities sree, AssignExpr a
|
a.getSource() = any(BooleanLiteralExpr b | b.getValue() = true) and
a.getDest() = sree and
sree.(MemberRefExpr).getBase() = parserSettings and
parserSettings.(MemberRefExpr).getBase() = this
)
}
}
/** An expression of type `AEXMLOptions.ParserSettings`. */
private class AexmlParserSettings extends Expr {
pragma[inline]
AexmlParserSettings() {
this.getType() instanceof AexmlOptionsParserSettingsType or
this.getType() = any(OptionalType t | t.getBaseType() instanceof AexmlOptionsParserSettingsType) or
this.getType() = any(LValueType t | t.getObjectType() instanceof AexmlOptionsParserSettingsType)
}
}
/** An expression of type `AEXMLOptions`. */
private class AexmlOptions extends Expr {
pragma[inline]
AexmlOptions() {
this.getType() instanceof AexmlOptionsType or
this.getType() = any(OptionalType t | t.getBaseType() instanceof AexmlOptionsType) or
this.getType() = any(LValueType t | t.getObjectType() instanceof AexmlOptionsType)
}
}

View File

@@ -0,0 +1,148 @@
// --- stubs ---
class Data {
init<S>(_ elements: S) {}
}
struct URL {
init?(string: String) {}
}
extension String {
struct Encoding: Hashable {
let rawValue: UInt
static let utf8 = String.Encoding(rawValue: 1)
}
init(contentsOf: URL) {
let data = ""
self.init(data)
}
}
class AEXMLElement {}
struct AEXMLOptions {
var parserSettings = ParserSettings()
struct ParserSettings {
public var shouldResolveExternalEntities = false
}
}
class AEXMLDocument {
init(root: AEXMLElement? = nil, options: AEXMLOptions) {}
init(xml: Data, options: AEXMLOptions = AEXMLOptions()) {}
init(xml: String, encoding: String.Encoding, options: AEXMLOptions) {}
func loadXML(_: Data) {}
}
class AEXMLParser {
init(document: AEXMLDocument, data: Data) {}
}
// --- tests ---
func testString() {
var options = AEXMLOptions()
options.parserSettings.shouldResolveExternalEntities = true
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let _ = AEXMLDocument(xml: remoteString, encoding: String.Encoding.utf8, options: options) // $ hasXXE=50
}
func testStringSafeImplicit() {
var options = AEXMLOptions()
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let _ = AEXMLDocument(xml: remoteString, encoding: String.Encoding.utf8, options: options) // NO XXE
}
func testStringSafeExplicit() {
var options = AEXMLOptions()
options.parserSettings.shouldResolveExternalEntities = false
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let _ = AEXMLDocument(xml: remoteString, encoding: String.Encoding.utf8, options: options) // NO XXE
}
func testData() {
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let remoteData = Data(remoteString)
var options = AEXMLOptions()
options.parserSettings.shouldResolveExternalEntities = true
let _ = AEXMLDocument(xml: remoteData, options: options) // $ hasXXE=70
}
func testDataSafeImplicit() {
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let remoteData = Data(remoteString)
var options = AEXMLOptions()
let _ = AEXMLDocument(xml: remoteData, options: options) // NO XXE
}
func testDataSafeExplicit() {
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let remoteData = Data(remoteString)
var options = AEXMLOptions()
options.parserSettings.shouldResolveExternalEntities = false
let _ = AEXMLDocument(xml: remoteData, options: options) // NO XXE
}
func testDataLoadXml() {
var options = AEXMLOptions()
options.parserSettings.shouldResolveExternalEntities = true
let doc = AEXMLDocument(root: nil, options: options)
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let remoteData = Data(remoteString)
doc.loadXML(remoteData) // $ hasXXE=97
}
func testDataLoadXmlSafeImplicit() {
var options = AEXMLOptions()
let doc = AEXMLDocument(root: nil, options: options)
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let remoteData = Data(remoteString)
doc.loadXML(remoteData) // NO XXE
}
func testDataLoadXmlSafeExplicit() {
var options = AEXMLOptions()
options.parserSettings.shouldResolveExternalEntities = false
let doc = AEXMLDocument(root: nil, options: options)
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let remoteData = Data(remoteString)
doc.loadXML(remoteData) // NO XXE
}
func testParser() {
var options = AEXMLOptions()
options.parserSettings.shouldResolveExternalEntities = true
let doc = AEXMLDocument(root: nil, options: options)
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let remoteData = Data(remoteString)
let _ = AEXMLParser(document: doc, data: remoteData) // $ hasXXE=126
}
func testParserSafeImplicit() {
var options = AEXMLOptions()
let doc = AEXMLDocument(root: nil, options: options)
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let remoteData = Data(remoteString)
let _ = AEXMLParser(document: doc, data: remoteData) // NO XXE
}
func testParserSafeExplicit() {
var options = AEXMLOptions()
options.parserSettings.shouldResolveExternalEntities = false
let doc = AEXMLDocument(root: nil, options: options)
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let remoteData = Data(remoteString)
let _ = AEXMLParser(document: doc, data: remoteData) // NO XXE
}