diff --git a/CODEOWNERS b/CODEOWNERS index da670301f30..89529f95924 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,3 +17,9 @@ /java/ql/src/semmle/code/java/dataflow/internal/DataFlowImplCommon.qll @github/codeql-java @github/codeql-go /java/ql/src/semmle/code/java/dataflow/internal/tainttracking1/TaintTrackingImpl.qll @github/codeql-java @github/codeql-go /java/ql/src/semmle/code/java/dataflow/internal/tainttracking2/TaintTrackingImpl.qll @github/codeql-java @github/codeql-go + +# CodeQL tools and associated docs +/docs/codeql-cli/ @github/codeql-cli-reviewers +/docs/codeql-for-visual-studio-code/ @github/codeql-vscode-reviewers +/docs/ql-language-reference/ @github/codeql-frontend-reviewers +/docs/query-*-style-guide.md @github/codeql-analysis-reviewers \ No newline at end of file diff --git a/cpp/change-notes/2021-06-15-path-sensitive-stack-reachability.md b/cpp/change-notes/2021-06-15-path-sensitive-stack-reachability.md deleted file mode 100644 index 2c57422d860..00000000000 --- a/cpp/change-notes/2021-06-15-path-sensitive-stack-reachability.md +++ /dev/null @@ -1,4 +0,0 @@ -lgtm,codescanning -* The `StackVariableReachability` library now ignores some paths that contain an infeasible combination - of conditionals. These improvements primarily affect the queries `cpp/uninitialized-local` and - `cpp/use-after-free`. \ No newline at end of file diff --git a/cpp/change-notes/2021-07-13-cleartext-storage-file.md b/cpp/change-notes/2021-07-13-cleartext-storage-file.md new file mode 100644 index 00000000000..9fcfd1fcdbd --- /dev/null +++ b/cpp/change-notes/2021-07-13-cleartext-storage-file.md @@ -0,0 +1,3 @@ +lgtm,codescanning +* The "Cleartext storage of sensitive information in file" (cpp/cleartext-storage-file) query now uses dataflow to produce additional results. +* Heuristics in the SensitiveExprs.qll library have been improved, making the "Cleartext storage of sensitive information in file" (cpp/cleartext-storage-file), "Cleartext storage of sensitive information in buffer" (cpp/cleartext-storage-buffer) and "Cleartext storage of sensitive information in an SQLite" (cpp/cleartext-storage-database) queries more accurate. diff --git a/cpp/change-notes/2021-07-20-toctou-race-condition.md b/cpp/change-notes/2021-07-20-toctou-race-condition.md new file mode 100644 index 00000000000..4999b017048 --- /dev/null +++ b/cpp/change-notes/2021-07-20-toctou-race-condition.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* Improvements have been made to the `cpp/toctou-race-condition` query, both to find more correct results and fewer false positive results. diff --git a/cpp/change-notes/2021-07-29-virtual-function-declaration-specifiers.md b/cpp/change-notes/2021-07-29-virtual-function-declaration-specifiers.md new file mode 100644 index 00000000000..137ac735c17 --- /dev/null +++ b/cpp/change-notes/2021-07-29-virtual-function-declaration-specifiers.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* Virtual function specifiers are now accessible via the new predicates on `Function` (`.isDeclaredVirtual`, `.isOverride`, and `.isFinal`). \ No newline at end of file diff --git a/cpp/ql/src/Likely Bugs/Memory Management/UninitializedLocal.ql b/cpp/ql/src/Likely Bugs/Memory Management/UninitializedLocal.ql index 5861167659f..dd87b7883a0 100644 --- a/cpp/ql/src/Likely Bugs/Memory Management/UninitializedLocal.ql +++ b/cpp/ql/src/Likely Bugs/Memory Management/UninitializedLocal.ql @@ -41,7 +41,7 @@ DeclStmt declWithNoInit(LocalVariable v) { result.getADeclaration() = v and not exists(v.getInitializer()) and /* The type of the variable is not stack-allocated. */ - not allocatedType(v.getType()) + exists(Type t | t = v.getType() | not allocatedType(t)) } class UninitialisedLocalReachability extends StackVariableReachability { diff --git a/cpp/ql/src/Security/CWE/CWE-311/CleartextFileWrite.ql b/cpp/ql/src/Security/CWE/CWE-311/CleartextFileWrite.ql index aa90ff9567c..62bd05d95aa 100644 --- a/cpp/ql/src/Security/CWE/CWE-311/CleartextFileWrite.ql +++ b/cpp/ql/src/Security/CWE/CWE-311/CleartextFileWrite.ql @@ -5,7 +5,7 @@ * @kind problem * @problem.severity warning * @security-severity 7.5 - * @precision medium + * @precision high * @id cpp/cleartext-storage-file * @tags security * external/cwe/cwe-313 @@ -14,10 +14,40 @@ import cpp import semmle.code.cpp.security.SensitiveExprs import semmle.code.cpp.security.FileWrite +import semmle.code.cpp.dataflow.DataFlow +import semmle.code.cpp.valuenumbering.GlobalValueNumbering -from FileWrite w, SensitiveExpr source, Expr dest +/** + * An operation on a filename. + */ +predicate filenameOperation(FunctionCall op, Expr path) { + exists(string name | name = op.getTarget().getName() | + name = + [ + "remove", "unlink", "rmdir", "rename", "fopen", "open", "freopen", "_open", "_wopen", + "_wfopen", "_fsopen", "_wfsopen", "chmod", "chown", "stat", "lstat", "fstat", "access", + "_access", "_waccess", "_access_s", "_waccess_s" + ] and + path = op.getArgument(0) + or + name = ["fopen_s", "wfopen_s", "rename"] and + path = op.getArgument(1) + ) +} + +predicate isFileName(GVN gvn) { + exists(FunctionCall op, Expr path | + filenameOperation(op, path) and + gvn = globalValueNumber(path) + ) +} + +from FileWrite w, SensitiveExpr source, Expr mid, Expr dest where - source = w.getASource() and - dest = w.getDest() + DataFlow::localFlow(DataFlow::exprNode(source), DataFlow::exprNode(mid)) and + mid = w.getASource() and + dest = w.getDest() and + not isFileName(globalValueNumber(source)) and // file names are not passwords + not exists(string convChar | convChar = w.getSourceConvChar(mid) | not convChar = ["s", "S"]) // ignore things written with other conversion characters select w, "This write into file '" + dest.toString() + "' may contain unencrypted data from $@", source, "this source." diff --git a/cpp/ql/src/Security/CWE/CWE-367/TOCTOUFilesystemRace.ql b/cpp/ql/src/Security/CWE/CWE-367/TOCTOUFilesystemRace.ql index f5bed0bee64..49f95f039c1 100644 --- a/cpp/ql/src/Security/CWE/CWE-367/TOCTOUFilesystemRace.ql +++ b/cpp/ql/src/Security/CWE/CWE-367/TOCTOUFilesystemRace.ql @@ -6,7 +6,7 @@ * @kind problem * @problem.severity warning * @security-severity 7.7 - * @precision medium + * @precision high * @id cpp/toctou-race-condition * @tags security * external/cwe/cwe-367 @@ -16,59 +16,60 @@ import cpp import semmle.code.cpp.controlflow.Guards /** - * An operation on a filename. + * An operation on a filename that is likely to modify the corresponding file + * and may return an indication of success. * - * Note: we're not interested in operations on file descriptors, as they - * are better behaved. + * Note: we're not interested in operations where the file is specified by a + * descriptor, rather than a filename, as they are better behaved. We are + * interested in functions that take a filename and return a file descriptor, + * however. */ FunctionCall filenameOperation(Expr path) { exists(string name | name = result.getTarget().getName() | - ( - name = "remove" or - name = "unlink" or - name = "rmdir" or - name = "rename" or - name = "chmod" or - name = "chown" or - name = "fopen" or - name = "open" or - name = "freopen" or - name = "_open" or - name = "_wopen" or - name = "_wfopen" - ) and + name = + [ + "remove", "unlink", "rmdir", "rename", "fopen", "open", "freopen", "_open", "_wopen", + "_wfopen", "_fsopen", "_wfsopen" + ] and result.getArgument(0) = path or - ( - name = "fopen_s" or - name = "wfopen_s" - ) and + name = ["fopen_s", "wfopen_s", "rename"] and result.getArgument(1) = path ) + or + result = sensitiveFilenameOperation(path) +} + +/** + * An operation on a filename that is likely to modify the security properties + * of the corresponding file and may return an indication of success. + */ +FunctionCall sensitiveFilenameOperation(Expr path) { + exists(string name | name = result.getTarget().getName() | + name = ["chmod", "chown"] and + result.getArgument(0) = path + ) } /** - * A use of `access` (or similar) on a filename. + * An operation on a filename that returns information in the return value but + * does not modify the corresponding file. For example, `access`. */ FunctionCall accessCheck(Expr path) { exists(string name | name = result.getTarget().getName() | - name = "access" or - name = "_access" or - name = "_waccess" or - name = "_access_s" or - name = "_waccess_s" + name = ["access", "_access", "_waccess", "_access_s", "_waccess_s"] ) and path = result.getArgument(0) } /** - * A use of `stat` (or similar) on a filename. + * An operation on a filename that returns information via a pointer argument + * and any return value, but does not modify the corresponding file. For + * example, `stat`. */ FunctionCall stat(Expr path, Expr buf) { exists(string name | name = result.getTarget().getName() | - name = "stat" or - name = "lstat" or - name = "fstat" or + name = ["stat", "lstat", "fstat"] or name.matches("\\_stat%") or name.matches("\\_wstat%") ) and @@ -77,7 +78,7 @@ FunctionCall stat(Expr path, Expr buf) { } /** - * Holds if `use` points to `source`, either by being the same or by + * Holds if `use` refers to `source`, either by being the same or by * one step of variable indirection. */ predicate referenceTo(Expr source, Expr use) { @@ -88,36 +89,45 @@ predicate referenceTo(Expr source, Expr use) { ) } -from FunctionCall fc, Expr check, Expr checkUse, Expr opUse +from Expr check, Expr checkPath, FunctionCall use, Expr usePath where - // checkUse looks like a check on a filename + // `check` looks like a check on a filename ( - // either: - // an access check - check = accessCheck(checkUse) - or - // a stat - check = stat(checkUse, _) + ( + // either: + // an access check + check = accessCheck(checkPath) + or + // a stat + check = stat(checkPath, _) + or + // access to a member variable on the stat buf + // (morally, this should be a use-use pair, but it seems unlikely + // that this variable will get reused in practice) + exists(Expr call, Expr e, Variable v | + call = stat(checkPath, e) and + e.getAChild*().(VariableAccess).getTarget() = v and + check.(VariableAccess).getTarget() = v and + not e.getAChild*() = check // the call that writes to the pointer is not where the pointer is checked. + ) + ) and + // `op` looks like an operation on a filename + use = filenameOperation(usePath) or // another filename operation (null pointers can indicate errors) - check = filenameOperation(checkUse) - or - // access to a member variable on the stat buf - // (morally, this should be a use-use pair, but it seems unlikely - // that this variable will get reused in practice) - exists(Variable buf | exists(stat(checkUse, buf.getAnAccess())) | - check.(VariableAccess).getQualifier() = buf.getAnAccess() - ) + check = filenameOperation(checkPath) and + // `op` looks like a sensitive operation on a filename + use = sensitiveFilenameOperation(usePath) ) and - // checkUse and opUse refer to the same SSA variable - exists(SsaDefinition def, StackVariable v | def.getAUse(v) = checkUse and def.getAUse(v) = opUse) and - // opUse looks like an operation on a filename - fc = filenameOperation(opUse) and - // the return value of check is used (possibly with one step of - // variable indirection) in a guard which controls fc + // `checkPath` and `usePath` refer to the same SSA variable + exists(SsaDefinition def, StackVariable v | + def.getAUse(v) = checkPath and def.getAUse(v) = usePath + ) and + // the return value of `check` is used (possibly with one step of + // variable indirection) in a guard which controls `use` exists(GuardCondition guard | referenceTo(check, guard.getAChild*()) | - guard.controls(fc.(ControlFlowNode).getBasicBlock(), _) + guard.controls(use.(ControlFlowNode).getBasicBlock(), _) ) -select fc, +select use, "The $@ being operated upon was previously $@, but the underlying file may have been changed since then.", - opUse, "filename", check, "checked" + usePath, "filename", check, "checked" diff --git a/cpp/ql/src/semmle/code/cpp/Function.qll b/cpp/ql/src/semmle/code/cpp/Function.qll index a5ef1f8ca5e..72e89b997dc 100644 --- a/cpp/ql/src/semmle/code/cpp/Function.qll +++ b/cpp/ql/src/semmle/code/cpp/Function.qll @@ -82,9 +82,23 @@ class Function extends Declaration, ControlFlowNode, AccessHolder, @function { /** Holds if this function is inline. */ predicate isInline() { this.hasSpecifier("inline") } - /** Holds if this function is virtual. */ + /** + * Holds if this function is virtual. + * + * Unlike `isDeclaredVirtual()`, `isVirtual()` holds even if the function + * is not explicitly declared with the `virtual` specifier. + */ predicate isVirtual() { this.hasSpecifier("virtual") } + /** Holds if this function is declared with the `virtual` specifier. */ + predicate isDeclaredVirtual() { this.hasSpecifier("declared_virtual") } + + /** Holds if this function is declared with the `override` specifier. */ + predicate isOverride() { this.hasSpecifier("override") } + + /** Holds if this function is declared with the `final` specifier. */ + predicate isFinal() { this.hasSpecifier("final") } + /** * Holds if this function is deleted. * This may be because it was explicitly deleted with an `= delete` diff --git a/cpp/ql/src/semmle/code/cpp/controlflow/DefinitionsAndUses.qll b/cpp/ql/src/semmle/code/cpp/controlflow/DefinitionsAndUses.qll index ae98b1600a1..f6eb0a8a645 100644 --- a/cpp/ql/src/semmle/code/cpp/controlflow/DefinitionsAndUses.qll +++ b/cpp/ql/src/semmle/code/cpp/controlflow/DefinitionsAndUses.qll @@ -208,7 +208,7 @@ private predicate bbSuccessorEntryReachesDefOrUse( boolean skipsFirstLoopAlwaysTrueUponEntry ) { exists(BasicBlock succ, boolean succSkipsFirstLoopAlwaysTrueUponEntry | - bbSuccessorEntryReachesLoopInvariant0(bb, succ, skipsFirstLoopAlwaysTrueUponEntry, + bbSuccessorEntryReachesLoopInvariant(bb, succ, skipsFirstLoopAlwaysTrueUponEntry, succSkipsFirstLoopAlwaysTrueUponEntry) | bbEntryReachesDefOrUseLocally(succ, v, defOrUse) and diff --git a/cpp/ql/src/semmle/code/cpp/controlflow/StackVariableReachability.qll b/cpp/ql/src/semmle/code/cpp/controlflow/StackVariableReachability.qll index f7b35e33f81..6c50d254faa 100644 --- a/cpp/ql/src/semmle/code/cpp/controlflow/StackVariableReachability.qll +++ b/cpp/ql/src/semmle/code/cpp/controlflow/StackVariableReachability.qll @@ -3,250 +3,7 @@ * reachability involving stack variables. */ -private import semmle.code.cpp.controlflow.Guards -private import semmle.code.cpp.valuenumbering.GlobalValueNumbering - -/** A `GuardCondition` which appear in a control-flow path to a sink. */ -abstract private class LogicalGuardCondition extends GuardCondition { - LogicalGuardCondition() { - // Either the `GuardCondition` is part of the path from a source to a sink - revBbSuccessorEntryReaches0(_, this.getBasicBlock(), _, _, _) - or - // or it controls the basic block that contains the source node. - this.controls(any(BasicBlock bb | fwdBbEntryReachesLocally(bb, _, _, _)), _) - } - - /** - * Holds if the truth of this logical expression having value `wholeIsTrue` - * implies that the truth of the child expression `part` has truth value `partIsTrue`. - */ - abstract predicate impliesCondition( - LogicalGuardCondition e, boolean testIsTrue, boolean condIsTrue - ); -} - -private class BinaryLogicalGuardCondition extends LogicalGuardCondition, BinaryLogicalOperation { - override predicate impliesCondition( - LogicalGuardCondition e, boolean testIsTrue, boolean condIsTrue - ) { - this.impliesValue(e, testIsTrue, condIsTrue) - } -} - -private class VariableGuardCondition extends LogicalGuardCondition, VariableAccess { - override predicate impliesCondition( - LogicalGuardCondition e, boolean testIsTrue, boolean condIsTrue - ) { - this = e and - ( - testIsTrue = true and condIsTrue = true - or - testIsTrue = false and condIsTrue = false - ) - } -} - -private class NotGuardCondition extends LogicalGuardCondition, NotExpr { - override predicate impliesCondition( - LogicalGuardCondition e, boolean testIsTrue, boolean condIsTrue - ) { - e = this.getOperand() and - ( - testIsTrue = true and - condIsTrue = false - or - testIsTrue = false and - condIsTrue = true - ) - } -} - -private newtype TCondition = - MkCondition(LogicalGuardCondition guard, boolean testIsTrue) { testIsTrue = [false, true] } - -private class Condition extends MkCondition { - boolean testIsTrue; - LogicalGuardCondition guard; - - Condition() { this = MkCondition(guard, testIsTrue) } - - /** - * Holds if this condition having the value `this.getTruthValue()` implies that `cond` has truth - * value `cond.getTruthValue()`. - */ - string toString() { result = guard.toString() + " == " + testIsTrue.toString() } - - /** Gets the value of this `Condition`. */ - boolean getTruthValue() { result = testIsTrue } - - LogicalGuardCondition getCondition() { result = guard } - - pragma[nomagic] - predicate impliesCondition(Condition cond) { - exists(LogicalGuardCondition other | - other = cond.getCondition() and - this.getCondition() - .impliesCondition(globalValueNumber(other).getAnExpr(), - pragma[only_bind_into](pragma[only_bind_out](testIsTrue)), - pragma[only_bind_into](pragma[only_bind_out](cond.getTruthValue()))) - ) - } - - /** Gets the negated expression represented by this `Condition`, if any. */ - private Condition negate() { - result.getCondition() = guard and - result.getTruthValue() = testIsTrue.booleanNot() - } - - /** - * Holds if this condition having the value `this.getTruthValue()` implies that `cond` cannot have - * the truth value `cond.getTruthValue()`. - */ - final predicate refutesCondition(Condition cond) { this.impliesCondition(cond.negate()) } - - /** Gets the `Location` of the expression that generated this `Condition`. */ - Location getLocation() { result = guard.getLocation() } -} - -/** - * Gets a `Condition` that controls `b`. That is, to enter `b` the condition must hold. - */ -private Condition getADirectCondition(BasicBlock b) { - result.getCondition().controls(b, result.getTruthValue()) -} - -/** - * Like the shared dataflow library, the reachability analysis is split into two stages: - * In the first stage, we compute an overapproximation of the possible control-flow paths where we don't - * reason about path conditions. This stage is split into phases: A forward phase (computed by the - * predicates prefixes with `fwd`), and a reverse phase (computed by the predicates prefixed with `rev`). - * - * The forward phease computes the set of control-flow nodes reachable from a given `source` and `v` such - * that `config.isSource(source, v)` holds. - * - * See the QLDoc on `revBbSuccessorEntryReaches0` for a description of what the reverse phase computes. - */ -private predicate fwdBbSuccessorEntryReaches0( - ControlFlowNode source, BasicBlock bb, SemanticStackVariable v, - boolean skipsFirstLoopAlwaysTrueUponEntry, StackVariableReachability config -) { - fwdBbEntryReachesLocally(bb, v, source, config) and - skipsFirstLoopAlwaysTrueUponEntry = false - or - exists(BasicBlock pred, boolean predSkipsFirstLoopAlwaysTrueUponEntry | - bbSuccessorEntryReachesLoopInvariant0(pred, bb, predSkipsFirstLoopAlwaysTrueUponEntry, - skipsFirstLoopAlwaysTrueUponEntry) - | - // Note we cannot filter out barriers at this point. - // See the comment in `revBbSuccessorEntryReaches0` for an explanation why, - fwdBbSuccessorEntryReaches0(source, pred, v, predSkipsFirstLoopAlwaysTrueUponEntry, config) - ) -} - -/** - * The second phase of the first stages computes, for each `source` and `v` pair such - * that `config.isSource(source, v)`, which sinks are reachable from that `(source, v)` pair. - */ -private predicate revBbSuccessorEntryReaches0( - ControlFlowNode source, BasicBlock bb, SemanticStackVariable v, - boolean skipsFirstLoopAlwaysTrueUponEntry, StackVariableReachability config -) { - exists(BasicBlock succ, boolean succSkipsFirstLoopAlwaysTrueUponEntry | - fwdBbSuccessorEntryReaches0(source, bb, v, skipsFirstLoopAlwaysTrueUponEntry, config) and - bbSuccessorEntryReachesLoopInvariant0(bb, succ, skipsFirstLoopAlwaysTrueUponEntry, - succSkipsFirstLoopAlwaysTrueUponEntry) - | - revBbEntryReachesLocally(succ, v, _, config) and - succSkipsFirstLoopAlwaysTrueUponEntry = false - or - // Note: We cannot rule out a successor block that contain a barrier here (like we do later in - // `bbSuccessorEntryReaches`) as we might later discover that the only way to get through a piece of - // code is through that barrier, and we want to discover this in - // `bbSuccessorEntryReachesLoopInvariant`. As an example, consider this piece of code: - // ``` - // if(b) { (1) source(); } - // (2) if(b) { (3) barrier(); } - // (4) sink(); - // ``` - // here, we want the successor relation to contain: - // 1 -> {2}, 2 -> {3, 4} - // since the second stage will deduce that the edge (2) -> (3) is unconditional (as b is always true - // if we start at `source()`), and so there is actually no path from (1) to (4) without going through - // a barrier. - revBbSuccessorEntryReaches0(source, succ, v, succSkipsFirstLoopAlwaysTrueUponEntry, config) - ) -} - -private predicate successorExitsLoop(BasicBlock pred, BasicBlock succ, Loop loop) { - pred.getASuccessor() = succ and - bbDominates(loop.getStmt(), pred) and - not bbDominates(loop.getStmt(), succ) -} - -private predicate successorExitsFirstDisjunct(BasicBlock pred, BasicBlock succ) { - exists(LogicalOrExpr orExpr | orExpr instanceof GuardCondition | - pred.getAFalseSuccessor() = succ and - pred.contains(orExpr.getLeftOperand()) - ) -} - -/** - * When we exit a loop, we filter out the conditions that arise from the loop's guard. - * To see why this is necessary, consider this example: - * ``` - * (1) source(); - * while (b) { (2) ... } - * (3) sink(); - * ``` - * If we keep all the conditions when we transition from (2) to (3) we learn that `b` is true at - * (3), but since we exited the loop we also learn that `b` is false at 3. - * Thus, when we transition from (2) to (3) we discard all those conditions that are true at (2), - * but NOT true at (3). - */ -private predicate isLoopCondition(LogicalGuardCondition cond, BasicBlock pred, BasicBlock bb) { - exists(Loop loop, boolean testIsTrue | successorExitsLoop(pred, bb, loop) | - // the resulting `Condition` holds inside the loop - cond.controls(pred, testIsTrue) and - // but not prior to the loop. - not cond.controls(loop.getBasicBlock(), testIsTrue) - ) -} - -/** - * When we leave the first disjunct we throw away the condition that says the the first disjunct is - * false. To see why this is necessary, consider this example: - * ``` - * if((1) b1 || (2) b2) { (3) ... } - * ``` - * it holds that `b1 == false` controls (2), and since (2) steps to (3) we learn that `b1 == false ` - * holds at (3). So we filter out the conditions that we learn from leaving taking the false - * branch in a disjunction. - */ -private predicate isDisjunctionCondition(LogicalGuardCondition cond, BasicBlock pred, BasicBlock bb) { - exists(boolean testIsTrue | successorExitsFirstDisjunct(pred, bb) | - // the resulting `Condition` holds after evaluating the left-hand side - cond.controls(bb, testIsTrue) and - // but not before evaluating the left-hand side. - not cond.controls(pred, testIsTrue) - ) -} - -private predicate isLoopVariantCondition(LogicalGuardCondition cond, BasicBlock pred, BasicBlock bb) { - exists(Loop loop | - bb.getEnd() = loop.getCondition() and - pred.getASuccessor() = bb and - bbDominates(bb, pred) and - loopVariant(cond.getAChild*(), loop) - ) -} - -private predicate loopVariant(VariableAccess e, Loop loop) { - exists(SsaDefinition d | d.getAUse(e.getTarget()) = e | - d.getAnUltimateDefiningValue(e.getTarget()) = loop.getCondition().getAChild*() or - d.getAnUltimateDefiningValue(e.getTarget()).getEnclosingStmt().getParent*() = loop.getStmt() or - d.getAnUltimateDefiningValue(e.getTarget()) = loop.(ForStmt).getUpdate().getAChild*() - ) -} +import cpp /** * A reachability analysis for control-flow nodes involving stack variables. @@ -303,8 +60,7 @@ abstract class StackVariableReachability extends string { * ``` * * In addition to using a better performing implementation, this analysis - * accounts for loops where the condition is provably true upon entry, and discards paths that require - * an infeasible combination of guard conditions (for example, `if(b) { ... }` and `if(!b) { ... }`). + * accounts for loops where the condition is provably true upon entry. */ predicate reaches(ControlFlowNode source, SemanticStackVariable v, ControlFlowNode sink) { /* @@ -324,184 +80,46 @@ abstract class StackVariableReachability extends string { j > i and sink = bb.getNode(j) and isSink(sink, v) and - not isBarrier(bb.getNode(pragma[only_bind_into]([i + 1 .. j - 1])), v) + not exists(int k | isBarrier(bb.getNode(k), v) | k in [i + 1 .. j - 1]) ) or not exists(int k | isBarrier(bb.getNode(k), v) | k > i) and - bbSuccessorEntryReaches(source, bb, v, sink, _) - ) - } - - private Condition getASinkCondition(SemanticStackVariable v) { - exists(BasicBlock bb | - revBbEntryReachesLocally(bb, v, _, this) and - result.getCondition().controls(bb, result.getTruthValue()) - ) - } - - private Condition getABarrierCondition(SemanticStackVariable v) { - exists(BasicBlock bb | - isBarrier(bb.getANode(), v) and - result.getCondition().controls(bb, result.getTruthValue()) - ) - } - - /** - * Gets a condition with a known truth value in `bb` when the control-flow starts at the source - * node `source` and we're tracking reachability using variable `v` (that is, - * `this.isSource(source, v)` holds). - * - * This predicate is `pragma[noopt]` as it seems difficult to get the correct join order for the - * recursive case otherwise: - * revBbSuccessorEntryReaches0(bb) -> getASuccessor -> prev_delta -> - * revBbSuccessorEntryReaches0(pred) -> {isLoopCondition, isDisjunctionCondition, isLoopVariantCondition} - */ - pragma[noopt] - private Condition getACondition(ControlFlowNode source, SemanticStackVariable v, BasicBlock bb) { - revBbSuccessorEntryReaches0(source, bb, v, _, this) and - ( - result = getADirectCondition(bb) and - ( - exists(Condition c | - c = getASinkCondition(v) and - result.refutesCondition(c) - ) - or - exists(Condition c | - c = getABarrierCondition(v) and - result.impliesCondition(c) - ) - ) - or - exists(BasicBlock pred | - pred.getASuccessor() = bb and - result = getACondition(source, v, pred) and - revBbSuccessorEntryReaches0(source, pred, v, _, this) and - exists(LogicalGuardCondition c | c = result.getCondition() | - not isLoopCondition(c, pred, bb) and - not isDisjunctionCondition(c, pred, bb) and - not isLoopVariantCondition(c, pred, bb) - ) - ) - ) - } - - pragma[nomagic] - private predicate bbSuccessorEntryReachesLoopInvariantSucc( - ControlFlowNode source, BasicBlock pred, SemanticStackVariable v, BasicBlock succ, - boolean predSkipsFirstLoopAlwaysTrueUponEntry - ) { - revBbSuccessorEntryReaches0(source, pragma[only_bind_into](pred), v, - predSkipsFirstLoopAlwaysTrueUponEntry, this) and - pred.getASuccessor() = succ - } - - pragma[nomagic] - private predicate bbSuccessorEntryReachesLoopInvariantCand( - ControlFlowNode source, BasicBlock pred, SemanticStackVariable v, BasicBlock succ, - boolean predSkipsFirstLoopAlwaysTrueUponEntry, boolean succSkipsFirstLoopAlwaysTrueUponEntry - ) { - bbSuccessorEntryReachesLoopInvariantSucc(source, pragma[only_bind_into](pred), v, succ, - predSkipsFirstLoopAlwaysTrueUponEntry) and - bbSuccessorEntryReachesLoopInvariant0(pred, succ, predSkipsFirstLoopAlwaysTrueUponEntry, - succSkipsFirstLoopAlwaysTrueUponEntry) - } - - /** - * Holds if `pred`, `succ`, `predSkipsFirstLoopAlwaysTrueUponEntry` and - * `succSkipsFirstLoopAlwaysTrueUponEntry` satisfy the loop invariants specified in the QLDoc - * for `bbSuccessorEntryReachesLoopInvariant0`. - * - * In addition, this predicate: - * 1. Rules out successor blocks that are unreachable due to contradictory path conditions. - * 2. Refines the successor relation when the edge `pred -> succ` is a conditional edge whose truth - * value is known. - */ - pragma[nomagic] - private predicate bbSuccessorEntryReachesLoopInvariant( - ControlFlowNode source, BasicBlock pred, SemanticStackVariable v, BasicBlock succ, - boolean predSkipsFirstLoopAlwaysTrueUponEntry, boolean succSkipsFirstLoopAlwaysTrueUponEntry - ) { - bbSuccessorEntryReachesLoopInvariantCand(source, pred, v, succ, - predSkipsFirstLoopAlwaysTrueUponEntry, succSkipsFirstLoopAlwaysTrueUponEntry) and - not exists(Condition cond, Condition direct | - cond = getACondition(source, v, pred) and - direct = pragma[only_bind_out](getADirectCondition(succ)) and - cond.refutesCondition(direct) - ) and - ( - // If we picked the successor edge corresponding to a condition being true, there must not be - // another path condition that refutes that the condition is true. - not exists(Condition cond | cond = getACondition(source, v, pred) | - succ = pred.getATrueSuccessor() and - cond.refutesCondition(pragma[only_bind_out](MkCondition(pred.getEnd(), true))) - ) and - // If we picked the successor edge corresponding to a condition being false, there must not be - // another path condition that refutes that the condition is false. - not exists(Condition cond | cond = getACondition(source, v, pred) | - succ = pred.getAFalseSuccessor() and - cond.refutesCondition(pragma[only_bind_out](MkCondition(pred.getEnd(), false))) - ) + bbSuccessorEntryReaches(bb, v, sink, _) ) } private predicate bbSuccessorEntryReaches( - ControlFlowNode source, BasicBlock bb, SemanticStackVariable v, ControlFlowNode node, + BasicBlock bb, SemanticStackVariable v, ControlFlowNode node, boolean skipsFirstLoopAlwaysTrueUponEntry ) { exists(BasicBlock succ, boolean succSkipsFirstLoopAlwaysTrueUponEntry | - bbSuccessorEntryReachesLoopInvariant(source, bb, v, succ, skipsFirstLoopAlwaysTrueUponEntry, + bbSuccessorEntryReachesLoopInvariant(bb, succ, skipsFirstLoopAlwaysTrueUponEntry, succSkipsFirstLoopAlwaysTrueUponEntry) | - revBbEntryReachesLocally(succ, v, node, this) and + bbEntryReachesLocally(succ, v, node) and succSkipsFirstLoopAlwaysTrueUponEntry = false or - bbSuccessorEntryReachesLoopInvariant(source, bb, v, succ, skipsFirstLoopAlwaysTrueUponEntry, - succSkipsFirstLoopAlwaysTrueUponEntry) and - not isBarrier(pragma[only_bind_out](succ.getANode()), v) and - pragma[only_bind_into](this) - .bbSuccessorEntryReaches(source, succ, v, node, succSkipsFirstLoopAlwaysTrueUponEntry) + not isBarrier(succ.getNode(_), v) and + bbSuccessorEntryReaches(succ, v, node, succSkipsFirstLoopAlwaysTrueUponEntry) ) } -} -private predicate fwdBbEntryReachesLocally( - BasicBlock bb, SemanticStackVariable v, ControlFlowNode node, StackVariableReachability config -) { - exists(int n | - node = bb.getNode(n) and - config.isSource(node, v) and - ( - not exists(lastBarrierIndexIn(bb, v, config)) + private predicate bbEntryReachesLocally( + BasicBlock bb, SemanticStackVariable v, ControlFlowNode node + ) { + exists(int n | + node = bb.getNode(n) and + isSink(node, v) + | + not exists(this.firstBarrierIndexIn(bb, v)) or - lastBarrierIndexIn(bb, v, config) <= n + n <= this.firstBarrierIndexIn(bb, v) ) - ) -} + } -private predicate revBbEntryReachesLocally( - BasicBlock bb, SemanticStackVariable v, ControlFlowNode node, StackVariableReachability config -) { - exists(int n | - node = bb.getNode(n) and - config.isSink(node, v) - | - not exists(firstBarrierIndexIn(bb, v, config)) - or - n <= firstBarrierIndexIn(bb, v, config) - ) -} - -private int firstBarrierIndexIn( - BasicBlock bb, SemanticStackVariable v, StackVariableReachability config -) { - result = min(int m | config.isBarrier(bb.getNode(m), v)) -} - -private int lastBarrierIndexIn( - BasicBlock bb, SemanticStackVariable v, StackVariableReachability config -) { - result = max(int m | config.isBarrier(bb.getNode(m), v)) + private int firstBarrierIndexIn(BasicBlock bb, SemanticStackVariable v) { + result = min(int m | isBarrier(bb.getNode(m), v)) + } } /** @@ -564,7 +182,7 @@ private predicate bbLoopConditionAlwaysTrueUponEntrySuccessor( * is provably true upon entry, then `succ` is not allowed to skip * that loop (`succSkipsFirstLoopAlwaysTrueUponEntry = false`). */ -predicate bbSuccessorEntryReachesLoopInvariant0( +predicate bbSuccessorEntryReachesLoopInvariant( BasicBlock pred, BasicBlock succ, boolean predSkipsFirstLoopAlwaysTrueUponEntry, boolean succSkipsFirstLoopAlwaysTrueUponEntry ) { @@ -678,52 +296,10 @@ abstract class StackVariableReachabilityWithReassignment extends StackVariableRe ) } - private predicate bbSuccessorEntryReaches( - BasicBlock bb, SemanticStackVariable v, ControlFlowNode node, - boolean skipsFirstLoopAlwaysTrueUponEntry - ) { - exists(BasicBlock succ, boolean succSkipsFirstLoopAlwaysTrueUponEntry | - bbSuccessorEntryReachesLoopInvariant0(bb, succ, skipsFirstLoopAlwaysTrueUponEntry, - succSkipsFirstLoopAlwaysTrueUponEntry) - | - revBbEntryReachesLocally(succ, v, node, this) and - succSkipsFirstLoopAlwaysTrueUponEntry = false - or - not isBarrier(succ.getNode(_), v) and - bbSuccessorEntryReaches(succ, v, node, succSkipsFirstLoopAlwaysTrueUponEntry) - ) - } - - private predicate reaches0(ControlFlowNode source, SemanticStackVariable v, ControlFlowNode sink) { - /* - * Implementation detail: the predicates in this class are a generalization of - * those in DefinitionsAndUses.qll, and should be kept in sync. - * - * Unfortunately, caching of abstract predicates does not work well, so the - * predicates in DefinitionsAndUses.qll cannot use this library. - */ - - exists(BasicBlock bb, int i | - isSource(source, v) and - bb.getNode(i) = source and - not bb.isUnreachable() - | - exists(int j | - j > i and - sink = bb.getNode(j) and - isSink(sink, v) and - not isBarrier(bb.getNode(pragma[only_bind_into]([i + 1 .. j - 1])), v) - ) - or - not exists(int k | isBarrier(bb.getNode(k), v) | k > i) and - bbSuccessorEntryReaches(bb, v, sink, _) - ) - } - private predicate reassignment( ControlFlowNode source, SemanticStackVariable v, ControlFlowNode def, SemanticStackVariable v0 ) { - reaches0(source, v, def) and + StackVariableReachability.super.reaches(source, v, def) and exprDefinition(v0, def, v.getAnAccess()) } @@ -789,11 +365,11 @@ abstract class StackVariableReachabilityExt extends string { boolean skipsFirstLoopAlwaysTrueUponEntry ) { exists(BasicBlock succ, boolean succSkipsFirstLoopAlwaysTrueUponEntry | - bbSuccessorEntryReachesLoopInvariant0(bb, succ, skipsFirstLoopAlwaysTrueUponEntry, + bbSuccessorEntryReachesLoopInvariant(bb, succ, skipsFirstLoopAlwaysTrueUponEntry, succSkipsFirstLoopAlwaysTrueUponEntry) and not isBarrier(source, bb.getEnd(), succ.getStart(), v) | - this.bbEntryReachesLocally(source, succ, v, node) and + bbEntryReachesLocally(source, succ, v, node) and succSkipsFirstLoopAlwaysTrueUponEntry = false or not exists(int k | isBarrier(source, succ.getNode(k), succ.getNode(k + 1), v)) and diff --git a/cpp/ql/src/semmle/code/cpp/dataflow/internal/DataFlowUtil.qll b/cpp/ql/src/semmle/code/cpp/dataflow/internal/DataFlowUtil.qll index 0d723ac05b6..01338eaeff4 100644 --- a/cpp/ql/src/semmle/code/cpp/dataflow/internal/DataFlowUtil.qll +++ b/cpp/ql/src/semmle/code/cpp/dataflow/internal/DataFlowUtil.qll @@ -735,7 +735,12 @@ private module FieldFlow { private class FieldConfiguration extends Configuration { FieldConfiguration() { this = "FieldConfiguration" } - override predicate isSource(Node source) { storeStep(source, _, _) } + override predicate isSource(Node source) { + storeStep(source, _, _) + or + // Also mark `foo(a.b);` as a source when `a.b` may be overwritten by `foo`. + readStep(_, _, any(Node node | node.asExpr() = source.asDefiningArgument())) + } override predicate isSink(Node sink) { readStep(_, _, sink) } diff --git a/cpp/ql/src/semmle/code/cpp/security/FileWrite.qll b/cpp/ql/src/semmle/code/cpp/security/FileWrite.qll index c51a428ab5e..7c3d893b471 100644 --- a/cpp/ql/src/semmle/code/cpp/security/FileWrite.qll +++ b/cpp/ql/src/semmle/code/cpp/security/FileWrite.qll @@ -19,6 +19,15 @@ class FileWrite extends Expr { * Gets the expression for the object being written to. */ Expr getDest() { fileWrite(this, _, result) } + + /** + * Gets the conversion character for this write, if it exists and is known. For example in the following code the write of `value1` has conversion character `"s"`, whereas the write of `value2` has no conversion specifier. + * ``` + * fprintf(file, "%s", value1); + * stream << value2; + * ``` + */ + string getSourceConvChar(Expr source) { fileWriteWithConvChar(this, source, result) } } /** @@ -150,3 +159,20 @@ private predicate fileWrite(Call write, Expr source, Expr dest) { // file stream using '<<', 'put' or 'write' fileStreamChain(write, source, dest) } + +/** + * Whether the function call is a write to a file from 'source' with + * conversion character 'conv'. Does not hold if there isn't a conversion + * character, or if it is unknown (for example the format string is not a + * constant). + */ +private predicate fileWriteWithConvChar(FormattingFunctionCall ffc, Expr source, string conv) { + // fprintf + exists(FormattingFunction f, int n | + f = ffc.getTarget() and + source = ffc.getFormatArgument(n) + | + exists(f.getOutputParameterIndex(true)) and + conv = ffc.(FormattingFunctionCall).getFormat().(FormatLiteral).getConversionChar(n) + ) +} diff --git a/cpp/ql/src/semmle/code/cpp/security/Overflow.qll b/cpp/ql/src/semmle/code/cpp/security/Overflow.qll index b8ed406cb4a..84b10be15af 100644 --- a/cpp/ql/src/semmle/code/cpp/security/Overflow.qll +++ b/cpp/ql/src/semmle/code/cpp/security/Overflow.qll @@ -5,6 +5,8 @@ import cpp import semmle.code.cpp.controlflow.Dominance +// `GlobalValueNumbering` is only imported to prevent IR re-evaluation. +private import semmle.code.cpp.valuenumbering.GlobalValueNumbering import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis import semmle.code.cpp.rangeanalysis.RangeAnalysisUtils diff --git a/cpp/ql/src/semmle/code/cpp/security/SensitiveExprs.qll b/cpp/ql/src/semmle/code/cpp/security/SensitiveExprs.qll index 553cc98351c..22e0ee71b66 100644 --- a/cpp/ql/src/semmle/code/cpp/security/SensitiveExprs.qll +++ b/cpp/ql/src/semmle/code/cpp/security/SensitiveExprs.qll @@ -14,14 +14,13 @@ private predicate suspicious(string s) { ( s.matches("%password%") or s.matches("%passwd%") or - s.matches("%account%") or - s.matches("%accnt%") or s.matches("%trusted%") ) and not ( - s.matches("%hashed%") or - s.matches("%encrypted%") or - s.matches("%crypt%") + s.matches("%hash%") or + s.matches("%crypt%") or + s.matches("%file%") or + s.matches("%path%") ) } @@ -29,14 +28,20 @@ private predicate suspicious(string s) { * A variable that might contain a password or other sensitive information. */ class SensitiveVariable extends Variable { - SensitiveVariable() { suspicious(getName().toLowerCase()) } + SensitiveVariable() { + suspicious(getName().toLowerCase()) and + not this.getUnspecifiedType() instanceof IntegralType + } } /** * A function that might return a password or other sensitive information. */ class SensitiveFunction extends Function { - SensitiveFunction() { suspicious(getName().toLowerCase()) } + SensitiveFunction() { + suspicious(getName().toLowerCase()) and + not this.getUnspecifiedType() instanceof IntegralType + } } /** diff --git a/cpp/ql/test/library-tests/clang_ms/element.expected b/cpp/ql/test/library-tests/clang_ms/element.expected index 8ca381ef406..b6d3b124eb5 100644 --- a/cpp/ql/test/library-tests/clang_ms/element.expected +++ b/cpp/ql/test/library-tests/clang_ms/element.expected @@ -79,6 +79,7 @@ | file://:0:0:0:0 | declaration of 1st parameter | | file://:0:0:0:0 | declared_constexpr | | file://:0:0:0:0 | declared_constinit | +| file://:0:0:0:0 | declared_virtual | | file://:0:0:0:0 | decltype(nullptr) | | file://:0:0:0:0 | definition of fp_offset | | file://:0:0:0:0 | definition of gp_offset | @@ -91,6 +92,7 @@ | file://:0:0:0:0 | explicit | | file://:0:0:0:0 | extern | | file://:0:0:0:0 | far | +| file://:0:0:0:0 | final | | file://:0:0:0:0 | float | | file://:0:0:0:0 | forceinline | | file://:0:0:0:0 | fp_offset | diff --git a/cpp/ql/test/library-tests/conditions/elements.expected b/cpp/ql/test/library-tests/conditions/elements.expected index cdab5557305..e9aaae34b3f 100644 --- a/cpp/ql/test/library-tests/conditions/elements.expected +++ b/cpp/ql/test/library-tests/conditions/elements.expected @@ -49,6 +49,7 @@ | file://:0:0:0:0 | const __va_list_tag & | | file://:0:0:0:0 | declared_constexpr | | file://:0:0:0:0 | declared_constinit | +| file://:0:0:0:0 | declared_virtual | | file://:0:0:0:0 | decltype(nullptr) | | file://:0:0:0:0 | definition of | | file://:0:0:0:0 | definition of fp_offset | @@ -62,6 +63,7 @@ | file://:0:0:0:0 | explicit | | file://:0:0:0:0 | extern | | file://:0:0:0:0 | far | +| file://:0:0:0:0 | final | | file://:0:0:0:0 | float | | file://:0:0:0:0 | forceinline | | file://:0:0:0:0 | fp_offset | diff --git a/cpp/ql/test/library-tests/dataflow/dataflow-tests/localFlow.expected b/cpp/ql/test/library-tests/dataflow/dataflow-tests/localFlow.expected index 75cdaee69ac..bb14729a12b 100644 --- a/cpp/ql/test/library-tests/dataflow/dataflow-tests/localFlow.expected +++ b/cpp/ql/test/library-tests/dataflow/dataflow-tests/localFlow.expected @@ -70,3 +70,8 @@ | test.cpp:391:11:391:13 | tmp | test.cpp:391:10:391:13 | & ... | | test.cpp:391:17:391:23 | source1 | test.cpp:391:10:391:13 | ref arg & ... | | test.cpp:391:17:391:23 | source1 | test.cpp:391:16:391:23 | & ... | +| test.cpp:480:67:480:67 | s | test.cpp:481:21:481:21 | s | +| test.cpp:480:67:480:67 | s | test.cpp:482:20:482:20 | s | +| test.cpp:481:21:481:21 | s [post update] | test.cpp:482:20:482:20 | s | +| test.cpp:481:24:481:30 | ref arg content | test.cpp:482:23:482:29 | content | +| test.cpp:482:23:482:29 | content | test.cpp:483:9:483:17 | p_content | diff --git a/cpp/ql/test/library-tests/dataflow/dataflow-tests/test.cpp b/cpp/ql/test/library-tests/dataflow/dataflow-tests/test.cpp index f59552aa2dd..afceddfabd0 100644 --- a/cpp/ql/test/library-tests/dataflow/dataflow-tests/test.cpp +++ b/cpp/ql/test/library-tests/dataflow/dataflow-tests/test.cpp @@ -470,3 +470,15 @@ void viaOutparam() { intOutparamSource(&x); sink(x); // $ ast,ir } + +void writes_to_content(void*); + +struct MyStruct { + int* content; +}; + +void local_field_flow_def_by_ref_steps_with_local_flow(MyStruct * s) { + writes_to_content(s->content); + int* p_content = s->content; + sink(*p_content); +} \ No newline at end of file diff --git a/cpp/ql/test/library-tests/dataflow/taint-tests/localTaint.expected b/cpp/ql/test/library-tests/dataflow/taint-tests/localTaint.expected index ce939661c92..27cfbb6430a 100644 --- a/cpp/ql/test/library-tests/dataflow/taint-tests/localTaint.expected +++ b/cpp/ql/test/library-tests/dataflow/taint-tests/localTaint.expected @@ -496,9 +496,13 @@ | map.cpp:49:7:49:7 | f [post update] | map.cpp:51:7:51:7 | f | | | map.cpp:49:7:49:7 | f [post update] | map.cpp:53:30:53:30 | f | | | map.cpp:49:7:49:7 | f [post update] | map.cpp:59:6:59:6 | f | | +| map.cpp:49:9:49:13 | ref arg first | map.cpp:54:9:54:13 | first | | +| map.cpp:49:9:49:13 | ref arg first | map.cpp:60:9:60:13 | first | | | map.cpp:50:7:50:7 | f [post update] | map.cpp:51:7:51:7 | f | | | map.cpp:50:7:50:7 | f [post update] | map.cpp:53:30:53:30 | f | | | map.cpp:50:7:50:7 | f [post update] | map.cpp:59:6:59:6 | f | | +| map.cpp:50:9:50:14 | ref arg second | map.cpp:55:9:55:14 | second | | +| map.cpp:50:9:50:14 | ref arg second | map.cpp:61:9:61:14 | second | | | map.cpp:53:30:53:30 | f | map.cpp:54:7:54:7 | g | | | map.cpp:53:30:53:30 | f | map.cpp:55:7:55:7 | g | | | map.cpp:53:30:53:30 | f | map.cpp:56:7:56:7 | g | | @@ -3395,6 +3399,7 @@ | smart_pointer.cpp:125:20:125:20 | call to operator-> [post update] | smart_pointer.cpp:125:18:125:19 | ref arg p1 | TAINT | | smart_pointer.cpp:125:22:125:22 | q | smart_pointer.cpp:125:18:125:22 | call to shared_ptr | | | smart_pointer.cpp:125:22:125:22 | ref arg q | smart_pointer.cpp:125:22:125:22 | q [inner post update] | | +| smart_pointer.cpp:125:22:125:22 | ref arg q | smart_pointer.cpp:126:12:126:12 | q | | | smart_pointer.cpp:126:8:126:9 | p1 | smart_pointer.cpp:126:10:126:10 | call to operator-> | | | smart_pointer.cpp:126:8:126:9 | ref arg p1 | smart_pointer.cpp:124:48:124:49 | p1 | | | smart_pointer.cpp:126:10:126:10 | call to operator-> [post update] | smart_pointer.cpp:126:8:126:9 | ref arg p1 | TAINT | @@ -3432,6 +3437,7 @@ | smart_pointer.cpp:133:23:133:24 | ref arg p1 | smart_pointer.cpp:132:53:132:54 | p1 | | | smart_pointer.cpp:133:23:133:24 | ref arg p1 | smart_pointer.cpp:134:8:134:9 | p1 | | | smart_pointer.cpp:133:25:133:25 | call to operator-> [post update] | smart_pointer.cpp:133:23:133:24 | ref arg p1 | TAINT | +| smart_pointer.cpp:133:27:133:27 | ref arg q | smart_pointer.cpp:134:12:134:12 | q | | | smart_pointer.cpp:134:8:134:9 | p1 | smart_pointer.cpp:134:10:134:10 | call to operator-> | | | smart_pointer.cpp:134:8:134:9 | ref arg p1 | smart_pointer.cpp:132:53:132:54 | p1 | | | smart_pointer.cpp:134:10:134:10 | call to operator-> [post update] | smart_pointer.cpp:134:8:134:9 | ref arg p1 | TAINT | @@ -6435,6 +6441,7 @@ | taint.cpp:669:18:669:18 | s [post update] | taint.cpp:671:7:671:7 | s | | | taint.cpp:669:18:669:18 | s [post update] | taint.cpp:672:7:672:7 | s | | | taint.cpp:669:18:669:18 | s [post update] | taint.cpp:673:7:673:7 | s | | +| taint.cpp:669:20:669:20 | ref arg x | taint.cpp:672:9:672:9 | x | | | taint.cpp:672:7:672:7 | s [post update] | taint.cpp:673:7:673:7 | s | | | vector.cpp:16:43:16:49 | source1 | vector.cpp:17:26:17:32 | source1 | | | vector.cpp:16:43:16:49 | source1 | vector.cpp:31:38:31:44 | source1 | | @@ -7076,14 +7083,20 @@ | vector.cpp:198:3:198:4 | ee [post update] | vector.cpp:200:3:200:4 | ee | | | vector.cpp:198:3:198:4 | ee [post update] | vector.cpp:201:8:201:9 | ee | | | vector.cpp:198:3:198:4 | ee [post update] | vector.cpp:202:2:202:2 | ee | | +| vector.cpp:198:6:198:7 | ref arg vs | vector.cpp:199:11:199:12 | vs | | +| vector.cpp:198:6:198:7 | ref arg vs | vector.cpp:200:6:200:7 | vs | | +| vector.cpp:198:6:198:7 | ref arg vs | vector.cpp:201:11:201:12 | vs | | | vector.cpp:198:19:198:19 | 0 | vector.cpp:198:6:198:7 | ref arg vs | TAINT | | vector.cpp:199:8:199:9 | ee [post update] | vector.cpp:200:3:200:4 | ee | | | vector.cpp:199:8:199:9 | ee [post update] | vector.cpp:201:8:201:9 | ee | | | vector.cpp:199:8:199:9 | ee [post update] | vector.cpp:202:2:202:2 | ee | | +| vector.cpp:199:11:199:12 | ref arg vs | vector.cpp:200:6:200:7 | vs | | +| vector.cpp:199:11:199:12 | ref arg vs | vector.cpp:201:11:201:12 | vs | | | vector.cpp:199:11:199:12 | vs | vector.cpp:199:13:199:13 | call to operator[] | TAINT | | vector.cpp:200:3:200:4 | ee [post update] | vector.cpp:201:8:201:9 | ee | | | vector.cpp:200:3:200:4 | ee [post update] | vector.cpp:202:2:202:2 | ee | | | vector.cpp:200:3:200:21 | ... = ... | vector.cpp:200:8:200:8 | call to operator[] [post update] | | +| vector.cpp:200:6:200:7 | ref arg vs | vector.cpp:201:11:201:12 | vs | | | vector.cpp:200:6:200:7 | vs | vector.cpp:200:8:200:8 | call to operator[] | TAINT | | vector.cpp:200:8:200:8 | call to operator[] [post update] | vector.cpp:200:6:200:7 | ref arg vs | TAINT | | vector.cpp:200:14:200:19 | call to source | vector.cpp:200:3:200:21 | ... = ... | | diff --git a/cpp/ql/test/library-tests/fields/fields/Fields.expected b/cpp/ql/test/library-tests/fields/fields/Fields.expected index a4cb4e549cc..6ef4a17ec50 100644 --- a/cpp/ql/test/library-tests/fields/fields/Fields.expected +++ b/cpp/ql/test/library-tests/fields/fields/Fields.expected @@ -1,21 +1,13 @@ | fields.cpp:3:8:3:12 | Entry | fields.cpp:4:9:4:12 | name | public | CharPointerType | char | -| fields.cpp:3:8:3:12 | Entry | fields.cpp:4:9:4:12 | name | public | PointerDumpType | char | | fields.cpp:3:8:3:12 | Entry | fields.cpp:5:8:5:8 | t | public | Enum | | -| fields.cpp:3:8:3:12 | Entry | fields.cpp:5:8:5:8 | t | public | UserDumpType | | | fields.cpp:3:8:3:12 | Entry | fields.cpp:6:9:6:9 | s | public | CharPointerType | char | -| fields.cpp:3:8:3:12 | Entry | fields.cpp:6:9:6:9 | s | public | PointerDumpType | char | | fields.cpp:3:8:3:12 | Entry | fields.cpp:7:7:7:7 | i | public | IntType | | -| fields.cpp:3:8:3:12 | Entry | fields.cpp:7:7:7:7 | i | public | IntegralDumpType | | | fields.cpp:3:8:3:12 | Entry | fields.cpp:7:7:7:7 | i | public | MicrosoftInt32Type | | | fields.cpp:3:8:3:12 | Entry | fields.cpp:9:7:9:14 | internal | private | IntType | | -| fields.cpp:3:8:3:12 | Entry | fields.cpp:9:7:9:14 | internal | private | IntegralDumpType | | | fields.cpp:3:8:3:12 | Entry | fields.cpp:9:7:9:14 | internal | private | MicrosoftInt32Type | | -| fields.cpp:12:7:12:10 | Name | fields.cpp:13:15:13:15 | s | private | PointerDumpType | const char | -| fields.cpp:16:7:16:11 | Table | fields.cpp:17:9:17:9 | p | private | PointerDumpType | Name | +| fields.cpp:12:7:12:10 | Name | fields.cpp:13:15:13:15 | s | private | PointerType | const char | +| fields.cpp:16:7:16:11 | Table | fields.cpp:17:9:17:9 | p | private | PointerType | Name | | fields.cpp:16:7:16:11 | Table | fields.cpp:18:7:18:8 | sz | private | IntType | | -| fields.cpp:16:7:16:11 | Table | fields.cpp:18:7:18:8 | sz | private | IntegralDumpType | | | fields.cpp:16:7:16:11 | Table | fields.cpp:18:7:18:8 | sz | private | MicrosoftInt32Type | | | fields.cpp:26:7:26:10 | Date | fields.cpp:28:16:28:26 | cache_valid | private | BoolType | | -| fields.cpp:26:7:26:10 | Date | fields.cpp:28:16:28:26 | cache_valid | private | IntegralDumpType | | | fields.cpp:26:7:26:10 | Date | fields.cpp:30:17:30:21 | cache | public | CharPointerType | char | -| fields.cpp:26:7:26:10 | Date | fields.cpp:30:17:30:21 | cache | public | PointerDumpType | char | diff --git a/cpp/ql/test/library-tests/functions/routinetype/types.expected b/cpp/ql/test/library-tests/functions/routinetype/types.expected index ca70c1ac23a..d620bea517e 100644 --- a/cpp/ql/test/library-tests/functions/routinetype/types.expected +++ b/cpp/ql/test/library-tests/functions/routinetype/types.expected @@ -1 +1 @@ -| routinetype.cpp:2:7:2:19 | myRoutineType | file://:0:0:0:0 | ..()(..) | PrintableElement, RoutineDumpType | +| routinetype.cpp:2:7:2:19 | myRoutineType | file://:0:0:0:0 | ..()(..) | RoutineType | diff --git a/cpp/ql/test/library-tests/specifiers2/specifiers2.expected b/cpp/ql/test/library-tests/specifiers2/specifiers2.expected index 2ccfc7b84a0..d718451459a 100644 --- a/cpp/ql/test/library-tests/specifiers2/specifiers2.expected +++ b/cpp/ql/test/library-tests/specifiers2/specifiers2.expected @@ -43,10 +43,12 @@ | Function | specifiers2pp.cpp:10:18:10:24 | MyClass | MyClass | public | | Function | specifiers2pp.cpp:12:14:12:22 | publicFun | publicFun | inline | | Function | specifiers2pp.cpp:12:14:12:22 | publicFun | publicFun | public | +| Function | specifiers2pp.cpp:13:21:13:26 | getInt | getInt | declared_virtual | | Function | specifiers2pp.cpp:13:21:13:26 | getInt | getInt | extern | | Function | specifiers2pp.cpp:13:21:13:26 | getInt | getInt | public | | Function | specifiers2pp.cpp:13:21:13:26 | getInt | getInt | pure | | Function | specifiers2pp.cpp:13:21:13:26 | getInt | getInt | virtual | +| Function | specifiers2pp.cpp:14:21:14:21 | f | f | declared_virtual | | Function | specifiers2pp.cpp:14:21:14:21 | f | f | extern | | Function | specifiers2pp.cpp:14:21:14:21 | f | f | public | | Function | specifiers2pp.cpp:14:21:14:21 | f | f | virtual | @@ -71,6 +73,7 @@ | Function | specifiers2pp.cpp:24:7:24:7 | operator= | operator= | inline | | Function | specifiers2pp.cpp:24:7:24:7 | operator= | operator= | public | | Function | specifiers2pp.cpp:24:7:24:7 | operator= | operator= | public | +| Function | specifiers2pp.cpp:26:21:26:21 | f | f | declared_virtual | | Function | specifiers2pp.cpp:26:21:26:21 | f | f | extern | | Function | specifiers2pp.cpp:26:21:26:21 | f | f | override | | Function | specifiers2pp.cpp:26:21:26:21 | f | f | public | diff --git a/cpp/ql/test/library-tests/templates/instantiations_functions/elements.expected b/cpp/ql/test/library-tests/templates/instantiations_functions/elements.expected index 44d8ff0cb83..c770da3da37 100644 --- a/cpp/ql/test/library-tests/templates/instantiations_functions/elements.expected +++ b/cpp/ql/test/library-tests/templates/instantiations_functions/elements.expected @@ -109,6 +109,7 @@ | file://:0:0:0:0 | declaration of 1st parameter | | file://:0:0:0:0 | declared_constexpr | | file://:0:0:0:0 | declared_constinit | +| file://:0:0:0:0 | declared_virtual | | file://:0:0:0:0 | decltype(nullptr) | | file://:0:0:0:0 | definition of fp_offset | | file://:0:0:0:0 | definition of gp_offset | @@ -121,6 +122,7 @@ | file://:0:0:0:0 | explicit | | file://:0:0:0:0 | extern | | file://:0:0:0:0 | far | +| file://:0:0:0:0 | final | | file://:0:0:0:0 | float | | file://:0:0:0:0 | forceinline | | file://:0:0:0:0 | fp_offset | diff --git a/cpp/ql/test/library-tests/typedefs/Typedefs2.expected b/cpp/ql/test/library-tests/typedefs/Typedefs2.expected index 75659f24bf3..8645ce97fcd 100644 --- a/cpp/ql/test/library-tests/typedefs/Typedefs2.expected +++ b/cpp/ql/test/library-tests/typedefs/Typedefs2.expected @@ -1,2 +1,2 @@ -| typedefs.cpp:6:6:6:7 | f1 | typedefs.cpp:8:15:8:18 | TYPE | CTypedefType, LocalTypedefType, PrintableElement, UserDumpType | -| typedefs.cpp:6:6:6:7 | f1 | typedefs.cpp:9:9:9:9 | D | DirectAccessHolder, LocalClass, MetricClass, PrintableElement, StructLikeClass, UserDumpType | +| typedefs.cpp:6:6:6:7 | f1 | typedefs.cpp:8:15:8:18 | TYPE | CTypedefType, LocalTypedefType | +| typedefs.cpp:6:6:6:7 | f1 | typedefs.cpp:9:9:9:9 | D | DirectAccessHolder, LocalClass, MetricClass, StructLikeClass | diff --git a/cpp/ql/test/library-tests/types/__wchar_t/wchar_t.expected b/cpp/ql/test/library-tests/types/__wchar_t/wchar_t.expected index de87cc30f58..84106f07a92 100644 --- a/cpp/ql/test/library-tests/types/__wchar_t/wchar_t.expected +++ b/cpp/ql/test/library-tests/types/__wchar_t/wchar_t.expected @@ -1,3 +1,3 @@ -| file://:0:0:0:0 | __wchar_t * | PointerDumpType, PrintableElement | IntegralDumpType, PrintableElement, Wchar_t, WideCharType | -| file://:0:0:0:0 | const __wchar_t | PrintableElement, SpecifiedDumpType | IntegralDumpType, PrintableElement, Wchar_t, WideCharType | -| file://:0:0:0:0 | wchar_t | IntegralDumpType, PrintableElement, Wchar_t, WideCharType | | +| file://:0:0:0:0 | __wchar_t * | PointerType | Wchar_t, WideCharType | +| file://:0:0:0:0 | const __wchar_t | SpecifiedType | Wchar_t, WideCharType | +| file://:0:0:0:0 | wchar_t | Wchar_t, WideCharType | | diff --git a/cpp/ql/test/library-tests/types/cstd_types/cstd_types_fastestminimumwidth.expected b/cpp/ql/test/library-tests/types/cstd_types/cstd_types_fastestminimumwidth.expected index b2c9acd8c14..13fe25a4819 100644 --- a/cpp/ql/test/library-tests/types/cstd_types/cstd_types_fastestminimumwidth.expected +++ b/cpp/ql/test/library-tests/types/cstd_types/cstd_types_fastestminimumwidth.expected @@ -1,8 +1,8 @@ -| cstd_types.cpp:47:13:47:15 | if8 | CTypedefType, FastestMinimumWidthIntegralType, Int_fast8_t, PrintableElement, UserDumpType | -| cstd_types.cpp:48:14:48:17 | if16 | CTypedefType, FastestMinimumWidthIntegralType, Int_fast16_t, PrintableElement, UserDumpType | -| cstd_types.cpp:49:14:49:17 | if32 | CTypedefType, FastestMinimumWidthIntegralType, Int_fast32_t, PrintableElement, UserDumpType | -| cstd_types.cpp:50:14:50:17 | if64 | CTypedefType, FastestMinimumWidthIntegralType, Int_fast64_t, PrintableElement, UserDumpType | -| cstd_types.cpp:51:14:51:16 | uf8 | CTypedefType, FastestMinimumWidthIntegralType, PrintableElement, UInt_fast8_t, UserDumpType | -| cstd_types.cpp:52:15:52:18 | uf16 | CTypedefType, FastestMinimumWidthIntegralType, PrintableElement, UInt_fast16_t, UserDumpType | -| cstd_types.cpp:53:15:53:18 | uf32 | CTypedefType, FastestMinimumWidthIntegralType, PrintableElement, UInt_fast32_t, UserDumpType | -| cstd_types.cpp:54:15:54:18 | uf64 | CTypedefType, FastestMinimumWidthIntegralType, PrintableElement, UInt_fast64_t, UserDumpType | +| cstd_types.cpp:47:13:47:15 | if8 | CTypedefType, FastestMinimumWidthIntegralType, Int_fast8_t | +| cstd_types.cpp:48:14:48:17 | if16 | CTypedefType, FastestMinimumWidthIntegralType, Int_fast16_t | +| cstd_types.cpp:49:14:49:17 | if32 | CTypedefType, FastestMinimumWidthIntegralType, Int_fast32_t | +| cstd_types.cpp:50:14:50:17 | if64 | CTypedefType, FastestMinimumWidthIntegralType, Int_fast64_t | +| cstd_types.cpp:51:14:51:16 | uf8 | CTypedefType, FastestMinimumWidthIntegralType, UInt_fast8_t | +| cstd_types.cpp:52:15:52:18 | uf16 | CTypedefType, FastestMinimumWidthIntegralType, UInt_fast16_t | +| cstd_types.cpp:53:15:53:18 | uf32 | CTypedefType, FastestMinimumWidthIntegralType, UInt_fast32_t | +| cstd_types.cpp:54:15:54:18 | uf64 | CTypedefType, FastestMinimumWidthIntegralType, UInt_fast64_t | \ No newline at end of file diff --git a/cpp/ql/test/library-tests/types/cstd_types/cstd_types_fixedwidth.expected b/cpp/ql/test/library-tests/types/cstd_types/cstd_types_fixedwidth.expected index bfa2a29f9f3..d1c64343636 100644 --- a/cpp/ql/test/library-tests/types/cstd_types/cstd_types_fixedwidth.expected +++ b/cpp/ql/test/library-tests/types/cstd_types/cstd_types_fixedwidth.expected @@ -1,8 +1,8 @@ -| cstd_types.cpp:31:8:31:9 | i8 | CTypedefType, FixedWidthIntegralType, Int8_t, PrintableElement, UserDumpType | -| cstd_types.cpp:32:9:32:11 | i16 | CTypedefType, FixedWidthIntegralType, Int16_t, PrintableElement, UserDumpType | -| cstd_types.cpp:33:9:33:11 | i32 | CTypedefType, FixedWidthIntegralType, Int32_t, PrintableElement, UserDumpType | -| cstd_types.cpp:34:9:34:11 | i64 | CTypedefType, FixedWidthIntegralType, Int64_t, PrintableElement, UserDumpType | -| cstd_types.cpp:35:9:35:11 | ui8 | CTypedefType, FixedWidthIntegralType, PrintableElement, UInt8_t, UserDumpType | -| cstd_types.cpp:36:10:36:13 | ui16 | CTypedefType, FixedWidthIntegralType, PrintableElement, UInt16_t, UserDumpType | -| cstd_types.cpp:37:10:37:13 | ui32 | CTypedefType, FixedWidthIntegralType, PrintableElement, UInt32_t, UserDumpType | -| cstd_types.cpp:38:10:38:13 | ui64 | CTypedefType, FixedWidthIntegralType, PrintableElement, UInt64_t, UserDumpType | +| cstd_types.cpp:31:8:31:9 | i8 | CTypedefType, FixedWidthIntegralType, Int8_t | +| cstd_types.cpp:32:9:32:11 | i16 | CTypedefType, FixedWidthIntegralType, Int16_t | +| cstd_types.cpp:33:9:33:11 | i32 | CTypedefType, FixedWidthIntegralType, Int32_t | +| cstd_types.cpp:34:9:34:11 | i64 | CTypedefType, FixedWidthIntegralType, Int64_t | +| cstd_types.cpp:35:9:35:11 | ui8 | CTypedefType, FixedWidthIntegralType, UInt8_t | +| cstd_types.cpp:36:10:36:13 | ui16 | CTypedefType, FixedWidthIntegralType, UInt16_t | +| cstd_types.cpp:37:10:37:13 | ui32 | CTypedefType, FixedWidthIntegralType, UInt32_t | +| cstd_types.cpp:38:10:38:13 | ui64 | CTypedefType, FixedWidthIntegralType, UInt64_t | diff --git a/cpp/ql/test/library-tests/types/cstd_types/cstd_types_fixedwidthenum.expected b/cpp/ql/test/library-tests/types/cstd_types/cstd_types_fixedwidthenum.expected index f313ab23b1a..d9884b22b00 100644 --- a/cpp/ql/test/library-tests/types/cstd_types/cstd_types_fixedwidthenum.expected +++ b/cpp/ql/test/library-tests/types/cstd_types/cstd_types_fixedwidthenum.expected @@ -1,2 +1,2 @@ -| cstd_types.cpp:74:4:74:6 | _e0 | Enum, FixedWidthEnumType, PrintableElement, UserDumpType | -| cstd_types.cpp:75:4:75:6 | _e1 | FixedWidthEnumType, PrintableElement, ScopedEnum, UserDumpType | +| cstd_types.cpp:74:4:74:6 | _e0 | Enum, FixedWidthEnumType | +| cstd_types.cpp:75:4:75:6 | _e1 | FixedWidthEnumType, ScopedEnum | diff --git a/cpp/ql/test/library-tests/types/cstd_types/cstd_types_maximumwidth.expected b/cpp/ql/test/library-tests/types/cstd_types/cstd_types_maximumwidth.expected index 6b57f8be126..0bf7779fcaf 100644 --- a/cpp/ql/test/library-tests/types/cstd_types/cstd_types_maximumwidth.expected +++ b/cpp/ql/test/library-tests/types/cstd_types/cstd_types_maximumwidth.expected @@ -1,2 +1,2 @@ -| cstd_types.cpp:55:10:55:11 | im | CTypedefType, Intmax_t, MaximumWidthIntegralType, PrintableElement, UserDumpType | -| cstd_types.cpp:56:11:56:13 | uim | CTypedefType, MaximumWidthIntegralType, PrintableElement, Uintmax_t, UserDumpType | +| cstd_types.cpp:55:10:55:11 | im | CTypedefType, Intmax_t, MaximumWidthIntegralType | +| cstd_types.cpp:56:11:56:13 | uim | CTypedefType, MaximumWidthIntegralType, Uintmax_t | diff --git a/cpp/ql/test/library-tests/types/cstd_types/cstd_types_minimumwidth.expected b/cpp/ql/test/library-tests/types/cstd_types/cstd_types_minimumwidth.expected index aac3507a366..2984f07be8c 100644 --- a/cpp/ql/test/library-tests/types/cstd_types/cstd_types_minimumwidth.expected +++ b/cpp/ql/test/library-tests/types/cstd_types/cstd_types_minimumwidth.expected @@ -1,8 +1,8 @@ -| cstd_types.cpp:39:15:39:16 | l8 | CTypedefType, Int_least8_t, MinimumWidthIntegralType, PrintableElement, UserDumpType | -| cstd_types.cpp:40:15:40:17 | l16 | CTypedefType, Int_least16_t, MinimumWidthIntegralType, PrintableElement, UserDumpType | -| cstd_types.cpp:41:15:41:17 | l32 | CTypedefType, Int_least32_t, MinimumWidthIntegralType, PrintableElement, UserDumpType | -| cstd_types.cpp:42:15:42:17 | l64 | CTypedefType, Int_least64_t, MinimumWidthIntegralType, PrintableElement, UserDumpType | -| cstd_types.cpp:43:15:43:17 | ul8 | CTypedefType, MinimumWidthIntegralType, PrintableElement, UInt_least8_t, UserDumpType | -| cstd_types.cpp:44:16:44:19 | ul16 | CTypedefType, MinimumWidthIntegralType, PrintableElement, UInt_least16_t, UserDumpType | -| cstd_types.cpp:45:16:45:19 | ul32 | CTypedefType, MinimumWidthIntegralType, PrintableElement, UInt_least32_t, UserDumpType | -| cstd_types.cpp:46:16:46:19 | ul64 | CTypedefType, MinimumWidthIntegralType, PrintableElement, UInt_least64_t, UserDumpType | +| cstd_types.cpp:39:15:39:16 | l8 | CTypedefType, Int_least8_t, MinimumWidthIntegralType | +| cstd_types.cpp:40:15:40:17 | l16 | CTypedefType, Int_least16_t, MinimumWidthIntegralType | +| cstd_types.cpp:41:15:41:17 | l32 | CTypedefType, Int_least32_t, MinimumWidthIntegralType | +| cstd_types.cpp:42:15:42:17 | l64 | CTypedefType, Int_least64_t, MinimumWidthIntegralType | +| cstd_types.cpp:43:15:43:17 | ul8 | CTypedefType, MinimumWidthIntegralType, UInt_least8_t | +| cstd_types.cpp:44:16:44:19 | ul16 | CTypedefType, MinimumWidthIntegralType, UInt_least16_t | +| cstd_types.cpp:45:16:45:19 | ul32 | CTypedefType, MinimumWidthIntegralType, UInt_least32_t | +| cstd_types.cpp:46:16:46:19 | ul64 | CTypedefType, MinimumWidthIntegralType, UInt_least64_t | diff --git a/cpp/ql/test/library-tests/types/integral_types_ms/vars.expected b/cpp/ql/test/library-tests/types/integral_types_ms/vars.expected index 31a7f01ba5f..d2aac7454fd 100644 --- a/cpp/ql/test/library-tests/types/integral_types_ms/vars.expected +++ b/cpp/ql/test/library-tests/types/integral_types_ms/vars.expected @@ -1,4 +1,4 @@ -| integral_types.cpp:2:8:2:9 | i8 | file://:0:0:0:0 | char | IntegralDumpType, MicrosoftInt8Type, PlainCharType, PrintableElement | -| integral_types.cpp:3:9:3:11 | i16 | file://:0:0:0:0 | short | IntegralDumpType, MicrosoftInt16Type, PrintableElement, ShortType | -| integral_types.cpp:4:9:4:11 | i32 | file://:0:0:0:0 | int | IntType, IntegralDumpType, MicrosoftInt32Type, PrintableElement | -| integral_types.cpp:5:9:5:11 | i64 | file://:0:0:0:0 | long long | IntegralDumpType, LongLongType, MicrosoftInt64Type, PrintableElement | +| integral_types.cpp:2:8:2:9 | i8 | file://:0:0:0:0 | char | MicrosoftInt8Type, PlainCharType | +| integral_types.cpp:3:9:3:11 | i16 | file://:0:0:0:0 | short | MicrosoftInt16Type, ShortType | +| integral_types.cpp:4:9:4:11 | i32 | file://:0:0:0:0 | int | IntType, MicrosoftInt32Type | +| integral_types.cpp:5:9:5:11 | i64 | file://:0:0:0:0 | long long | LongLongType, MicrosoftInt64Type | diff --git a/cpp/ql/test/library-tests/types/wchar_t_typedef/wchar_t.expected b/cpp/ql/test/library-tests/types/wchar_t_typedef/wchar_t.expected index a7d80758611..e0f36ebd68a 100644 --- a/cpp/ql/test/library-tests/types/wchar_t_typedef/wchar_t.expected +++ b/cpp/ql/test/library-tests/types/wchar_t_typedef/wchar_t.expected @@ -1,3 +1,3 @@ -| file://:0:0:0:0 | wchar_t | IntegralDumpType, PrintableElement, Wchar_t, WideCharType | | -| file://:0:0:0:0 | wchar_t * | PointerDumpType, PrintableElement | CTypedefType, PrintableElement, UserDumpType, Wchar_t | -| ms.c:2:24:2:30 | wchar_t | CTypedefType, PrintableElement, UserDumpType, Wchar_t | | +| file://:0:0:0:0 | wchar_t | Wchar_t, WideCharType | | +| file://:0:0:0:0 | wchar_t * | PointerType | CTypedefType, Wchar_t | +| ms.c:2:24:2:30 | wchar_t | CTypedefType, Wchar_t | | diff --git a/cpp/ql/test/library-tests/unnamed/elements.expected b/cpp/ql/test/library-tests/unnamed/elements.expected index 2b32306fbac..c9478377064 100644 --- a/cpp/ql/test/library-tests/unnamed/elements.expected +++ b/cpp/ql/test/library-tests/unnamed/elements.expected @@ -41,6 +41,7 @@ | file://:0:0:0:0 | const | Other | | file://:0:0:0:0 | declared_constexpr | Other | | file://:0:0:0:0 | declared_constinit | Other | +| file://:0:0:0:0 | declared_virtual | Other | | file://:0:0:0:0 | decltype(nullptr) | Other | | file://:0:0:0:0 | definition of fp_offset | Other | | file://:0:0:0:0 | definition of gp_offset | Other | @@ -53,6 +54,7 @@ | file://:0:0:0:0 | explicit | Other | | file://:0:0:0:0 | extern | Other | | file://:0:0:0:0 | far | Other | +| file://:0:0:0:0 | final | Other | | file://:0:0:0:0 | float | Other | | file://:0:0:0:0 | forceinline | Other | | file://:0:0:0:0 | fp_offset | Other | diff --git a/cpp/ql/test/library-tests/variables/variables/types.expected b/cpp/ql/test/library-tests/variables/variables/types.expected index a77e0ec43d8..712ec74c914 100644 --- a/cpp/ql/test/library-tests/variables/variables/types.expected +++ b/cpp/ql/test/library-tests/variables/variables/types.expected @@ -1,94 +1,94 @@ -| ..()(..) | PrintableElement, RoutineDumpType | | | | | -| ..(*)(..) | FunctionPointerDumpType, PrintableElement | | ..()(..) | | | -| _Complex __float128 | BinaryFloatingPointType, BuiltInDumpType, ComplexNumberType, PrintableElement | | | | | -| _Complex double | BinaryFloatingPointType, BuiltInDumpType, ComplexNumberType, PrintableElement | | | | | -| _Complex float | BinaryFloatingPointType, BuiltInDumpType, ComplexNumberType, PrintableElement | | | | | -| _Complex long double | BinaryFloatingPointType, BuiltInDumpType, ComplexNumberType, PrintableElement | | | | | -| _Decimal32 | BuiltInDumpType, Decimal32Type, PrintableElement | | | | | -| _Decimal64 | BuiltInDumpType, Decimal64Type, PrintableElement | | | | | -| _Decimal128 | BuiltInDumpType, Decimal128Type, PrintableElement | | | | | -| _Float32 | BinaryFloatingPointType, BuiltInDumpType, PrintableElement, RealNumberType | | | | | -| _Float32x | BinaryFloatingPointType, BuiltInDumpType, PrintableElement, RealNumberType | | | | | -| _Float64 | BinaryFloatingPointType, BuiltInDumpType, PrintableElement, RealNumberType | | | | | -| _Float64x | BinaryFloatingPointType, BuiltInDumpType, PrintableElement, RealNumberType | | | | | -| _Float128 | BinaryFloatingPointType, BuiltInDumpType, PrintableElement, RealNumberType | | | | | -| _Float128x | BinaryFloatingPointType, BuiltInDumpType, PrintableElement, RealNumberType | | | | | -| _Imaginary double | BinaryFloatingPointType, BuiltInDumpType, ImaginaryNumberType, PrintableElement | | | | | -| _Imaginary float | BinaryFloatingPointType, BuiltInDumpType, ImaginaryNumberType, PrintableElement | | | | | -| _Imaginary long double | BinaryFloatingPointType, BuiltInDumpType, ImaginaryNumberType, PrintableElement | | | | | -| __float128 | BuiltInDumpType, Float128Type, PrintableElement | | | | | -| __int128 | Int128Type, IntegralDumpType, PrintableElement | | | | | -| __va_list_tag | DirectAccessHolder, MetricClass, PrintableElement, Struct, StructLikeClass, UserDumpType | | | | | -| __va_list_tag & | LValueReferenceDumpType, PrintableElement | | __va_list_tag | | | -| __va_list_tag && | PrintableElement, RValueReferenceDumpType | | __va_list_tag | | | -| address | DirectAccessHolder, MetricClass, PrintableElement, Struct, StructLikeClass, UserDumpType | | | | | -| address & | LValueReferenceDumpType, PrintableElement | | address | | | -| address && | PrintableElement, RValueReferenceDumpType | | address | | | -| auto | AutoType, PrintableElement, UserDumpType | | | | | -| bool | BoolType, IntegralDumpType, PrintableElement | | | | | -| char | IntegralDumpType, MicrosoftInt8Type, PlainCharType, PrintableElement | | | | | -| char8_t | Char8Type, IntegralDumpType, PrintableElement | | | | | -| char16_t | Char16Type, IntegralDumpType, PrintableElement | | | | | -| char32_t | Char32Type, IntegralDumpType, PrintableElement | | | | | -| char * | CharPointerType, PointerDumpType, PrintableElement | | char | | | -| char *[3] | ArrayDumpType, PrintableElement | char * | char * | | | -| char *[32] | ArrayDumpType, PrintableElement | char * | char * | | | -| char *[] | ArrayDumpType, PrintableElement | char * | char * | | | -| char[2] | ArrayDumpType, PrintableElement | char | char | | | -| char[3] | ArrayDumpType, PrintableElement | char | char | | | -| char[5] | ArrayDumpType, PrintableElement | char | char | | | -| char[6] | ArrayDumpType, PrintableElement | char | char | | | -| char[8] | ArrayDumpType, PrintableElement | char | char | | | -| char[9] | ArrayDumpType, PrintableElement | char | char | | | -| char[10] | ArrayDumpType, PrintableElement | char | char | | | -| char[53] | ArrayDumpType, PrintableElement | char | char | | | -| char[] | ArrayDumpType, PrintableElement | char | char | | | -| const __va_list_tag | PrintableElement, SpecifiedDumpType | | __va_list_tag | | | -| const __va_list_tag & | LValueReferenceDumpType, PrintableElement | | const __va_list_tag | | | -| const address | PrintableElement, SpecifiedDumpType | | address | | | -| const address & | LValueReferenceDumpType, PrintableElement | | const address | | | -| const char | PrintableElement, SpecifiedDumpType | | char | | | -| const char * | PointerDumpType, PrintableElement | | const char | | | -| const char *[3] | ArrayDumpType, PrintableElement | const char * | const char * | | | -| const char *[] | ArrayDumpType, PrintableElement | const char * | const char * | | | -| const char[5] | ArrayDumpType, PrintableElement | const char | const char | | | -| const char[6] | ArrayDumpType, PrintableElement | const char | const char | | | -| const char[8] | ArrayDumpType, PrintableElement | const char | const char | | | -| const char[9] | ArrayDumpType, PrintableElement | const char | const char | | | -| const char[10] | ArrayDumpType, PrintableElement | const char | const char | | | -| const char[53] | ArrayDumpType, PrintableElement | const char | const char | | | -| const double | PrintableElement, SpecifiedDumpType | | double | | | -| const int | PrintableElement, SpecifiedDumpType | | int | | | -| decltype(nullptr) | BuiltInDumpType, NullPointerType, PrintableElement | | | | | -| double | BuiltInDumpType, DoubleType, PrintableElement | | | | | -| error | BuiltInDumpType, ErroneousType, PrintableElement | | | | | -| float | BuiltInDumpType, FloatType, PrintableElement | | | | | -| float[3] | ArrayDumpType, PrintableElement | float | float | | | -| int | IntType, IntegralDumpType, MicrosoftInt32Type, PrintableElement | | | | | -| int * | IntPointerType, PointerDumpType, PrintableElement | | int | | | -| int[4] | ArrayDumpType, PrintableElement | int | int | | | -| int[8] | ArrayDumpType, PrintableElement | int | int | | | -| int[10] | ArrayDumpType, PrintableElement | int | int | | | -| int[10][20] | ArrayDumpType, PrintableElement | int[20] | int[20] | | | -| int[20] | ArrayDumpType, PrintableElement | int | int | | | -| int[] | ArrayDumpType, PrintableElement | int | int | | | -| long | IntegralDumpType, LongType, PrintableElement | | | | | -| long double | BuiltInDumpType, LongDoubleType, PrintableElement | | | | | -| long long | IntegralDumpType, LongLongType, MicrosoftInt64Type, PrintableElement | | | | | -| short | IntegralDumpType, MicrosoftInt16Type, PrintableElement, ShortType | | | | | -| signed __int128 | Int128Type, IntegralDumpType, PrintableElement | | | | | -| signed char | IntegralDumpType, PrintableElement, SignedCharType | | | | | -| signed int | IntType, IntegralDumpType, PrintableElement | | | | | -| signed long | IntegralDumpType, LongType, PrintableElement | | | | | -| signed long long | IntegralDumpType, LongLongType, PrintableElement | | | | | -| signed short | IntegralDumpType, PrintableElement, ShortType | | | | | -| unknown | BuiltInDumpType, PrintableElement, UnknownType | | | | | -| unsigned __int128 | Int128Type, IntegralDumpType, PrintableElement | | | | unsigned integral | -| unsigned char | IntegralDumpType, PrintableElement, UnsignedCharType | | | | unsigned integral | -| unsigned int | IntType, IntegralDumpType, PrintableElement | | | unsigned int | unsigned integral | -| unsigned long | IntegralDumpType, LongType, PrintableElement | | | | unsigned integral | -| unsigned long long | IntegralDumpType, LongLongType, PrintableElement | | | | unsigned integral | -| unsigned short | IntegralDumpType, PrintableElement, ShortType | | | | unsigned integral | -| void | BuiltInDumpType, PrintableElement, VoidType | | | | | -| void * | PointerDumpType, PrintableElement, VoidPointerType | | void | | | -| wchar_t | IntegralDumpType, PrintableElement, Wchar_t, WideCharType | | | | | +| ..()(..) | RoutineType | | | | | +| ..(*)(..) | FunctionPointerType | | ..()(..) | | | +| _Complex __float128 | BinaryFloatingPointType, ComplexNumberType | | | | | +| _Complex double | BinaryFloatingPointType, ComplexNumberType | | | | | +| _Complex float | BinaryFloatingPointType, ComplexNumberType | | | | | +| _Complex long double | BinaryFloatingPointType, ComplexNumberType | | | | | +| _Decimal32 | Decimal32Type | | | | | +| _Decimal64 | Decimal64Type | | | | | +| _Decimal128 | Decimal128Type | | | | | +| _Float32 | BinaryFloatingPointType, RealNumberType | | | | | +| _Float32x | BinaryFloatingPointType, RealNumberType | | | | | +| _Float64 | BinaryFloatingPointType, RealNumberType | | | | | +| _Float64x | BinaryFloatingPointType, RealNumberType | | | | | +| _Float128 | BinaryFloatingPointType, RealNumberType | | | | | +| _Float128x | BinaryFloatingPointType, RealNumberType | | | | | +| _Imaginary double | BinaryFloatingPointType, ImaginaryNumberType | | | | | +| _Imaginary float | BinaryFloatingPointType, ImaginaryNumberType | | | | | +| _Imaginary long double | BinaryFloatingPointType, ImaginaryNumberType | | | | | +| __float128 | Float128Type | | | | | +| __int128 | Int128Type | | | | | +| __va_list_tag | DirectAccessHolder, MetricClass, Struct, StructLikeClass | | | | | +| __va_list_tag & | LValueReferenceType | | __va_list_tag | | | +| __va_list_tag && | RValueReferenceType | | __va_list_tag | | | +| address | DirectAccessHolder, MetricClass, Struct, StructLikeClass | | | | | +| address & | LValueReferenceType | | address | | | +| address && | RValueReferenceType | | address | | | +| auto | AutoType | | | | | +| bool | BoolType | | | | | +| char | MicrosoftInt8Type, PlainCharType | | | | | +| char8_t | Char8Type | | | | | +| char16_t | Char16Type | | | | | +| char32_t | Char32Type | | | | | +| char * | CharPointerType | | char | | | +| char *[3] | ArrayType | char * | char * | | | +| char *[32] | ArrayType | char * | char * | | | +| char *[] | ArrayType | char * | char * | | | +| char[2] | ArrayType | char | char | | | +| char[3] | ArrayType | char | char | | | +| char[5] | ArrayType | char | char | | | +| char[6] | ArrayType | char | char | | | +| char[8] | ArrayType | char | char | | | +| char[9] | ArrayType | char | char | | | +| char[10] | ArrayType | char | char | | | +| char[53] | ArrayType | char | char | | | +| char[] | ArrayType | char | char | | | +| const __va_list_tag | SpecifiedType | | __va_list_tag | | | +| const __va_list_tag & | LValueReferenceType | | const __va_list_tag | | | +| const address | SpecifiedType | | address | | | +| const address & | LValueReferenceType | | const address | | | +| const char | SpecifiedType | | char | | | +| const char * | PointerType | | const char | | | +| const char *[3] | ArrayType | const char * | const char * | | | +| const char *[] | ArrayType | const char * | const char * | | | +| const char[5] | ArrayType | const char | const char | | | +| const char[6] | ArrayType | const char | const char | | | +| const char[8] | ArrayType | const char | const char | | | +| const char[9] | ArrayType | const char | const char | | | +| const char[10] | ArrayType | const char | const char | | | +| const char[53] | ArrayType | const char | const char | | | +| const double | SpecifiedType | | double | | | +| const int | SpecifiedType | | int | | | +| decltype(nullptr) | NullPointerType | | | | | +| double | DoubleType | | | | | +| error | ErroneousType | | | | | +| float | FloatType | | | | | +| float[3] | ArrayType | float | float | | | +| int | IntType, MicrosoftInt32Type | | | | | +| int * | IntPointerType | | int | | | +| int[4] | ArrayType | int | int | | | +| int[8] | ArrayType | int | int | | | +| int[10] | ArrayType | int | int | | | +| int[10][20] | ArrayType | int[20] | int[20] | | | +| int[20] | ArrayType | int | int | | | +| int[] | ArrayType | int | int | | | +| long | LongType | | | | | +| long double | LongDoubleType | | | | | +| long long | LongLongType, MicrosoftInt64Type | | | | | +| short | MicrosoftInt16Type, ShortType | | | | | +| signed __int128 | Int128Type | | | | | +| signed char | SignedCharType | | | | | +| signed int | IntType | | | | | +| signed long | LongType | | | | | +| signed long long | LongLongType | | | | | +| signed short | ShortType | | | | | +| unknown | UnknownType | | | | | +| unsigned __int128 | Int128Type | | | | unsigned integral | +| unsigned char | UnsignedCharType | | | | unsigned integral | +| unsigned int | IntType | | | unsigned int | unsigned integral | +| unsigned long | LongType | | | | unsigned integral | +| unsigned long long | LongLongType | | | | unsigned integral | +| unsigned short | ShortType | | | | unsigned integral | +| void | VoidType | | | | | +| void * | VoidPointerType | | void | | | +| wchar_t | Wchar_t, WideCharType | | | | | diff --git a/cpp/ql/test/library-tests/variables/variables/variable.expected b/cpp/ql/test/library-tests/variables/variables/variable.expected index ad190643839..e99ff83d440 100644 --- a/cpp/ql/test/library-tests/variables/variables/variable.expected +++ b/cpp/ql/test/library-tests/variables/variables/variable.expected @@ -1,111 +1,70 @@ -| file://:0:0:0:0 | (unnamed parameter 0) | file://:0:0:0:0 | __va_list_tag && | DumpVariable | | | | file://:0:0:0:0 | (unnamed parameter 0) | file://:0:0:0:0 | __va_list_tag && | SemanticStackVariable | | | -| file://:0:0:0:0 | (unnamed parameter 0) | file://:0:0:0:0 | address && | DumpVariable | | | | file://:0:0:0:0 | (unnamed parameter 0) | file://:0:0:0:0 | address && | SemanticStackVariable | | | -| file://:0:0:0:0 | (unnamed parameter 0) | file://:0:0:0:0 | const __va_list_tag & | DumpVariable | | | | file://:0:0:0:0 | (unnamed parameter 0) | file://:0:0:0:0 | const __va_list_tag & | SemanticStackVariable | | | -| file://:0:0:0:0 | (unnamed parameter 0) | file://:0:0:0:0 | const address & | DumpVariable | | | | file://:0:0:0:0 | (unnamed parameter 0) | file://:0:0:0:0 | const address & | SemanticStackVariable | | | -| file://:0:0:0:0 | fp_offset | file://:0:0:0:0 | unsigned int | DumpVariable | | | | file://:0:0:0:0 | fp_offset | file://:0:0:0:0 | unsigned int | Field | | | -| file://:0:0:0:0 | gp_offset | file://:0:0:0:0 | unsigned int | DumpVariable | | | | file://:0:0:0:0 | gp_offset | file://:0:0:0:0 | unsigned int | Field | | | -| file://:0:0:0:0 | overflow_arg_area | file://:0:0:0:0 | void * | DumpVariable | | | | file://:0:0:0:0 | overflow_arg_area | file://:0:0:0:0 | void * | Field | | | -| file://:0:0:0:0 | reg_save_area | file://:0:0:0:0 | void * | DumpVariable | | | | file://:0:0:0:0 | reg_save_area | file://:0:0:0:0 | void * | Field | | | -| variables.cpp:1:12:1:12 | i | file://:0:0:0:0 | int | DumpVariable | | | | variables.cpp:1:12:1:12 | i | file://:0:0:0:0 | int | GlobalVariable | | | | variables.cpp:1:12:1:12 | i | file://:0:0:0:0 | int | StaticStorageDurationVariable | | | -| variables.cpp:2:12:2:12 | i | file://:0:0:0:0 | int | DumpVariable | | | | variables.cpp:2:12:2:12 | i | file://:0:0:0:0 | int | GlobalVariable | | | | variables.cpp:2:12:2:12 | i | file://:0:0:0:0 | int | StaticStorageDurationVariable | | | -| variables.cpp:3:12:3:12 | i | file://:0:0:0:0 | int | DumpVariable | | | | variables.cpp:3:12:3:12 | i | file://:0:0:0:0 | int | GlobalVariable | | | | variables.cpp:3:12:3:12 | i | file://:0:0:0:0 | int | StaticStorageDurationVariable | | | -| variables.cpp:5:11:5:11 | c | file://:0:0:0:0 | const int | DumpVariable | const | static | | variables.cpp:5:11:5:11 | c | file://:0:0:0:0 | const int | GlobalVariable | const | static | | variables.cpp:5:11:5:11 | c | file://:0:0:0:0 | const int | StaticStorageDurationVariable | const | static | -| variables.cpp:6:14:6:15 | pi | file://:0:0:0:0 | const double | DumpVariable | const | static | | variables.cpp:6:14:6:15 | pi | file://:0:0:0:0 | const double | GlobalVariable | const | static | | variables.cpp:6:14:6:15 | pi | file://:0:0:0:0 | const double | StaticStorageDurationVariable | const | static | -| variables.cpp:8:10:8:10 | a | file://:0:0:0:0 | unsigned int | DumpVariable | | | | variables.cpp:8:10:8:10 | a | file://:0:0:0:0 | unsigned int | GlobalVariable | | | | variables.cpp:8:10:8:10 | a | file://:0:0:0:0 | unsigned int | StaticStorageDurationVariable | | | -| variables.cpp:10:14:10:14 | b | file://:0:0:0:0 | unsigned int | DumpVariable | | | | variables.cpp:10:14:10:14 | b | file://:0:0:0:0 | unsigned int | GlobalVariable | | | | variables.cpp:10:14:10:14 | b | file://:0:0:0:0 | unsigned int | StaticStorageDurationVariable | | | -| variables.cpp:12:13:12:17 | kings | file://:0:0:0:0 | const char *[] | DumpVariable | | | | variables.cpp:12:13:12:17 | kings | file://:0:0:0:0 | const char *[] | GlobalVariable | | | | variables.cpp:12:13:12:17 | kings | file://:0:0:0:0 | const char *[] | StaticStorageDurationVariable | | | -| variables.cpp:14:6:14:6 | p | file://:0:0:0:0 | int * | DumpVariable | | | | variables.cpp:14:6:14:6 | p | file://:0:0:0:0 | int * | GlobalVariable | | | | variables.cpp:14:6:14:6 | p | file://:0:0:0:0 | int * | StaticStorageDurationVariable | | | -| variables.cpp:14:9:14:9 | q | file://:0:0:0:0 | int | DumpVariable | | | | variables.cpp:14:9:14:9 | q | file://:0:0:0:0 | int | GlobalVariable | | | | variables.cpp:14:9:14:9 | q | file://:0:0:0:0 | int | StaticStorageDurationVariable | | | -| variables.cpp:15:12:15:13 | v1 | file://:0:0:0:0 | int[10] | DumpVariable | | static | | variables.cpp:15:12:15:13 | v1 | file://:0:0:0:0 | int[10] | GlobalVariable | | static | | variables.cpp:15:12:15:13 | v1 | file://:0:0:0:0 | int[10] | StaticStorageDurationVariable | | static | -| variables.cpp:15:21:15:22 | pv | file://:0:0:0:0 | int * | DumpVariable | | static | | variables.cpp:15:21:15:22 | pv | file://:0:0:0:0 | int * | GlobalVariable | | static | | variables.cpp:15:21:15:22 | pv | file://:0:0:0:0 | int * | StaticStorageDurationVariable | | static | -| variables.cpp:17:7:17:8 | fp | file://:0:0:0:0 | ..(*)(..) | DumpVariable | | | | variables.cpp:17:7:17:8 | fp | file://:0:0:0:0 | ..(*)(..) | FunctionPointerVariable | | | | variables.cpp:17:7:17:8 | fp | file://:0:0:0:0 | ..(*)(..) | GlobalVariable | | | | variables.cpp:17:7:17:8 | fp | file://:0:0:0:0 | ..(*)(..) | StaticStorageDurationVariable | | | -| variables.cpp:19:7:19:8 | v2 | file://:0:0:0:0 | float[3] | DumpVariable | | | | variables.cpp:19:7:19:8 | v2 | file://:0:0:0:0 | float[3] | GlobalVariable | | | | variables.cpp:19:7:19:8 | v2 | file://:0:0:0:0 | float[3] | StaticStorageDurationVariable | | | -| variables.cpp:20:7:20:8 | v3 | file://:0:0:0:0 | char *[32] | DumpVariable | | | | variables.cpp:20:7:20:8 | v3 | file://:0:0:0:0 | char *[32] | GlobalVariable | | | | variables.cpp:20:7:20:8 | v3 | file://:0:0:0:0 | char *[32] | StaticStorageDurationVariable | | | -| variables.cpp:22:5:22:6 | d2 | file://:0:0:0:0 | int[10][20] | DumpVariable | | | | variables.cpp:22:5:22:6 | d2 | file://:0:0:0:0 | int[10][20] | GlobalVariable | | | | variables.cpp:22:5:22:6 | d2 | file://:0:0:0:0 | int[10][20] | StaticStorageDurationVariable | | | -| variables.cpp:24:6:24:7 | v4 | file://:0:0:0:0 | char[3] | DumpVariable | | | | variables.cpp:24:6:24:7 | v4 | file://:0:0:0:0 | char[3] | GlobalVariable | | | | variables.cpp:24:6:24:7 | v4 | file://:0:0:0:0 | char[3] | StaticStorageDurationVariable | | | -| variables.cpp:26:5:26:6 | v5 | file://:0:0:0:0 | int[8] | DumpVariable | | | | variables.cpp:26:5:26:6 | v5 | file://:0:0:0:0 | int[8] | GlobalVariable | | | | variables.cpp:26:5:26:6 | v5 | file://:0:0:0:0 | int[8] | StaticStorageDurationVariable | | | -| variables.cpp:28:7:28:8 | p2 | file://:0:0:0:0 | char * | DumpVariable | | | | variables.cpp:28:7:28:8 | p2 | file://:0:0:0:0 | char * | GlobalVariable | | | | variables.cpp:28:7:28:8 | p2 | file://:0:0:0:0 | char * | StaticStorageDurationVariable | | | -| variables.cpp:29:6:29:7 | p3 | file://:0:0:0:0 | char[] | DumpVariable | | | | variables.cpp:29:6:29:7 | p3 | file://:0:0:0:0 | char[] | GlobalVariable | | | | variables.cpp:29:6:29:7 | p3 | file://:0:0:0:0 | char[] | StaticStorageDurationVariable | | | -| variables.cpp:31:6:31:10 | alpha | file://:0:0:0:0 | char[] | DumpVariable | | | | variables.cpp:31:6:31:10 | alpha | file://:0:0:0:0 | char[] | GlobalVariable | | | | variables.cpp:31:6:31:10 | alpha | file://:0:0:0:0 | char[] | StaticStorageDurationVariable | | | -| variables.cpp:34:5:34:6 | av | file://:0:0:0:0 | int[] | DumpVariable | | | | variables.cpp:34:5:34:6 | av | file://:0:0:0:0 | int[] | GlobalVariable | | | | variables.cpp:34:5:34:6 | av | file://:0:0:0:0 | int[] | StaticStorageDurationVariable | | | -| variables.cpp:35:6:35:8 | ap1 | file://:0:0:0:0 | int * | DumpVariable | | | | variables.cpp:35:6:35:8 | ap1 | file://:0:0:0:0 | int * | GlobalVariable | | | | variables.cpp:35:6:35:8 | ap1 | file://:0:0:0:0 | int * | StaticStorageDurationVariable | | | -| variables.cpp:36:6:36:8 | ap2 | file://:0:0:0:0 | int * | DumpVariable | | | | variables.cpp:36:6:36:8 | ap2 | file://:0:0:0:0 | int * | GlobalVariable | | | | variables.cpp:36:6:36:8 | ap2 | file://:0:0:0:0 | int * | StaticStorageDurationVariable | | | -| variables.cpp:37:6:37:8 | ap3 | file://:0:0:0:0 | int * | DumpVariable | | | | variables.cpp:37:6:37:8 | ap3 | file://:0:0:0:0 | int * | GlobalVariable | | | | variables.cpp:37:6:37:8 | ap3 | file://:0:0:0:0 | int * | StaticStorageDurationVariable | | | -| variables.cpp:41:7:41:11 | local | file://:0:0:0:0 | char[] | DumpVariable | | | | variables.cpp:41:7:41:11 | local | file://:0:0:0:0 | char[] | LocalVariable | | | | variables.cpp:41:7:41:11 | local | file://:0:0:0:0 | char[] | SemanticStackVariable | | | -| variables.cpp:43:14:43:18 | local | file://:0:0:0:0 | int | DumpVariable | | static | | variables.cpp:43:14:43:18 | local | file://:0:0:0:0 | int | StaticLocalVariable | | static | -| variables.cpp:48:9:48:12 | name | file://:0:0:0:0 | char * | DumpVariable | | | | variables.cpp:48:9:48:12 | name | file://:0:0:0:0 | char * | Field | | | -| variables.cpp:49:12:49:17 | number | file://:0:0:0:0 | long | DumpVariable | | | | variables.cpp:49:12:49:17 | number | file://:0:0:0:0 | long | Field | | | -| variables.cpp:50:9:50:14 | street | file://:0:0:0:0 | char * | DumpVariable | | | | variables.cpp:50:9:50:14 | street | file://:0:0:0:0 | char * | Field | | | -| variables.cpp:51:9:51:12 | town | file://:0:0:0:0 | char * | DumpVariable | | | | variables.cpp:51:9:51:12 | town | file://:0:0:0:0 | char * | Field | | | -| variables.cpp:52:16:52:22 | country | file://:0:0:0:0 | char * | DumpVariable | | static | | variables.cpp:52:16:52:22 | country | file://:0:0:0:0 | char * | MemberVariable | | static | | variables.cpp:52:16:52:22 | country | file://:0:0:0:0 | char * | StaticStorageDurationVariable | | static | -| variables.cpp:56:14:56:29 | externInFunction | file://:0:0:0:0 | int | DumpVariable | | | | variables.cpp:56:14:56:29 | externInFunction | file://:0:0:0:0 | int | GlobalVariable | | | | variables.cpp:56:14:56:29 | externInFunction | file://:0:0:0:0 | int | StaticStorageDurationVariable | | | diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-311/semmle/tests/CleartextFileWrite.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-311/semmle/tests/CleartextFileWrite.expected index 61226e5e46f..d17ca2c429e 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-311/semmle/tests/CleartextFileWrite.expected +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-311/semmle/tests/CleartextFileWrite.expected @@ -1,3 +1,10 @@ +| test2.cpp:43:2:43:8 | call to fprintf | This write into file 'log' may contain unencrypted data from $@ | test2.cpp:43:36:43:43 | password | this source. | +| test2.cpp:44:2:44:8 | call to fprintf | This write into file 'log' may contain unencrypted data from $@ | test2.cpp:44:37:44:45 | thepasswd | this source. | +| test2.cpp:50:2:50:8 | call to fprintf | This write into file 'log' may contain unencrypted data from $@ | test2.cpp:50:41:50:53 | passwd_config | this source. | +| test2.cpp:54:2:54:8 | call to fprintf | This write into file 'log' may contain unencrypted data from $@ | test2.cpp:54:41:54:52 | widepassword | this source. | +| test2.cpp:55:2:55:8 | call to fprintf | This write into file 'log' may contain unencrypted data from $@ | test2.cpp:55:40:55:51 | widepassword | this source. | +| test2.cpp:57:2:57:8 | call to fprintf | This write into file 'log' may contain unencrypted data from $@ | test2.cpp:57:39:57:49 | call to getPassword | this source. | +| test2.cpp:65:3:65:9 | call to fprintf | This write into file 'log' may contain unencrypted data from $@ | test2.cpp:62:18:62:25 | password | this source. | | test.cpp:45:3:45:7 | call to fputs | This write into file 'file' may contain unencrypted data from $@ | test.cpp:45:9:45:19 | thePassword | this source. | | test.cpp:70:35:70:35 | call to operator<< | This write into file 'mystream' may contain unencrypted data from $@ | test.cpp:70:38:70:48 | thePassword | this source. | | test.cpp:73:37:73:41 | call to write | This write into file 'mystream' may contain unencrypted data from $@ | test.cpp:73:43:73:53 | thePassword | this source. | diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-311/semmle/tests/test2.cpp b/cpp/ql/test/query-tests/Security/CWE/CWE-311/semmle/tests/test2.cpp new file mode 100644 index 00000000000..fd672cf61bf --- /dev/null +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-311/semmle/tests/test2.cpp @@ -0,0 +1,94 @@ + +#define FILE int + +typedef unsigned long size_t; + +FILE *fopen(const char *filename, const char *mode); +int snprintf(char *s, size_t n, const char *format, ...); +int fprintf(FILE *stream, const char *format, ...); +char *strcpy(char *s1, const char *s2); + +char *crypt(char *input); + +struct myStruct +{ + // sensitive + char *password; + char *thepasswd; + char *accountkey; + wchar_t *widepassword; + + // encrypted + char password_hash[64]; + char *encrypted_passwd; + + // not sensitive + char *password_file; + char *password_path; + int num_passwords; + int *password_tries; + bool have_passwd; + + // dubious + char *passwd_config; + char *passwd_config2; +}; + +char *getPassword(); +char *getPasswordHash(); +int getPasswordMaxChars(); + +void tests(FILE *log, myStruct &s) +{ + fprintf(log, "password = %s\n", s.password); // BAD + fprintf(log, "thepasswd = %s\n", s.thepasswd); // BAD + fprintf(log, "accountkey = %s\n", s.accountkey); // DUBIOUS [NOT REPORTED] + fprintf(log, "password_hash = %s\n", s.password_hash); // GOOD + fprintf(log, "encrypted_passwd = %s\n", s.encrypted_passwd); // GOOD + fprintf(log, "password_file = %s\n", s.password_file); // GOOD + fprintf(log, "password_path = %s\n", s.password_path); // GOOD + fprintf(log, "passwd_config = %s\n", s.passwd_config); // DUBIOUS [REPORTED] + fprintf(log, "num_passwords = %i\n", s.num_passwords); // GOOD + fprintf(log, "password_tries = %i\n", *(s.password_tries)); // GOOD + fprintf(log, "have_passwd = %i\n", s.have_passwd); // GOOD + fprintf(log, "widepassword = %ls\n", s.widepassword); // BAD + fprintf(log, "widepassword = %S\n", s.widepassword); // BAD + + fprintf(log, "getPassword() = %s\n", getPassword()); // BAD + fprintf(log, "getPasswordHash() = %s\n", getPasswordHash()); // GOOD + fprintf(log, "getPasswordMaxChars() = %i\n", getPasswordMaxChars()); // GOOD + + { + char *cpy1 = s.password; + char *cpy2 = crypt(s.password); + + fprintf(log, "cpy1 = %s\n", cpy1); // BAD + fprintf(log, "cpy2 = %s\n", cpy2); // GOOD + } + + { + char buf[1024]; + + strcpy(buf, s.password); + fprintf(log, "buf = %s\n", buf); // BAD [NOT DETECTED] + + strcpy(buf, s.password_hash); + fprintf(log, "buf = %s\n", buf); // GOOD + } + + fprintf(log, "password = %p\n", s.password); // GOOD + + { + if (fopen(s.passwd_config2, "rt") == 0) + { + fprintf(log, "could not open file '%s'.\n", s.passwd_config2); // GOOD + } + } + + { + char buffer[1024]; + + snprintf(buffer, 1024, "password = %s", s.password); + fprintf(log, "log: %s", buffer); // BAD [NOT DETECTED] + } +} diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-367/semmle/TOCTOUFilesystemRace.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-367/semmle/TOCTOUFilesystemRace.expected index f514742ff0a..f0f7aaaefee 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-367/semmle/TOCTOUFilesystemRace.expected +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-367/semmle/TOCTOUFilesystemRace.expected @@ -1,3 +1,13 @@ -| test.cpp:21:3:21:8 | call to remove | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test.cpp:21:10:21:14 | file1 | filename | test.cpp:19:7:19:12 | call to rename | checked | -| test.cpp:35:3:35:8 | call to remove | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test.cpp:35:10:35:14 | file1 | filename | test.cpp:32:7:32:12 | call to rename | checked | -| test.cpp:49:3:49:8 | call to remove | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test.cpp:49:10:49:14 | file1 | filename | test.cpp:47:7:47:12 | call to rename | checked | +| test2.cpp:69:7:69:11 | call to fopen | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:69:13:69:16 | path | filename | test2.cpp:67:6:67:9 | call to stat | checked | +| test2.cpp:83:7:83:11 | call to fopen | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:83:13:83:16 | path | filename | test2.cpp:81:6:81:8 | buf | checked | +| test2.cpp:98:7:98:11 | call to fopen | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:98:13:98:16 | path | filename | test2.cpp:96:6:96:12 | buf_ptr | checked | +| test2.cpp:115:7:115:11 | call to fopen | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:115:13:115:16 | path | filename | test2.cpp:113:22:113:24 | buf | checked | +| test2.cpp:130:7:130:11 | call to fopen | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:130:13:130:16 | path | filename | test2.cpp:128:21:128:27 | buf_ptr | checked | +| test2.cpp:157:7:157:10 | call to open | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:157:12:157:15 | path | filename | test2.cpp:155:6:155:9 | call to stat | checked | +| test2.cpp:170:7:170:10 | call to open | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:170:12:170:15 | path | filename | test2.cpp:168:6:168:10 | call to lstat | checked | +| test2.cpp:245:3:245:7 | call to chmod | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:245:9:245:12 | path | filename | test2.cpp:238:6:238:10 | call to fopen | checked | +| test2.cpp:277:7:277:11 | call to fopen | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:277:13:277:16 | path | filename | test2.cpp:275:6:275:11 | call to access | checked | +| test2.cpp:303:7:303:11 | call to fopen | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:303:13:303:16 | path | filename | test2.cpp:301:7:301:12 | call to access | checked | +| test2.cpp:317:7:317:11 | call to fopen | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:317:13:317:16 | path | filename | test2.cpp:313:6:313:11 | call to access | checked | +| test2.cpp:348:3:348:7 | call to chmod | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:348:9:348:12 | path | filename | test2.cpp:341:6:341:10 | call to fopen | checked | +| test2.cpp:356:3:356:7 | call to chmod | The $@ being operated upon was previously $@, but the underlying file may have been changed since then. | test2.cpp:356:9:356:13 | path2 | filename | test2.cpp:354:7:354:12 | call to rename | checked | diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-367/semmle/test.cpp b/cpp/ql/test/query-tests/Security/CWE/CWE-367/semmle/test.cpp index b876146f571..9e0b22b5ced 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-367/semmle/test.cpp +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-367/semmle/test.cpp @@ -18,7 +18,7 @@ void test1() create(file1); if (!rename(file1, file2)) { - remove(file1); // BAD + remove(file1); // DUBIOUS (bad but perhaps not exploitable) } } @@ -32,7 +32,7 @@ void test2() if (!rename(file1, file2)) { file1.set("d.txt"); - remove(file1); // GOOD [FALSE POSITIVE] + remove(file1); // GOOD } } @@ -46,6 +46,6 @@ void test3() create(file1); if (!rename(file1, file2)) { - remove(file1); // BAD + remove(file1); // DUBIOUS (bad but perhaps not exploitable) } } diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-367/semmle/test2.cpp b/cpp/ql/test/query-tests/Security/CWE/CWE-367/semmle/test2.cpp new file mode 100644 index 00000000000..e1317e9a9ca --- /dev/null +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-367/semmle/test2.cpp @@ -0,0 +1,358 @@ +// More test cases. Some of these are inspired by real-world cases, others are synthetic or variations. + +#define NULL 0 + +typedef struct {} FILE; +typedef struct { + int foo; +} stat_data; + +FILE *fopen(const char *filename, const char *mode); +int fclose(FILE *stream); + +int open(const char *filename, int arg); +int creat(const char *filename, int arg); +int openat(int dir, const char *filename, int arg); +int close(int file); + +bool stat(const char *path, stat_data *buf); +bool fstat(int file, stat_data *buf); +bool lstat(const char *path, stat_data *buf); +bool fstatat(int dir, const char *path, stat_data *buf); +void chmod(const char *path, int setting); +int rename(const char *from, const char *to); +bool remove(const char *path); + +bool access(const char *path); + +// --- open -> open --- + +void test1_1(const char *path) +{ + FILE *f = NULL; + + f = fopen(path, "r"); + + if (f == NULL) + { + // retry + f = fopen(path, "r"); // GOOD (this is just trying again) + } + + // ... +} + +void test1_2(const char *path) +{ + FILE *f = NULL; + + // try until we succeed + while (f == NULL) + { + f = fopen(path, "r"); // GOOD (this is just trying again) + + // ... + } + + // ... +} + +// --- stat -> open --- + +void test2_1(const char *path) +{ + FILE *f = NULL; + stat_data buf; + + if (stat(path, &buf)) + { + f = fopen(path, "r"); // BAD + } + + // ... +} + +void test2_2(const char *path) +{ + FILE *f = NULL; + stat_data buf; + + stat(path, &buf); + if (buf.foo > 0) + { + f = fopen(path, "r"); // BAD + } + + // ... +} + +void test2_3(const char *path) +{ + FILE *f = NULL; + stat_data buf; + stat_data *buf_ptr = &buf; + + stat(path, buf_ptr); + if (buf_ptr->foo > 0) + { + f = fopen(path, "r"); // BAD + } + + // ... +} + +bool stat_condition(const stat_data *buf); +bool other_condition(); + +void test2_4(const char *path) +{ + FILE *f = NULL; + stat_data buf; + + stat(path, &buf); + if (stat_condition(&buf)) + { + f = fopen(path, "r"); // BAD + } + + // ... +} + +void test2_5(const char *path) +{ + FILE *f = NULL; + stat_data buf; + stat_data *buf_ptr = &buf; + + stat(path, buf_ptr); + if (stat_condition(buf_ptr)) + { + f = fopen(path, "r"); // BAD + } + + // ... +} + +void test2_6(const char *path) +{ + FILE *f = NULL; + stat_data buf; + + stat(path, &buf); + if (other_condition()) + { + f = fopen(path, "r"); // GOOD (does not depend on the result of stat) + } + + // ... +} + +void test2_7(const char *path, int arg) +{ + stat_data buf; + int f; + + if (stat(path, &buf)) + { + f = open(path, arg); // BAD + } + + // ... +} + +void test2_8(const char *path, int arg) +{ + stat_data buf; + int f; + + if (lstat(path, &buf)) + { + f = open(path, arg); // BAD + } + + // ... +} + +void test2_9(const char *path, int arg) +{ + stat_data buf; + int f; + + if (stat(path, &buf)) + { + f = creat(path, arg); // BAD [NOT DETECTED] + } + + // ... +} + +void test2_10(int dir, const char *path, int arg) +{ + stat_data buf; + int f; + + if (fstatat(dir, path, &buf)) + { + f = openat(dir, path, arg); // BAD [NOT DETECTED] + } + + // ... +} + +// --- open -> stat --- + +void test3_1(const char *path, int arg) +{ + stat_data buf; + int f; + + f = open(path, arg); + if (stat(path, &buf)) // BAD [NOT DETECTED] + { + // ... + } + + // ... +} + +void test3_2(const char *path, int arg) +{ + stat_data buf; + int f; + + f = open(path, arg); + if (fstat(f, &buf)) // GOOD (uses file descriptor, not path) + { + // ... + } + + // ... +} + +// --- open -> chmod --- + +void test4_1(const char *path) +{ + FILE *f = NULL; + + f = fopen(path, "w"); + if (f) + { + // ... + + fclose(f); + + chmod(path, 0); // BAD + } +} + +// --- rename -> remove / open --- + +void test5_1(const char *path1, const char *path2) +{ + if (rename(path1, path2)) + { + remove(path1); // DUBIOUS (bad but perhaps not exploitable) + } +} + +void test5_2(const char *path1, const char *path2) +{ + FILE *f = NULL; + + if (!rename(path1, path2)) + { + f = fopen(path2, "r"); // BAD [NOT DETECTED] + } +} + +// --- access -> open --- + +void test6_1(const char *path) +{ + FILE *f = NULL; + + if (access(path)) + { + f = fopen(path, "r"); // BAD + + // ... + } +} + +void test6_2(const char *path) +{ + FILE *f = NULL; + + if (access(path)) + { + // ... + } + + f = fopen(path, "r"); // GOOD (appears not to be intended to depend on the access check) + + // ... +} + +void test6_3(const char *path) +{ + FILE *f = NULL; + + if (!access(path)) + { + f = fopen(path, "r"); // BAD + + // ... + } +} + +void test6_4(const char *path) +{ + FILE *f = NULL; + + if (access(path)) + { + // ... + } else { + f = fopen(path, "r"); // BAD + + // ... + } +} + +void test6_5(const char *path1, const char *path2) +{ + FILE *f = NULL; + + if (access(path1)) + { + f = fopen(path2, "r"); // GOOD (different file) + + // ... + } +} + +// --- open / rename -> chmod --- + +void test7_1(const char *path) +{ + FILE *f; + + f = fopen(path, "wt"); + if (f != 0) + { + // ... + + fclose(f); + + chmod(path, 1234); // BAD + } +} + +void test7_1(const char *path1, const char *path2) +{ + if (!rename(path1, path2)) + { + chmod(path2, 1234); // BAD + } +} diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-416/semmle/tests/UseAfterFree.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-416/semmle/tests/UseAfterFree.expected index 9d8a9d2be24..92aa7a96756 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-416/semmle/tests/UseAfterFree.expected +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-416/semmle/tests/UseAfterFree.expected @@ -7,4 +7,3 @@ | test.cpp:170:6:170:9 | data | Memory pointed to by 'data' may have been previously freed $@ | test.cpp:165:2:165:5 | call to free | here | | test.cpp:193:6:193:9 | data | Memory pointed to by 'data' may have been previously freed $@ | test.cpp:191:3:191:6 | call to free | here | | test.cpp:201:6:201:6 | x | Memory pointed to by 'x' may have been previously freed $@ | test.cpp:200:2:200:9 | delete | here | -| test.cpp:242:14:242:17 | data | Memory pointed to by 'data' may have been previously freed $@ | test.cpp:243:11:243:14 | call to free | here | diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-416/semmle/tests/test.cpp b/cpp/ql/test/query-tests/Security/CWE/CWE-416/semmle/tests/test.cpp index 092a430808d..7018af457ba 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-416/semmle/tests/test.cpp +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-416/semmle/tests/test.cpp @@ -213,42 +213,3 @@ void regression_test_for_static_var_handling() data = (char *)malloc(100*sizeof(char)); use(data); // GOOD } - -void test16(int n, bool b) { - char* data = NULL; - for(int i = 0; i < n; ++i) { - if(b) data = (char*)malloc(10 * sizeof(char)); - if(!b || data == NULL) return; - use(data); // GOOD - free(data); // GOOD - } -} - -void test17(int n, bool b) { - char* data = (char*)malloc(10); - if(b) { - free(data); - } - - if(!b) { - use(data); // GOOD - } -} - -void test18(int* array) { - char* data = (char*)malloc(10 * sizeof(char)); - for (int i = 0; i < 4; ++i) { - int b = array[i]; - if(b) use(data); // BAD - if(!b) free(data); - } -} - -void test19(int* array) { - char* data = (char*)malloc(10 * sizeof(char)); - int b = array[0]; - for (int i = 0; i < 4; ++i) { - if(b) use(data); // GOOD - if(!b) free(data); - } -} diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-457/semmle/tests/UninitializedLocal.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-457/semmle/tests/UninitializedLocal.expected index 985be87585a..0c1d750a8f6 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-457/semmle/tests/UninitializedLocal.expected +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-457/semmle/tests/UninitializedLocal.expected @@ -8,6 +8,7 @@ | test.cpp:132:9:132:9 | j | The variable $@ may not be initialized here. | test.cpp:126:6:126:6 | j | j | | test.cpp:219:3:219:3 | x | The variable $@ may not be initialized here. | test.cpp:218:7:218:7 | x | x | | test.cpp:243:13:243:13 | i | The variable $@ may not be initialized here. | test.cpp:241:6:241:6 | i | i | +| test.cpp:329:9:329:11 | val | The variable $@ may not be initialized here. | test.cpp:321:6:321:8 | val | val | | test.cpp:336:10:336:10 | a | The variable $@ may not be initialized here. | test.cpp:333:7:333:7 | a | a | | test.cpp:369:10:369:10 | a | The variable $@ may not be initialized here. | test.cpp:358:7:358:7 | a | a | | test.cpp:378:9:378:11 | val | The variable $@ may not be initialized here. | test.cpp:359:6:359:8 | val | val | diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-457/semmle/tests/test.cpp b/cpp/ql/test/query-tests/Security/CWE/CWE-457/semmle/tests/test.cpp index 5902c21d6fc..d289d223a48 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-457/semmle/tests/test.cpp +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-457/semmle/tests/test.cpp @@ -326,7 +326,7 @@ int test28() { a = false; c = false; } - return val; // GOOD + return val; // GOOD [FALSE POSITIVE] } int test29() { diff --git a/csharp/ql/src/Bad Practices/UseOfSystemOutputStream.qhelp b/csharp/ql/src/Bad Practices/UseOfSystemOutputStream.qhelp index cc4804526b9..65cab70cab8 100644 --- a/csharp/ql/src/Bad Practices/UseOfSystemOutputStream.qhelp +++ b/csharp/ql/src/Bad Practices/UseOfSystemOutputStream.qhelp @@ -6,7 +6,7 @@

Writing directly to system output streams is often used as an unstructured form of logging. A proper logging mechanism is a better way to direct messages to the desired location and also ensures that no critical information is leaked to the standard outputs. The rule points out any -call to the Console.Write*(...) methods and any access to Console.Out or +call to the Console.Write*(...) methods and any access to Console.Out or Console.Error.

diff --git a/csharp/ql/src/Input Validation/ValueShadowingServerVariable.qhelp b/csharp/ql/src/Input Validation/ValueShadowingServerVariable.qhelp index 0589752453d..f5cdb7df8d7 100644 --- a/csharp/ql/src/Input Validation/ValueShadowingServerVariable.qhelp +++ b/csharp/ql/src/Input Validation/ValueShadowingServerVariable.qhelp @@ -6,7 +6,7 @@

Relying on HttpRequest to provide access to a particular server variable is not safe as it can be overridden by the client. The HttpRequest class implements an indexer to provide a simplified, combined access to its QueryString, Form -, Cookies, or ServerVariables collections, in that particular order. +, Cookies, or ServerVariables collections, in that particular order. When searching for a variable, the first match is returned: QueryString parameters hence supersede values from forms, cookies and server variables, and so on. This is a serious attack vector since an attacker could inject a value in the query string that you do not expect, diff --git a/csharp/ql/src/Linq/MissedCastOpportunity.qhelp b/csharp/ql/src/Linq/MissedCastOpportunity.qhelp index 5bd3c92019a..d4de22226c2 100644 --- a/csharp/ql/src/Linq/MissedCastOpportunity.qhelp +++ b/csharp/ql/src/Linq/MissedCastOpportunity.qhelp @@ -6,8 +6,8 @@

Casts are often used when you iterate over a collection of elements of a type that is known to contain only elements of a different type (possibly more specific). For example, List<Animal > might refer to a collection of instances of Dog, a class derived from - Animal. Programmers often write a loop to iterate over the collection and cast each - Animal in turn to Dog before using it

+Animal. Programmers often write a loop to iterate over the collection and cast each +Animal in turn to Dog before using it

diff --git a/docs/codeql/codeql-cli/about-codeql-packs.rst b/docs/codeql/codeql-cli/about-codeql-packs.rst new file mode 100644 index 00000000000..2f9c6eebd37 --- /dev/null +++ b/docs/codeql/codeql-cli/about-codeql-packs.rst @@ -0,0 +1,104 @@ +.. _about-codeql-packs: + +About CodeQL packs +================== + +.. include:: ../reusables/beta-note-package-management.rst + +CodeQL packs are used to create, share, depend on, and run CodeQL queries and libraries. You can publish your own CodeQL packs and download packs created by others. CodeQL packs contain queries, library files, query suites, and metadata. + +There are two types of CodeQL packs: query packs and library packs. + +* Query packs are designed to be run. When a query pack is published, the bundle includes all the transitive dependencies and a compilation cache. This ensures consistent and efficient execution of the queries in the pack. +* Library packs are designed to be used by query packs (or other library packs) and do not contain queries themselves. The libraries are not compiled and there is no compilation cache included when the pack is published. + +You can use the package management commands in the CodeQL CLI to create CodeQL packs, add dependencies to packs, and install or update dependencies. For more information, see ":ref:`Creating and working with CodeQL packs `." You can also publish and download CodeQL packs using the CodeQL CLI. For more information, see ":doc:`Publishing and using CodeQL packs `." + +CodeQL pack structure +--------------------- + +A CodeQL pack must contain a file called ``qlpack.yml`` in its root directory. In the ``qlpack.yml`` file, the ``name:`` field must have a value that follows the format of ``/``, where ```` is the GitHub organization or user account that the pack will be published to and ```` is the name of the pack. The other +files and directories within the pack should be logically organized. For example, typically: + +- Queries are organized into directories for specific categories. +- Queries for specific products, libraries, and frameworks are organized into + their own top-level directories. + +About ``qlpack.yml`` files +-------------------------- + +When executing query-related commands, CodeQL first looks in siblings of the installation directory (and their subdirectories) for ``qlpack.yml`` files. +Then it checks the package cache for CodeQL packs which have been downloaded. This means that when you are developing queries locally, the local packages +in the installation directory override packages of the same name in the package cache, so that you can test your local changes. + +The metadata in each `qlpack.yml`` file tells +CodeQL how to compile any queries in the pack, what libraries the pack depends on, and where to +find query suite definitions. + +The contents of the CodeQL pack (queries or libraries used in CodeQL analysis) is +included in the same directory as ``qlpack.yml``, or its subdirectories. + +The location of ``qlpack.yml`` defines the library path for the content +of the CodeQL pack. That is, for all ``.ql`` and ``.qll`` files in the pack, +CodeQL will resolve all import statements relative to the ``qlpack.yml`` at the +pack's root. + +.. _codeqlpack-yml-properties: + +``qlpack.yml`` properties +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following properties are supported in ``qlpack.yml`` files. + +.. list-table:: + :header-rows: 1 + :widths: auto + + * - Property + - Example + - Required + - Purpose + * - ``name`` + - ``octo-org/security-queries`` + - All packs + - The scope, where the CodeQL pack is published, and the name of the pack defined using alphanumeric characters and hyphens. It must be unique as CodeQL cannot differentiate between CodeQL packs with identical names. Name components cannot start or end with a hyphen. Additionally, a period is not allowed in pack names at all. Use the pack name to specify queries to run using ``database analyze`` and to define dependencies between QL packs (see examples below). + * - ``version`` + - ``0.0.0`` + - All packs + - A version range for this CodeQL pack. This must be a valid semantic version that meets the `SemVer v2.0.0 specification `__. + * - ``dependencies`` + - ``codeql/javascript-all: ^1.2.3`` + - Optional + - The names and version ranges of any CodeQL packs that this pack depends on, as a mapping. This gives the pack access to any libraries, database schema, and query suites defined in the dependency. For more information, see `SemVer ranges `__ in the NPM documentation. + * - ``suites`` + - ``octo-org-query-suites`` + - Optional + - The path to a directory in the pack that contains the query suites you want to make known to the CLI, defined relative to the pack directory. QL pack users can run "well-known" suites stored in this directory by specifying the pack name, without providing their full path. This is not supported for CodeQL packs downloaded from a package registry. For more information about query suites, see ":doc:`Creating CodeQL query suites `." + * - ``extractor`` + - ``javascript`` + - All test packs + - The CodeQL language extractor to use when the CLI creates a database in the pack. For more information about testing queries, see ":doc:`Testing custom queries `." + * - ``tests`` + - ``.`` + - Optional for test packs + - The path to a directory within the pack that contains tests, defined relative to the pack directory. Use ``.`` to specify the whole pack. Any queries in this directory are run as tests when ``test run`` is run with the ``--strict-test-discovery`` option. These queries are ignored by query suite definitions that use ``queries`` or ``qlpack`` instructions to ask for all queries in a particular pack. + * - ``dbscheme`` + - ``semmlecode.python.dbscheme`` + - Core language packs only + - The path to the :ref:`database schema ` for all libraries and queries written for this CodeQL language (see example below). + * - ``upgrades`` + - ``.`` + - Core language packs only + - The path to a directory within the pack that contains upgrade scripts, defined relative to the pack directory. The ``database upgrade`` action uses these scripts to update databases that were created by an older version of an extractor so they're compatible with the current extractor (see `Upgrade scripts for a language <#upgrade-scripts-for-a-language>`__ below.) + * - ``authors`` + - ``example@github.com`` + - All packs + - Metadata that will be displayed on the packaging search page in the packages section of the account that the CodeQL pack is published to. + * - ``licenses`` + - ``(LGPL-2.1 AND MIT)`` + - All packs + - Metadata that will be displayed on the packaging search page in the packages section of the account that the CodeQL pack is published to. For a list of allowed licenses, see `SPDX License List `__ in the SPDX Specification. + * - ``description`` + - ``Human-readable description of the contents of the CodeQL pack.`` + - All packs + - Metadata that will be displayed on the packaging search page in the packages section of the account that the CodeQL pack is published to. diff --git a/docs/codeql/codeql-cli/about-ql-packs.rst b/docs/codeql/codeql-cli/about-ql-packs.rst index b3f3c13ed88..35fdd3c1c60 100644 --- a/docs/codeql/codeql-cli/about-ql-packs.rst +++ b/docs/codeql/codeql-cli/about-ql-packs.rst @@ -87,7 +87,7 @@ The following properties are supported in ``qlpack.yml`` files. * - ``suites`` - ``suites`` - Optional - - The path to a directory that contains the "well-known" query suites in the pack, defined relative to the pack directory. You can run "well-known" suites stored in this directory by specifying the pack name, without providing their full path. To use query suites stored in other directories in the pack, you must provide their full path. For more information about query suites, see ":doc:`Creating CodeQL query suites `." + - The path to a directory in the pack that contains the query suites you want to make known to the CLI, defined relative to the pack directory. QL pack users can run "well-known" suites stored in this directory by specifying the pack name, without providing their full path. For more information about query suites, see ":doc:`Creating CodeQL query suites `." * - ``extractor`` - ``javascript`` - All test packs diff --git a/docs/codeql/codeql-cli/analyzing-databases-with-the-codeql-cli.rst b/docs/codeql/codeql-cli/analyzing-databases-with-the-codeql-cli.rst index d4e242ede3e..884eaf69444 100644 --- a/docs/codeql/codeql-cli/analyzing-databases-with-the-codeql-cli.rst +++ b/docs/codeql/codeql-cli/analyzing-databases-with-the-codeql-cli.rst @@ -16,9 +16,9 @@ For information about writing queries to run with ``database analyze``, see Before starting an analysis you must: - :doc:`Set up the CodeQL CLI ` so that it can find the queries - and libraries included in the CodeQL repository. -- :doc:`Create a CodeQL database ` for the source - code you want to analyze. + and libraries included in the CodeQL repository. +- :doc:`Create a CodeQL database ` for the source + code you want to analyze. Running ``codeql database analyze`` @@ -65,7 +65,7 @@ You can also specify: - .. include:: ../reusables/threads-query-execution.rst -.. pull-quote:: +.. pull-quote:: Upgrading databases @@ -94,7 +94,7 @@ Running a single query To run a single query over a CodeQL database for a JavaScript codebase, you could use the following command from the directory containing your database:: - codeql database analyze ../ql/javascript/ql/src/Declarations/UnusedVariable.ql --format=csv --output=js-analysis/js-results.csv + codeql database analyze ../ql/javascript/ql/src/Declarations/UnusedVariable.ql --format=csv --output=js-analysis/js-results.csv This command runs a simple query that finds potential bugs related to unused variables, imports, functions, or classes---it is one of the JavaScript @@ -102,28 +102,44 @@ queries included in the CodeQL repository. You could run more than one query by specifying a space-separated list of similar paths. The analysis generates a CSV file (``js-results.csv``) in a new directory -(``js-analysis``). +(``js-analysis``). You can also run your own custom queries with the ``database analyze`` command. For more information about preparing your queries to use with the CodeQL CLI, see ":doc:`Using custom queries with the CodeQL CLI `." -Running GitHub code scanning suites -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Running a CodeQL pack +~~~~~~~~~~~~~~~~~~~~~ -To run the GitHub code scanning suite of queries over a CodeQL database for a C/C++ codebase, +.. include:: ../reusables/beta-note-package-management.rst + +To run an existing CodeQL query pack from the GitHub Container registry, you need to download it first:: + + codeql pack download microsoft/coding-standards@1.0.0 + +Afterwards, you can run the pack on a specific database:: + + codeql database analyze microsoft/coding-standards@1.0.0 / --format=sarifv2.1.0 --output=query-results.sarif + +The ``analyze`` command above runs the default suite from ``microsoft/coding-standards v1.0.0`` and the latest version of ``scope/other-pack`` on the specified database. +For further information about default suites, see ":ref:`Publishing and using CodeQL packs `". + +Running query suites +~~~~~~~~~~~~~~~~~~~~ + +To run a query suite over a CodeQL database for a C/C++ codebase, you could use the following command from the directory containing your database:: codeql database analyze cpp-code-scanning.qls --format=sarifv2.1.0 --output=cpp-results.sarif The analysis generates a file in the v2.1.0 SARIF format that is supported by all versions of GitHub. -This file can be uploaded to GitHub using ``github upload-results`` or the code scanning API. -For more information, see `Analyzing a CodeQL database `__ +This file can be uploaded to GitHub by executing ``codeql github upload-results`` or the code scanning API. +For more information, see `Analyzing a CodeQL database `__ or `Code scanning API `__ in the GitHub documentation. -CodeQL query suites are ``.qls`` files that use directives to select queries to run +CodeQL query suites are ``.qls`` files that use directives to select queries to run based on certain metadata properties. The standard QL packs have metadata that specify -the location of the code scanning suites, so the CodeQL CLI knows where to find these +the location of the query suites used by code scanning, so the CodeQL CLI knows where to find these suite files automatically, and you don't have to specify the full path on the command line. For more information, see ":ref:`About QL packs `." @@ -137,7 +153,7 @@ and at the following path in the CodeQL for Go repository:: ql/src/codeql-suites/go-code-scanning.qls The repository also includes the query suites used by `LGTM.com `__. -These are stored alongside the code scanning suites with names of the form: ``-lgtm.qls``. +These are stored alongside the query suites for code scanning with names of the form: ``-lgtm.qls``. For information about creating custom query suites, see ":doc:`Creating CodeQL query suites `." @@ -149,12 +165,21 @@ When you create a CodeQL database, the extractor stores diagnostic data in the d If the analysis found fewer results for standard queries than you expected, review the results of the diagnostic and summary queries to check whether the CodeQL database is likely to be a good representation of the codebase that you want to analyze. +Integrating a CodeQL pack into a code scanning workflow in GitHub +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. include:: ../reusables/beta-note-package-management.rst + +You can use CodeQL query packs in your Code Scanning setup. This allows you to select query packs published by various sources and use them to analyze your code. +For more information, see "`Using CodeQL query packs in the CodeQL action `_" or "`Downloading and using CodeQL query packs in your CI system `_." + + Running all queries in a directory ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can run all the queries located in a directory by providing the directory path, rather than listing all the individual query files. Paths are searched -recursively, so any queries contained in subfolders will also be executed. +recursively, so any queries contained in subfolders will also be executed. .. pull-quote:: @@ -164,12 +189,12 @@ recursively, so any queries contained in subfolders will also be executed. ` when executing ``database analyze`` as it contains some special queries that aren't designed to be used with the command. Rather, to run a wide range of useful queries, run one of the - LGTM.com query suites. - + LGTM.com query suites. + For example, to execute all Python queries contained in the ``Functions`` directory you would run:: - codeql database analyze ../ql/python/ql/src/Functions/ --format=sarif-latest --output=python-analysis/python-results.sarif + codeql database analyze ../ql/python/ql/src/Functions/ --format=sarif-latest --output=python-analysis/python-results.sarif A SARIF results file is generated. Specifying ``--format=sarif-latest`` ensures that the results are formatted according to the most recent SARIF specification @@ -196,19 +221,19 @@ corresponds to an alert. Each line is a comma-separated list with the following - Description - Example * - Name - - Name of the query that identified the result. + - Name of the query that identified the result. - ``Inefficient regular expression`` * - Description - - Description of the query. - - ``A regular expression that requires exponential time to match certain - inputs can be a performance bottleneck, and may be vulnerable to + - Description of the query. + - ``A regular expression that requires exponential time to match certain + inputs can be a performance bottleneck, and may be vulnerable to denial-of-service attacks.`` * - Severity - Severity of the query. - ``error`` * - Message - - Alert message. - - ``This part of the regular expression may cause exponential backtracking + - Alert message. + - ``This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '\\\\'.`` * - Path - Path of the file containing the alert. @@ -225,14 +250,14 @@ corresponds to an alert. Each line is a comma-separated list with the following included when the same value as the start line. - ``64`` * - End column - - Where available, the column of the end line that marks the end of the + - Where available, the column of the end line that marks the end of the alert code. Otherwise the end line is repeated. - ``617`` Results files can be integrated into your own code-review or debugging infrastructure. For example, SARIF file output can be used to highlight alerts in the correct location in your source code using a SARIF viewer plugin for your -IDE. +IDE. Further reading --------------- diff --git a/docs/codeql/codeql-cli/codeql-cli-reference.rst b/docs/codeql/codeql-cli/codeql-cli-reference.rst index 0c53121d379..2cc5e755350 100644 --- a/docs/codeql/codeql-cli/codeql-cli-reference.rst +++ b/docs/codeql/codeql-cli/codeql-cli-reference.rst @@ -9,12 +9,14 @@ Learn more about the files you can use when running CodeQL processes and the res :titlesonly: :hidden: + about-codeql-packs about-ql-packs query-reference-files sarif-output exit-codes +- :doc:`About CodeQL packs `: CodeQL packs are created with the CodeQL CLI and are used to create, depend on, publish, and run CodeQL queries and libraries. - :doc:`About QL packs `: QL packs are used to organize the files used in CodeQL analysis. They contain queries, library files, query suites, and important metadata. - :doc:`Query reference files `: A query reference file is text file that defines the location of one query to test. diff --git a/docs/codeql/codeql-cli/creating-and-working-with-codeql-packs.rst b/docs/codeql/codeql-cli/creating-and-working-with-codeql-packs.rst new file mode 100644 index 00000000000..6373440bcbb --- /dev/null +++ b/docs/codeql/codeql-cli/creating-and-working-with-codeql-packs.rst @@ -0,0 +1,70 @@ +.. _creating-and-working-with-codeql-packs: + +Creating and working with CodeQL packs +====================================== + +You can use CodeQL packs to create, share, depend on, and run CodeQL queries and libraries. + +.. include:: ../reusables/beta-note-package-management.rst + +About CodeQL packs and the CodeQL CLI +------------------------------------- + +With CodeQL packs and the package management commands in the CodeQL CLI, you can publish your custom queries and integrate them into your codebase analysis. + +There are two types of CodeQL packs: query packs and library packs. + +* Query packs are designed to be run. When a query pack is published, the bundle includes all the transitive dependencies and a compilation cache. This ensures consistent and efficient execution of the queries in the pack. +* Library packs are designed to be used by query packs (or other library packs) and do not contain queries themselves. The libraries are not compiled and there is no compilation cache included when the pack is published. + +You can use the ``pack`` command in the CodeQL CLI to create CodeQL packs, add dependencies to packs, and install or update dependencies. You can also publish and download CodeQL packs using the ``pack`` command. For more information, see ":doc:`Publishing and using CodeQL packs `." + +Creating a CodeQL pack +---------------------- +You can create a CodeQL pack by running the following command from the checkout root of your project: + +:: + + codeql pack init / + +You must specify: + +- ````: the name of the GitHub organization or user account that you will publish to. +- ````: the name for the pack that you are creating. + +The ``codeql pack init`` command creates the directory structure and configuration files for a CodeQL pack. By default, the command creates a query pack. If you want to create a library pack, you must edit the ``qlpack.yml`` file to explicitly declare the file as a library pack by including the ``library:true`` property. + +Modifying an existing QL pack to create a CodeQL pack +----------------------------------------------------- +If you already have a ``qlpack.yml`` file, you can edit it manually to convert it into a CodeQL pack. + +#. Edit the ``name`` property so that it matches the format ``/``, where ```` is the name of the GitHub organization or user account that you will publish to. +#. In the ``qlpack.yml`` file, include a ``version`` property with a semver identifier, as well as an optional ``dependencies`` block. + +For more information about the properties, see ":ref:`About CodeQL packs `." + +Adding and installing dependencies to a CodeQL pack +--------------------------------------------------- +You can add dependencies on CodeQL packs using the command ``codeql pack add``. You must specify the scope, name, and version range. + +:: + + codeql pack add /@x.x.x / + +The version range is optional. If you leave off the version range, the latest version will be added. Otherwise, the latest version that satisfies the requested range will be added. + +This command updates the ``qlpack.yml`` file with the requested dependencies and downloads them into the package cache. Please note that this command will reformat the file and remove all comments. + +You can also manually edit the ``qlpack.yml`` file to include dependencies and install the dependencies with the command: + +:: + + codeql pack install + +This command downloads all dependencies to the shared cache on the local disk. + +.. pull-quote:: + + Note + + Running the ``codeql pack add`` and ``codeql pack install`` commands will generate or update the ``qlpack.lock.yml`` file. This file should be checked-in to version control. The ``qlpack.lock.yml`` file contains the precise version numbers used by the pack. diff --git a/docs/codeql/codeql-cli/getting-started-with-the-codeql-cli.rst b/docs/codeql/codeql-cli/getting-started-with-the-codeql-cli.rst index c9bcbe5d6c5..1144c1c3e6c 100644 --- a/docs/codeql/codeql-cli/getting-started-with-the-codeql-cli.rst +++ b/docs/codeql/codeql-cli/getting-started-with-the-codeql-cli.rst @@ -18,9 +18,9 @@ structures. To get started quickly, we recommend adopting a relatively simple setup, as outlined in the steps below. If you use Linux, Windows, or macOS version 10.14 ("Mojave") or earlier, simply -follow the steps below. For macOS version 10.15 ("Catalina"), steps 1 and 4 are -slightly different---for further details, see the sections labeled **Information -for macOS "Catalina" users**. +follow the steps below. For macOS version 10.15 ("Catalina") or newer, steps 1 +and 4 are slightly different---for further details, see the sections labeled +**Information for macOS "Catalina" (or newer) users**. For information about installing the CodeQL CLI in a CI system to create results to display in GitHub as code scanning alerts, see @@ -66,13 +66,14 @@ Alternatively, you can download ``codeql.zip``, which contains the CLI for all s .. container:: name - **Information for macOS "Catalina" users** + **Information for macOS "Catalina" (or newer) users** - .. pull-quote:: macOS "Catalina" + .. pull-quote:: macOS "Catalina" (or newer) - If you use macOS version 10.15 ("Catalina"), you need to ensure that your web - browser does not automatically extract zip files. If you use Safari, - complete the following steps before downloading the CodeQL CLI zip archive: + If you use macOS version 10.15 ("Catalina"), version 11 ("Big Sur"), or the upcoming + version 12 ("Monterey"), you need to ensure that your web browser does not automatically + extract zip files. If you use Safari, complete the following steps before downloading + the CodeQL CLI zip archive: i. Open Safari. ii. From the Safari menu, select **Preferences...**. @@ -164,16 +165,17 @@ For example, if the path to your copy of the CodeQL repository is .. container:: name - **Information for macOS "Catalina" users** + **Information for macOS "Catalina" (or newer) users** .. pull-quote:: macOS "Catalina" - macOS "Catalina" users should run the following commands in the Terminal, - where ``${install_loc}`` is the path to the directory you created in step 2: + macOS "Catalina", "Big Sur", or "Monterey" users should run the following + commands in the Terminal, where ``${install_loc}`` is the path to the + directory you created in step 2: i. ``mv ~/Downloads/codeql*.zip ${install_loc}`` ii. ``cd ${install_loc}`` - iii. ``xattr -c codeql*.zip`` + iii. ``/usr/bin/xattr -c codeql*.zip`` iv. ``unzip codeql*.zip`` 5. Launch ``codeql`` diff --git a/docs/codeql/codeql-cli/publishing-and-using-codeql-packs.rst b/docs/codeql/codeql-cli/publishing-and-using-codeql-packs.rst new file mode 100644 index 00000000000..d28e27e10d7 --- /dev/null +++ b/docs/codeql/codeql-cli/publishing-and-using-codeql-packs.rst @@ -0,0 +1,74 @@ +.. _publishing-and-using-codeql-packs: + +Publishing and using CodeQL packs +================================= + +You can publish your own CodeQL packs and use packs published by other people. + +.. include:: ../reusables/beta-note-package-management.rst + +Configuring the ``qlpack.yml`` file before publishing +----------------------------------------------------- + +You can check and modify the configuration details of your CodeQL pack prior to publishing. Open the ``qlpack.yml`` file in your preferred text editor. + +.. code-block:: none + + library: # set to true if the pack is a library. Set to false or omit for a query pack + name: / + version: + description: + default-suite: # optional, one or more queries in the pack to run by default + - query: /query-file>.ql + default-suite-file: default-queries.qls # optional, a pointer to a query-suite in this pack + license: # optional, the license under which the pack is published + dependencies: # map from CodeQL pack name to version range + +- ``name:`` must follow the / format, where is the GitHub organization that you will publish to and is the name for the pack. +- A maximum of one of ``default-suite`` or ``default-suite-file`` is allowed. These are two different ways to define a default query suite to be run, the first by specifying queries directly in the `qlpack.yml` file and the second by specifying a query suite in the pack. + +Running ``codeql pack publish`` +------------------------------- + +When you are ready to publish a pack to the GitHub Container registry, you can run the following command in the root of the pack directory: + +:: + + codeql pack publish + +The published package will be displayed in the packages section of GitHub organization specified by the scope in the ``qlpack.yml`` file. + +Running ``codeql pack download /`` +----------------------------------------------- + +To run a pack that someone else has created, you must first download it by running the following command: + +:: + + codeql pack download /@x.x.x + +- ````: the name of the GitHub organization that you will download from. +- ````: the name for the pack that you want to download. +- ``@x.x.x``: an optional version number. If omitted, the latest version will be downloaded. + +This command accepts arguments for multiple packs. + +Using a CodeQL pack to analyze a CodeQL database +------------------------------------------------ + +To analyze a CodeQL database with a CodeQL pack, run the following command: + +:: + + codeql database analyze /@x.x.x + +- ````: the CodeQL database to be analyzed. +- ````: the name of the GitHub organization that the pack is published to. +- ````: the name for the pack that you are using. +- ``@x.x.x``: an optional version number. If omitted, the latest version will be used. + +The ``analyze`` command will run the default suite of any specified CodeQL packs. You can specify multiple CodeQL packs to be used for analyzing a CodeQL database. For example: + +:: + + codeql analyze / / diff --git a/docs/codeql/codeql-cli/using-custom-queries-with-the-codeql-cli.rst b/docs/codeql/codeql-cli/using-custom-queries-with-the-codeql-cli.rst index 51d63ed3709..8208cd59a56 100644 --- a/docs/codeql/codeql-cli/using-custom-queries-with-the-codeql-cli.rst +++ b/docs/codeql/codeql-cli/using-custom-queries-with-the-codeql-cli.rst @@ -8,7 +8,7 @@ specific vulnerabilities or errors. This topic is specifically about writing queries to use with the `database analyze <../manual/database-analyze>`__ -command to produce :ref:`interpreted results `. +command to produce :ref:`interpreted results `. .. include:: ../reusables/advanced-query-execution.rst @@ -26,16 +26,16 @@ Including query metadata ------------------------ Query metadata is included at the top of each query file. It provides users with information about -the query, and tells the CodeQL CLI how to process the query results. +the query, and tells the CodeQL CLI how to process the query results. When running queries with the ``database analyze`` command, you must include the following two properties to ensure that the results are interpreted correctly: - Query identifier (``@id``): a sequence of words composed of lowercase letters or - digits, delimited by ``/`` or ``-``, identifying and classifying the query. + digits, delimited by ``/`` or ``-``, identifying and classifying the query. - Query type (``@kind``): identifies the query as a simple alert (``@kind problem``), - an alert documented by a sequence of code locations (``@kind path-problem``), - for extractor troubleshooting (``@kind diagnostic``), or a summary metric + an alert documented by a sequence of code locations (``@kind path-problem``), + for extractor troubleshooting (``@kind diagnostic``), or a summary metric (``@kind metric`` and ``@tags summary``). For more information about these metadata properties, see ":ref:`Metadata for CodeQL queries @@ -47,22 +47,30 @@ For more information about these metadata properties, see ":ref:`Metadata for Co Metadata requirements may differ if you want to use your query with other applications. For more information, see ":ref:`Metadata for CodeQL queries ` - ." + ." +Packaging custom QL queries +--------------------------- -Creating a custom QL pack -------------------------- +.. include:: ../reusables/beta-note-package-management.rst -When writing your own queries, you should save them in a custom QL pack -directory. QL packs provide a way of organizing -the files used in CodeQL analysis. This directory must contain a file -named ``qlpack.yml`` at the root. Your custom queries should be saved in the QL -pack root, or its subdirectories. +When you write your own queries, you should save them in a custom QL pack +directory. When you are ready to share your queries with other users, you can publish the pack as a CodeQL pack to GitHub Packages - the GitHub Container registry. + +QL packs organize the files used in CodeQL analysis and can store queries, +library files, query suites, and important metadata. Their root directory must +contain a file named ``qlpack.yml``. Your custom queries should be saved in the +QL pack root, or its subdirectories. For each QL pack, the ``qlpack.yml`` file includes information that tells CodeQL -how to compile the queries, what libraries the pack depends on, and where to find -query suite definitions. For more information about what to include in this -file, see ":ref:`About QL packs `." +how to compile the queries, which other CodeQL packs and libraries the pack +depends on, and where to find query suite definitions. For more information +about what to include in this file, see ":ref:`About QL packs `." + +CodeQL packages are used to create, share, depend on, and run CodeQL queries and +libraries. You can publish your own CodeQL packages and download ones created by +others via the the Container registry. For further information see +":ref:`About CodeQL packs `." Contributing to the CodeQL repository ------------------------------------- diff --git a/docs/codeql/codeql-cli/using-the-codeql-cli.rst b/docs/codeql/codeql-cli/using-the-codeql-cli.rst index 064f6369eca..d48d8b19e4d 100644 --- a/docs/codeql/codeql-cli/using-the-codeql-cli.rst +++ b/docs/codeql/codeql-cli/using-the-codeql-cli.rst @@ -41,6 +41,12 @@ See the following links to learn how to get set up and run CodeQL commands: Test query help files by rendering them as markdown to ensure they are valid before adding them to the CodeQL repository or using them in code scanning. +- :doc:`Creating and working with CodeQL packs `: + Create, share, depend on, and run CodeQL queries and libraries. + +- :doc:`Publishing and using CodeQL packs `: + Publish your own or use others CodeQL packs for code scanning. + - :doc:`Specifying command options in a CodeQL configuration file `: You can save default or frequently used options for your commands in a per-user configuration file. @@ -57,4 +63,6 @@ See the following links to learn how to get set up and run CodeQL commands: creating-codeql-query-suites testing-custom-queries testing-query-help-files + creating-and-working-with-codeql-packs + publishing-and-using-codeql-packs Specifying command options diff --git a/docs/codeql/ql-language-reference/ql-language-specification.rst b/docs/codeql/ql-language-reference/ql-language-specification.rst index c1b2f830dba..38a55edc8fa 100644 --- a/docs/codeql/ql-language-reference/ql-language-specification.rst +++ b/docs/codeql/ql-language-reference/ql-language-specification.rst @@ -497,6 +497,7 @@ The following sequences of characters are keyword tokens: max min module + newtype none not or @@ -514,6 +515,7 @@ The following sequences of characters are keyword tokens: then this true + unique where Operators diff --git a/docs/codeql/ql-language-reference/variables.rst b/docs/codeql/ql-language-reference/variables.rst index 7f151a362e9..2b84e125971 100644 --- a/docs/codeql/ql-language-reference/variables.rst +++ b/docs/codeql/ql-language-reference/variables.rst @@ -57,7 +57,7 @@ As an aside, note that the following query leads to a compile-time error: select i In theory, it would have infinitely many results, as the variable ``i`` is not constrained to a -finite number of possible values. For more informaion, see ":ref:`binding`." +finite number of possible values. For more information, see ":ref:`binding`." .. index:: variable; free, variable; bound .. _free-variables: diff --git a/docs/codeql/query-help/codeql-cwe-coverage.md b/docs/codeql/query-help/codeql-cwe-coverage.rst similarity index 54% rename from docs/codeql/query-help/codeql-cwe-coverage.md rename to docs/codeql/query-help/codeql-cwe-coverage.rst index 245318a5258..ca7d73ba587 100644 --- a/docs/codeql/query-help/codeql-cwe-coverage.md +++ b/docs/codeql/query-help/codeql-cwe-coverage.rst @@ -1,8 +1,10 @@ -# CodeQL CWE coverage +CodeQL CWE coverage +=================== -An overview of the coverage of MITRE's Common Weakness Enumeration (CWE) for the latest release of CodeQL. +You can view the full coverage of MITRE's Common Weakness Enumeration (CWE) or coverage by language for the latest release of CodeQL. -## About CWEs +About CWEs +########## The CWE categorization contains several types of entity, collectively known as CWEs. The CWEs that we consider in this report are only those of the types: @@ -11,15 +13,22 @@ The CWE categorization contains several types of entity, collectively known as C - Weakness Variant - Compound Element -Other types of CWE do not correspond directly to weaknesses, so are omitted. +Other types of CWE that do not correspond directly to weaknesses are omitted. The CWE categorization includes relationships between entities, in particular a parent-child relationship. -These relationships are associated with Views (another kind of CWE entity). For the purposes of coverage claims, we use the "[Research View](https://cwe.mitre.org/data/definitions/1000.html)." +These relationships are associated with Views (another kind of CWE entity). For the purposes of coverage claims, we use the "`Research View `_." Every security query is associated with one or more CWEs, which are the most precise CWEs that are covered by that query. Overall coverage is claimed for the most-precise CWEs, as well as for any of their ancestors in the View. -## Overview - - +.. toctree:: + :hidden: + :titlesonly: + full-cwe + cpp-cwe + csharp-cwe + go-cwe + java-cwe + javascript-cwe + python-cwe diff --git a/docs/codeql/query-help/cpp-cwe.md b/docs/codeql/query-help/cpp-cwe.md new file mode 100644 index 00000000000..5a2b657c3f0 --- /dev/null +++ b/docs/codeql/query-help/cpp-cwe.md @@ -0,0 +1,8 @@ +# CWE coverage for C and C++ + +An overview of CWE coverage for C and C++ in the latest release of CodeQL. + +## Overview + + + diff --git a/docs/codeql/query-help/csharp-cwe.md b/docs/codeql/query-help/csharp-cwe.md new file mode 100644 index 00000000000..228d4d21192 --- /dev/null +++ b/docs/codeql/query-help/csharp-cwe.md @@ -0,0 +1,8 @@ +# CWE coverage for C# + +An overview of CWE coverage for C# in the latest release of CodeQL. + +## Overview + + + diff --git a/docs/codeql/query-help/full-cwe.md b/docs/codeql/query-help/full-cwe.md new file mode 100644 index 00000000000..51f28fdb24e --- /dev/null +++ b/docs/codeql/query-help/full-cwe.md @@ -0,0 +1,8 @@ +# CodeQL full CWE coverage + +An overview of the full coverage of MITRE's Common Weakness Enumeration (CWE) for the latest release of CodeQL. + +## Overview + + + diff --git a/docs/codeql/query-help/go-cwe.md b/docs/codeql/query-help/go-cwe.md new file mode 100644 index 00000000000..cfcac3dae20 --- /dev/null +++ b/docs/codeql/query-help/go-cwe.md @@ -0,0 +1,8 @@ +# CWE coverage for Go + +An overview of CWE coverage for Go in the latest release of CodeQL. + +## Overview + + + diff --git a/docs/codeql/query-help/java-cwe.md b/docs/codeql/query-help/java-cwe.md new file mode 100644 index 00000000000..bbcc061b754 --- /dev/null +++ b/docs/codeql/query-help/java-cwe.md @@ -0,0 +1,8 @@ +# CWE coverage for Java + +An overview of CWE coverage for Java in the latest release of CodeQL. + +## Overview + + + diff --git a/docs/codeql/query-help/javascript-cwe.md b/docs/codeql/query-help/javascript-cwe.md new file mode 100644 index 00000000000..628d07a38bf --- /dev/null +++ b/docs/codeql/query-help/javascript-cwe.md @@ -0,0 +1,8 @@ +# CWE coverage for JavaScript + +An overview of CWE coverage for JavaScript in the latest release of CodeQL. + +## Overview + + + diff --git a/docs/codeql/query-help/python-cwe.md b/docs/codeql/query-help/python-cwe.md new file mode 100644 index 00000000000..fbee93b3cb9 --- /dev/null +++ b/docs/codeql/query-help/python-cwe.md @@ -0,0 +1,8 @@ +# CWE coverage for Python + +An overview of CWE coverage for Python in the latest release of CodeQL. + +## Overview + + + diff --git a/docs/codeql/query-help/readme.md b/docs/codeql/query-help/readme.md index 4e98220f759..a6b6faee48e 100644 --- a/docs/codeql/query-help/readme.md +++ b/docs/codeql/query-help/readme.md @@ -2,7 +2,9 @@ CodeQL query help Sphinx documentation -------------------------------------- This project supplies the configuration and some boiler plate -index files for the CodeQL query help documentation. +index files for the CodeQL query help and CWE coverage documentation. The query help itself is automatically generated by the -"Generate CodeQL query help documentation using Sphinx" workflow. \ No newline at end of file +"Generate CodeQL query help documentation using Sphinx" workflow. + +The CWE coverage tables are generated and appended to pages by the "Docs generate query help" workflow in the `semmle-code` repository. diff --git a/docs/codeql/reusables/beta-note-package-management.rst b/docs/codeql/reusables/beta-note-package-management.rst new file mode 100644 index 00000000000..025e89489ab --- /dev/null +++ b/docs/codeql/reusables/beta-note-package-management.rst @@ -0,0 +1,5 @@ +.. pull-quote:: + + Note + + The CodeQL package management functionality, including CodeQL packs, is currently available as a beta release and is subject to change. During the beta release, CodeQL packs are available only using GitHub Packages - the GitHub Container registry. To use this beta functionality, install the beta release of the CodeQL CLI bundle from: https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.6.0-beta.1. \ No newline at end of file diff --git a/docs/codeql/support/reusables/frameworks.rst b/docs/codeql/support/reusables/frameworks.rst index 9404f2dfb00..01d6c2cb312 100644 --- a/docs/codeql/support/reusables/frameworks.rst +++ b/docs/codeql/support/reusables/frameworks.rst @@ -95,6 +95,7 @@ Java built-in support Guava, Utility and collections library Hibernate, Database iBatis / MyBatis, Database + Jackson, Serialization Java Persistence API (JPA), Database JDBC, Database Protobuf, Serialization diff --git a/docs/codeql/writing-codeql-queries/metadata-for-codeql-queries.rst b/docs/codeql/writing-codeql-queries/metadata-for-codeql-queries.rst index 93e164ac8ed..1af60dc6921 100644 --- a/docs/codeql/writing-codeql-queries/metadata-for-codeql-queries.rst +++ b/docs/codeql/writing-codeql-queries/metadata-for-codeql-queries.rst @@ -45,10 +45,12 @@ The following properties are supported by all query files: | | | ``high`` | | | | | ``very-high`` | | +-----------------------+---------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| ``@problem.severity`` | | ``error`` | Defines the level of severity of any alerts generated by the query. This, along with the ``@precision`` property, determines whether the results are displayed by default on LGTM. | +| ``@problem.severity`` | | ``error`` | Defines the level of severity of any alerts generated by a non-security query. This, along with the ``@precision`` property, determines whether the results are displayed by default on LGTM. | | | | ``warning`` | | | | | ``recommendation`` | | +-----------------------+---------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| ``@security-severity``| ```` | Defines the level of severity, between 0.0 and 10.0, for queries with `@tags security`. For more information about calculating `@security-severity`, see the [GitHub changelog](https://github.blog/changelog/2021-07-19-codeql-code-scanning-new-severity-levels-for-security-alerts/). | ++-----------------------+---------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ Additional properties for filter queries ---------------------------------------- diff --git a/docs/query-help-style-guide.md b/docs/query-help-style-guide.md index f7377b6c955..0964b233c39 100644 --- a/docs/query-help-style-guide.md +++ b/docs/query-help-style-guide.md @@ -110,6 +110,21 @@ For example: If your query checks code for a CWE weakness, you should use the `@tags` element in the query file to reference the associated CWEs, as explained [here](query-metadata-style-guide.md). When you use these tags, a link to the appropriate entry from the [MITRE.org](https://cwe.mitre.org/scoring/index.html) site will automatically appear as a reference in the output HTML file. +## Validating qhelp files + +Before making a pull request, please ensure the `.qhelp` files are well-formed and can be generated without errors. This can be done locally with the CodeQL CLI, as shown in the following example: + +```bash +# codeql generate query-help --format= +# For example: +codeql generate query-help ./myCustomQuery.qhelp --format=markdown +``` + + +Please include the `.qhelp` files (and any associated code snippets) in your pull request, but do not commit the generated Markdown. + +More information on how to test your `.qhelp` files can be found [within the documentation](https://codeql.github.com/docs/codeql-cli/testing-query-help-files/) + ## Query help example The following example is a query help file for a query from the standard query suite for Java: diff --git a/docs/query-metadata-style-guide.md b/docs/query-metadata-style-guide.md index 074e2d47528..544c1ac4cb4 100644 --- a/docs/query-metadata-style-guide.md +++ b/docs/query-metadata-style-guide.md @@ -115,10 +115,11 @@ Alert queries (`@kind problem` or `path-problem`) support two further properties * `medium` * `high` * `very-high` -* `@problem.severity`–defines the level of severity of the alert: +* `@problem.severity`–defines the level of severity of non-security alerts: * `error`–an issue that is likely to cause incorrect program behavior, for example a crash or vulnerability. * `warning`–an issue that indicates a potential problem in the code, or makes the code fragile if another (unrelated) part of code is changed. * `recommendation`–an issue where the code behaves correctly, but it could be improved. +* `@security-severity`-defines the level of severity, between 0.0 and 10.0, for queries with `@tags security`. For more information about calculating `@security-severity`, see the [GitHub changelog](https://github.blog/changelog/2021-07-19-codeql-code-scanning-new-severity-levels-for-security-alerts/). The values of `@precision` and `@problem.severity` assigned to a query that is part of the standard set determine how the results are displayed by LGTM. See [About alerts](https://help.semmle.com/lgtm-enterprise/user/help/about-alerts.html) and [Alert interest](https://lgtm.com/help/lgtm/alert-interest) for further information. For information about using custom queries in LGTM on a 'per-project' basis, see [Writing custom queries to include in LGTM analysis](https://lgtm.com/help/lgtm/writing-custom-queries) and [About adding custom queries](https://help.semmle.com/lgtm-enterprise/admin/help/about-adding-custom-queries.html). @@ -165,6 +166,8 @@ When you tag a query like this, the associated CWE pages from [MITRE.org](https: Code Scanning may use tags to identify queries with specific meanings across languages. Currently, there is only one such tag: `lines-of-code`. The sum of the results for queries with this tag that return a single number column ([example for JavaScript](https://github.com/github/codeql/blob/c47d680d65f09a851e41d4edad58ffa7486b5431/java/ql/src/Metrics/Summaries/LinesOfCode.ql)) is interpreted by Code Scanning as the lines of code under the source root present in the database. Each language should have exactly one query of this form. +Maintainers are expected to add a `@security-severity` tag to security relevant queries that will be run on Code Scanning. There is a documented internal process for generating these `@security-severity` values. + ## QL area ### Alert messages diff --git a/java/change-notes/2021-05-17-jackson-deserialization-sink.md b/java/change-notes/2021-05-17-jackson-deserialization-sink.md new file mode 100644 index 00000000000..6db65d9dba4 --- /dev/null +++ b/java/change-notes/2021-05-17-jackson-deserialization-sink.md @@ -0,0 +1,3 @@ +lgtm,codescanning +* The "Deserialization of user-controlled data" (`java/unsafe-deserialization`) query + now recognizes `Jackson` deserialization. diff --git a/java/change-notes/2021-06-02-mvel-injection-query.md b/java/change-notes/2021-06-02-mvel-injection-query.md new file mode 100644 index 00000000000..53b4634e9fe --- /dev/null +++ b/java/change-notes/2021-06-02-mvel-injection-query.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* The query "Expression language injection (MVEL) (`java/mvel-expression-injection`) has been promoted from experimental to the main query pack. Its results will now appear by default. This query was originally [submitted as an experimental query by @artem-smotrakov](https://github.com/github/codeql/pull/3329) \ No newline at end of file diff --git a/java/change-notes/2021-07-22-model-collection-constructors.md b/java/change-notes/2021-07-22-model-collection-constructors.md new file mode 100644 index 00000000000..4595a7ba9de --- /dev/null +++ b/java/change-notes/2021-07-22-model-collection-constructors.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* Added additional taint steps modeling constructors for collections in `java.util`. diff --git a/java/documentation/library-coverage/coverage.csv b/java/documentation/library-coverage/coverage.csv index dfcc97bd19c..2e7b87c3f32 100644 --- a/java/documentation/library-coverage/coverage.csv +++ b/java/documentation/library-coverage/coverage.csv @@ -9,6 +9,7 @@ com.fasterxml.jackson.databind,,,3,,,,,,,,,,,,,,,3, com.google.common.base,,,85,,,,,,,,,,,,,,,62,23 com.google.common.io,6,,73,,,,,,,,,,6,,,,,72,1 com.unboundid.ldap.sdk,17,,,,,,,,17,,,,,,,,,, +jakarta.json,,,123,,,,,,,,,,,,,,,100,23 jakarta.ws.rs.client,1,,,,,,,,,1,,,,,,,,, jakarta.ws.rs.core,2,,143,,,,,,,,,,,2,,,,88,55 java.beans,,,1,,,,,,,,,,,,,,,1, @@ -17,7 +18,8 @@ java.lang,,,3,,,,,,,,,,,,,,,1,2 java.net,10,3,6,,,,,,,10,,,,,,,3,6, java.nio,10,,2,,10,,,,,,,,,,,,,2, java.sql,7,,,,,,,,,,,7,,,,,,, -java.util,,,295,,,,,,,,,,,,,,,15,280 +java.util,,,332,,,,,,,,,,,,,,,15,317 +javax.json,,,123,,,,,,,,,,,,,,,100,23 javax.naming.directory,1,,,,,,,,1,,,,,,,,,, javax.net.ssl,2,,,,,,,,,,2,,,,,,,, javax.servlet,4,21,2,,,3,1,,,,,,,,,,21,2, @@ -47,7 +49,7 @@ org.hibernate,7,,,,,,,,,,,7,,,,,,, org.jooq,1,,,,,,,,,,,1,,,,,,, org.springframework.beans,,,26,,,,,,,,,,,,,,,,26 org.springframework.cache,,,13,,,,,,,,,,,,,,,,13 -org.springframework.http,14,,,,,,,,,14,,,,,,,,, +org.springframework.http,14,,70,,,,,,,14,,,,,,,,60,10 org.springframework.jdbc.core,10,,,,,,,,,,,10,,,,,,, org.springframework.jdbc.object,9,,,,,,,,,,,9,,,,,,, org.springframework.ldap.core,14,,,,,,,,14,,,,,,,,,, diff --git a/java/documentation/library-coverage/coverage.rst b/java/documentation/library-coverage/coverage.rst index 85ed28ad330..98d289fcffd 100644 --- a/java/documentation/library-coverage/coverage.rst +++ b/java/documentation/library-coverage/coverage.rst @@ -14,9 +14,9 @@ Java framework & library support `Apache Commons Text `_,``org.apache.commons.text``,,272,,,,,,,, `Apache HttpComponents `_,"``org.apache.hc.core5.*``, ``org.apache.http``",5,136,28,,,3,,,,25 `Google Guava `_,``com.google.common.*``,,158,6,,6,,,,, - Java Standard Library,``java.*``,3,327,30,13,,,7,,,10 - Java extensions,"``javax.*``, ``jakarta.*``",22,294,18,,,,,1,1,2 - `Spring `_,``org.springframework.*``,29,236,62,,,,19,14,,29 + Java Standard Library,``java.*``,3,364,30,13,,,7,,,10 + Java extensions,"``javax.*``, ``jakarta.*``",22,540,18,,,,,1,1,2 + `Spring `_,``org.springframework.*``,29,306,62,,,,19,14,,29 Others,"``com.esotericsoftware.kryo.io``, ``com.esotericsoftware.kryo5.io``, ``com.fasterxml.jackson.databind``, ``com.unboundid.ldap.sdk``, ``org.apache.commons.codec``, ``org.apache.commons.jexl2``, ``org.apache.commons.jexl3``, ``org.apache.directory.ldap.client.api``, ``org.apache.ibatis.jdbc``, ``org.dom4j``, ``org.hibernate``, ``org.jooq``, ``org.xml.sax``, ``org.xmlpull.v1``, ``play.mvc``",7,12,82,,,,14,18,, - Totals,,84,2112,296,13,6,6,107,33,1,66 + Totals,,84,2465,296,13,6,6,107,33,1,66 diff --git a/java/ql/src/Language Abuse/DubiousTypeTestOfThis.ql b/java/ql/src/Language Abuse/DubiousTypeTestOfThis.ql index 2b627149130..0a78c3338ec 100644 --- a/java/ql/src/Language Abuse/DubiousTypeTestOfThis.ql +++ b/java/ql/src/Language Abuse/DubiousTypeTestOfThis.ql @@ -17,7 +17,7 @@ from InstanceOfExpr ioe, RefType t, RefType ct where ioe.getExpr() instanceof ThisAccess and t = ioe.getExpr().getType() and - ct = ioe.getTypeName().getType() and + ct = ioe.getCheckedType() and ct.getASupertype*() = t select ioe, "Testing whether 'this' is an instance of $@ in $@ introduces a dependency cycle between the two types.", diff --git a/java/ql/src/Language Abuse/UselessTypeTest.ql b/java/ql/src/Language Abuse/UselessTypeTest.ql index fac42d30596..6157500a8a0 100644 --- a/java/ql/src/Language Abuse/UselessTypeTest.ql +++ b/java/ql/src/Language Abuse/UselessTypeTest.ql @@ -15,7 +15,7 @@ import java from InstanceOfExpr ioe, RefType t, RefType ct where t = ioe.getExpr().getType() and - ct = ioe.getTypeName().getType() and + ct = ioe.getCheckedType() and ct = t.getASupertype+() select ioe, "There is no need to test whether an instance of $@ is also an instance of $@ - it always is.", t, diff --git a/java/ql/src/Likely Bugs/Comparison/EqualsUsesInstanceOf.ql b/java/ql/src/Likely Bugs/Comparison/EqualsUsesInstanceOf.ql index 18a734a1c23..b1b23038262 100644 --- a/java/ql/src/Likely Bugs/Comparison/EqualsUsesInstanceOf.ql +++ b/java/ql/src/Likely Bugs/Comparison/EqualsUsesInstanceOf.ql @@ -17,8 +17,8 @@ predicate instanceofInEquals(EqualsMethod m, InstanceOfExpr e) { m.fromSource() and e.getEnclosingCallable() = m and e.getExpr().(VarAccess).getVariable() = m.getParameter() and - exists(Class instanceofType | - instanceofType = e.getTypeName().getType() and + exists(RefType instanceofType | + instanceofType = e.getCheckedType() and not instanceofType.isFinal() ) } diff --git a/java/ql/src/Likely Bugs/Concurrency/LazyInitStaticField.ql b/java/ql/src/Likely Bugs/Concurrency/LazyInitStaticField.ql index e6d755eb48c..e6d0b955388 100644 --- a/java/ql/src/Likely Bugs/Concurrency/LazyInitStaticField.ql +++ b/java/ql/src/Likely Bugs/Concurrency/LazyInitStaticField.ql @@ -51,7 +51,7 @@ class LockObjectField extends Field { class ValidSynchStmt extends Stmt { ValidSynchStmt() { // It's OK to lock the enclosing class. - this.(SynchronizedStmt).getExpr().(TypeLiteral).getTypeName().getType() = + this.(SynchronizedStmt).getExpr().(TypeLiteral).getReferencedType() = this.getEnclosingCallable().getDeclaringType() or // It's OK to lock on a "lock object field". diff --git a/java/ql/src/Likely Bugs/Concurrency/SynchSetUnsynchGet.ql b/java/ql/src/Likely Bugs/Concurrency/SynchSetUnsynchGet.ql index 8f91ae89211..3cee55f4833 100644 --- a/java/ql/src/Likely Bugs/Concurrency/SynchSetUnsynchGet.ql +++ b/java/ql/src/Likely Bugs/Concurrency/SynchSetUnsynchGet.ql @@ -24,7 +24,7 @@ import java predicate isSynchronizedByBlock(Method m) { exists(SynchronizedStmt sync, Expr on | sync = m.getBody().getAChild*() and on = sync.getExpr() | if m.isStatic() - then on.(TypeLiteral).getTypeName().getType() = m.getDeclaringType() + then on.(TypeLiteral).getReferencedType() = m.getDeclaringType() else on.(ThisAccess).getType().(RefType).getSourceDeclaration() = m.getDeclaringType() ) } diff --git a/java/ql/src/Likely Bugs/Likely Typos/ContradictoryTypeChecks.ql b/java/ql/src/Likely Bugs/Likely Typos/ContradictoryTypeChecks.ql index 73cb99be0c3..b7cabb114a7 100644 --- a/java/ql/src/Likely Bugs/Likely Typos/ContradictoryTypeChecks.ql +++ b/java/ql/src/Likely Bugs/Likely Typos/ContradictoryTypeChecks.ql @@ -18,7 +18,7 @@ import semmle.code.java.dataflow.SSA /** `ioe` is of the form `va instanceof t`. */ predicate instanceOfCheck(InstanceOfExpr ioe, VarAccess va, RefType t) { ioe.getExpr() = va and - ioe.getTypeName().getType().(RefType).getSourceDeclaration() = t + ioe.getCheckedType().getSourceDeclaration() = t } /** Expression `e` assumes that `va` could be of type `t`. */ diff --git a/java/ql/src/Performance/InnerClassCouldBeStatic.ql b/java/ql/src/Performance/InnerClassCouldBeStatic.ql index 5dba77761c6..2160916c3ea 100644 --- a/java/ql/src/Performance/InnerClassCouldBeStatic.ql +++ b/java/ql/src/Performance/InnerClassCouldBeStatic.ql @@ -127,7 +127,9 @@ predicate potentiallyStatic(InnerClass c) { forall(InnerClass superOfNested | superOfNested = nested.getASourceSupertype+() | potentiallyStatic(superOfNested) ) - ) + ) and + // JUnit Nested test classes are required to be non-static. + not c.hasAnnotation("org.junit.jupiter.api", "Nested") } /** diff --git a/java/ql/src/Security/CWE/CWE-094/MvelExpressionEvaluation.java b/java/ql/src/Security/CWE/CWE-094/MvelExpressionEvaluation.java new file mode 100644 index 00000000000..b8ec0a4db4d --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-094/MvelExpressionEvaluation.java @@ -0,0 +1,25 @@ +public void evaluate(Socket socket) throws IOException { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(socket.getInputStream()))) { + + String expression = reader.readLine(); + // BAD: the user-provided expression is directly evaluated + MVEL.eval(expression); + } +} + +public void safeEvaluate(Socket socket) throws IOException { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(socket.getInputStream()))) { + + String expression = reader.readLine(); + // GOOD: the user-provided expression is validated before evaluation + validateExpression(expression); + MVEL.eval(expression); + } +} + +private void validateExpression(String expression) { + // Validate that the expression does not contain unexpected code. + // For instance, this can be done with allow-lists or deny-lists of code patterns. +} \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/MvelInjection.qhelp b/java/ql/src/Security/CWE/CWE-094/MvelInjection.qhelp similarity index 55% rename from java/ql/src/experimental/Security/CWE/CWE-094/MvelInjection.qhelp rename to java/ql/src/Security/CWE/CWE-094/MvelInjection.qhelp index d68d298b5f5..ad303285920 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/MvelInjection.qhelp +++ b/java/ql/src/Security/CWE/CWE-094/MvelInjection.qhelp @@ -3,11 +3,11 @@

-MVEL is an expression language based on Java-syntax. -The language offers many features +MVEL is an expression language based on Java-syntax, +which offers many features including invocation of methods available in the JVM. If a MVEL expression is built using attacker-controlled data, -and then evaluated, then it may allow the attacker to run arbitrary code. +and then evaluated, then it may allow attackers to run arbitrary code.

@@ -19,10 +19,12 @@ Including user input in a MVEL expression should be avoided.

-The following example uses untrusted data to build a MVEL expression -and then runs it in the default powerfull context. +In the following sample, the first example uses untrusted data to build a MVEL expression +and then runs it in the default context. In the second example, the untrusted data is +validated with a custom method that checks that the expression does not contain unexpected code +before evaluating it.

- +
@@ -35,4 +37,4 @@ and then runs it in the default powerfull context. Expression Language Injection. - \ No newline at end of file + diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/MvelInjection.ql b/java/ql/src/Security/CWE/CWE-094/MvelInjection.ql similarity index 88% rename from java/ql/src/experimental/Security/CWE/CWE-094/MvelInjection.ql rename to java/ql/src/Security/CWE/CWE-094/MvelInjection.ql index d32c33c343c..9a65bb5e481 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/MvelInjection.ql +++ b/java/ql/src/Security/CWE/CWE-094/MvelInjection.ql @@ -11,9 +11,9 @@ */ import java -import MvelInjectionLib +import semmle.code.java.security.MvelInjectionQuery import DataFlow::PathGraph -from DataFlow::PathNode source, DataFlow::PathNode sink, MvelInjectionConfig conf +from DataFlow::PathNode source, DataFlow::PathNode sink, MvelInjectionFlowConfig conf where conf.hasFlowPath(source, sink) select sink.getNode(), source, sink, "MVEL injection from $@.", source.getNode(), "this user input" diff --git a/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.qhelp b/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.qhelp index e9600f11b93..c580d36e2cc 100644 --- a/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.qhelp +++ b/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.qhelp @@ -14,8 +14,8 @@ may have unforeseen effects, such as the execution of arbitrary code.

There are many different serialization frameworks. This query currently -supports Kryo, XmlDecoder, XStream, SnakeYaml, JYaml, JsonIO, YAMLBeans, HessianBurlap, Castor, Burlap -and Java IO serialization through ObjectInputStream/ObjectOutputStream. +supports Kryo, XmlDecoder, XStream, SnakeYaml, JYaml, JsonIO, YAMLBeans, HessianBurlap, Castor, Burlap, +Jackson and Java IO serialization through ObjectInputStream/ObjectOutputStream.

@@ -91,6 +91,15 @@ Remote code execution in JYaml library: JsonIO deserialization vulnerabilities: JsonIO deserialization. +
  • +Research by Moritz Bechler: +Java Unmarshaller Security - Turning your data into code execution +
  • +
  • +Blog posts by the developer of Jackson libraries: +On Jackson CVEs: Don’t Panic — Here is what you need to know +Jackson 2.10: Safe Default Typing +
  • diff --git a/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.ql b/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.ql index 606468d451d..6bb4cdc3561 100644 --- a/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.ql +++ b/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.ql @@ -12,51 +12,9 @@ */ import java -import semmle.code.java.dataflow.FlowSources -import semmle.code.java.security.UnsafeDeserialization +import semmle.code.java.security.UnsafeDeserializationQuery import DataFlow::PathGraph -class UnsafeDeserializationConfig extends TaintTracking::Configuration { - UnsafeDeserializationConfig() { this = "UnsafeDeserializationConfig" } - - override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } - - override predicate isSink(DataFlow::Node sink) { sink instanceof UnsafeDeserializationSink } - - override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { - exists(ClassInstanceExpr cie | - cie.getArgument(0) = pred.asExpr() and - cie = succ.asExpr() and - ( - cie.getConstructor().getDeclaringType() instanceof JsonIoJsonReader or - cie.getConstructor().getDeclaringType() instanceof YamlBeansReader or - cie.getConstructor().getDeclaringType().getASupertype*() instanceof UnsafeHessianInput or - cie.getConstructor().getDeclaringType() instanceof BurlapInput - ) - ) - or - exists(MethodAccess ma | - ma.getMethod() instanceof BurlapInputInitMethod and - ma.getArgument(0) = pred.asExpr() and - ma.getQualifier() = succ.asExpr() - ) - } - - override predicate isSanitizer(DataFlow::Node node) { - exists(ClassInstanceExpr cie | - cie.getConstructor().getDeclaringType() instanceof JsonIoJsonReader and - cie = node.asExpr() and - exists(SafeJsonIoConfig sji | sji.hasFlowToExpr(cie.getArgument(1))) - ) - or - exists(MethodAccess ma | - ma.getMethod() instanceof JsonIoJsonToJavaMethod and - ma.getArgument(0) = node.asExpr() and - exists(SafeJsonIoConfig sji | sji.hasFlowToExpr(ma.getArgument(1))) - ) - } -} - from DataFlow::PathNode source, DataFlow::PathNode sink, UnsafeDeserializationConfig conf where conf.hasFlowPath(source, sink) select sink.getNode().(UnsafeDeserializationSink).getMethodAccess(), source, sink, diff --git a/java/ql/src/Violations of Best Practice/Implementation Hiding/AbstractToConcreteCollection.ql b/java/ql/src/Violations of Best Practice/Implementation Hiding/AbstractToConcreteCollection.ql index cedecf07e24..1200e1efbff 100644 --- a/java/ql/src/Violations of Best Practice/Implementation Hiding/AbstractToConcreteCollection.ql +++ b/java/ql/src/Violations of Best Practice/Implementation Hiding/AbstractToConcreteCollection.ql @@ -16,9 +16,9 @@ import java import semmle.code.java.Collections predicate guardedByInstanceOf(VarAccess e, RefType t) { - exists(IfStmt s, InstanceOfExpr instanceCheck, Type checkType | + exists(IfStmt s, InstanceOfExpr instanceCheck, RefType checkType | s.getCondition() = instanceCheck and - instanceCheck.getTypeName().getType() = checkType and + instanceCheck.getCheckedType() = checkType and // The same variable appears as the subject of the `instanceof`. instanceCheck.getExpr() = e.getVariable().getAnAccess() and // The checked type is either the type itself, or a raw version. For example, it is usually diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/MvelInjectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-094/MvelInjectionLib.qll deleted file mode 100644 index a6cf891330f..00000000000 --- a/java/ql/src/experimental/Security/CWE/CWE-094/MvelInjectionLib.qll +++ /dev/null @@ -1,367 +0,0 @@ -import java -import semmle.code.java.dataflow.FlowSources -import semmle.code.java.dataflow.TaintTracking - -/** - * A taint-tracking configuration for unsafe user input - * that is used to construct and evaluate a MVEL expression. - */ -class MvelInjectionConfig extends TaintTracking::Configuration { - MvelInjectionConfig() { this = "MvelInjectionConfig" } - - override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } - - override predicate isSink(DataFlow::Node sink) { sink instanceof MvelEvaluationSink } - - override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { - expressionCompilationStep(node1, node2) or - createExpressionCompilerStep(node1, node2) or - expressionCompilerCompileStep(node1, node2) or - createCompiledAccExpressionStep(node1, node2) or - scriptCompileStep(node1, node2) or - createMvelCompiledScriptStep(node1, node2) or - templateCompileStep(node1, node2) or - createTemplateCompilerStep(node1, node2) - } -} - -/** - * A sink for EL injection vulnerabilities via MVEL, - * i.e. methods that run evaluation of a MVEL expression. - */ -class MvelEvaluationSink extends DataFlow::ExprNode { - MvelEvaluationSink() { - exists(StaticMethodAccess ma, Method m | m = ma.getMethod() | - ( - m instanceof MvelEvalMethod or - m instanceof TemplateRuntimeEvaluationMethod - ) and - ma.getArgument(0) = asExpr() - ) - or - exists(MethodAccess ma, Method m | m = ma.getMethod() | - m instanceof MvelScriptEngineEvaluationMethod and - ma.getArgument(0) = asExpr() - ) - or - exists(MethodAccess ma, Method m | m = ma.getMethod() | - ( - m instanceof ExecutableStatementEvaluationMethod or - m instanceof CompiledExpressionEvaluationMethod or - m instanceof CompiledAccExpressionEvaluationMethod or - m instanceof AccessorEvaluationMethod or - m instanceof CompiledScriptEvaluationMethod or - m instanceof MvelCompiledScriptEvaluationMethod - ) and - ma.getQualifier() = asExpr() - ) - or - exists(StaticMethodAccess ma, Method m | m = ma.getMethod() | - m instanceof MvelRuntimeEvaluationMethod and - ma.getArgument(1) = asExpr() - ) - } -} - -/** - * Holds if `node1` to `node2` is a dataflow step that compiles a MVEL expression - * by callilng `MVEL.compileExpression(tainted)`. - */ -predicate expressionCompilationStep(DataFlow::Node node1, DataFlow::Node node2) { - exists(StaticMethodAccess ma, Method m | ma.getMethod() = m | - m.getDeclaringType() instanceof MVEL and - m.hasName("compileExpression") and - ma.getAnArgument() = node1.asExpr() and - node2.asExpr() = ma - ) -} - -/** - * Holds if `node1` to `node2` is a dataflow step creates `ExpressionCompiler`, - * i.e. `new ExpressionCompiler(tainted)`. - */ -predicate createExpressionCompilerStep(DataFlow::Node node1, DataFlow::Node node2) { - exists(ConstructorCall cc | - cc.getConstructedType() instanceof ExpressionCompiler and - cc = node2.asExpr() and - cc.getArgument(0) = node1.asExpr() - ) -} - -/** - * Holds if `node1` to `node2` is a dataflow step creates `CompiledAccExpression`, - * i.e. `new CompiledAccExpression(tainted, ...)`. - */ -predicate createCompiledAccExpressionStep(DataFlow::Node node1, DataFlow::Node node2) { - exists(ConstructorCall cc | - cc.getConstructedType() instanceof CompiledAccExpression and - cc = node2.asExpr() and - cc.getArgument(0) = node1.asExpr() - ) -} - -/** - * Holds if `node1` to `node2` is a dataflow step that compiles a MVEL expression - * by calling `ExpressionCompiler.compile()`. - */ -predicate expressionCompilerCompileStep(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodAccess ma, Method m | ma.getMethod() = m | - m.getDeclaringType() instanceof ExpressionCompiler and - m.hasName("compile") and - ma = node2.asExpr() and - ma.getQualifier() = node1.asExpr() - ) -} - -/** - * Holds if `node1` to `node2` is a dataflow step that compiles a script via `MvelScriptEngine`, - * i.e. `engine.compile(tainted)` or `engine.compiledScript(tainted)`. - */ -predicate scriptCompileStep(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodAccess ma, Method m | ma.getMethod() = m | - m instanceof MvelScriptEngineCompilationMethod and - ma = node2.asExpr() and - ma.getArgument(0) = node1.asExpr() - ) -} - -/** - * Holds if `node1` to `node2` is a dataflow step creates `MvelCompiledScript`, - * i.e. `new MvelCompiledScript(engine, tainted)`. - */ -predicate createMvelCompiledScriptStep(DataFlow::Node node1, DataFlow::Node node2) { - exists(ConstructorCall cc | - cc.getConstructedType() instanceof MvelCompiledScript and - cc = node2.asExpr() and - cc.getArgument(1) = node1.asExpr() - ) -} - -/** - * Holds if `node1` to `node2` is a dataflow step creates `TemplateCompiler`, - * i.e. `new TemplateCompiler(tainted)`. - */ -predicate createTemplateCompilerStep(DataFlow::Node node1, DataFlow::Node node2) { - exists(ConstructorCall cc | - cc.getConstructedType() instanceof TemplateCompiler and - cc = node2.asExpr() and - cc.getArgument(0) = node1.asExpr() - ) -} - -/** - * Holds if `node1` to `node2` is a dataflow step that compiles a script via `TemplateCompiler`, - * i.e. `compiler.compile()` or `TemplateCompiler.compileTemplate(tainted)`. - */ -predicate templateCompileStep(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodAccess ma, Method m | ma.getMethod() = m | - m instanceof TemplateCompilerCompileMethod and - ma.getQualifier() = node1.asExpr() and - ma = node2.asExpr() - ) - or - exists(StaticMethodAccess ma, Method m | ma.getMethod() = m | - m instanceof TemplateCompilerCompileTemplateMethod and - ma = node2.asExpr() and - ma.getArgument(0) = node1.asExpr() - ) -} - -/** - * Methods in the MVEL class that evaluate a MVEL expression. - */ -class MvelEvalMethod extends Method { - MvelEvalMethod() { - getDeclaringType() instanceof MVEL and - ( - hasName("eval") or - hasName("executeExpression") or - hasName("evalToBoolean") or - hasName("evalToString") or - hasName("executeAllExpression") or - hasName("executeSetExpression") - ) - } -} - -/** - * Methods in `MVEL` class that compile a MVEL expression. - */ -class MvelCompileExpressionMethod extends Method { - MvelCompileExpressionMethod() { - getDeclaringType() instanceof MVEL and - ( - hasName("compileExpression") or - hasName("compileGetExpression") or - hasName("compileSetExpression") - ) - } -} - -/** - * Methods in `ExecutableStatement` that evaluate a MVEL expression. - */ -class ExecutableStatementEvaluationMethod extends Method { - ExecutableStatementEvaluationMethod() { - getDeclaringType() instanceof ExecutableStatement and - hasName("getValue") - } -} - -/** - * Methods in `CompiledExpression` that evaluate a MVEL expression. - */ -class CompiledExpressionEvaluationMethod extends Method { - CompiledExpressionEvaluationMethod() { - getDeclaringType() instanceof CompiledExpression and - hasName("getDirectValue") - } -} - -/** - * Methods in `CompiledAccExpression` that evaluate a MVEL expression. - */ -class CompiledAccExpressionEvaluationMethod extends Method { - CompiledAccExpressionEvaluationMethod() { - getDeclaringType() instanceof CompiledAccExpression and - hasName("getValue") - } -} - -/** - * Methods in `Accessor` that evaluate a MVEL expression. - */ -class AccessorEvaluationMethod extends Method { - AccessorEvaluationMethod() { - getDeclaringType() instanceof Accessor and - hasName("getValue") - } -} - -/** - * Methods in `MvelScriptEngine` that evaluate a MVEL expression. - */ -class MvelScriptEngineEvaluationMethod extends Method { - MvelScriptEngineEvaluationMethod() { - getDeclaringType() instanceof MvelScriptEngine and - (hasName("eval") or hasName("evaluate")) - } -} - -/** - * Methods in `MvelScriptEngine` that compile a MVEL expression. - */ -class MvelScriptEngineCompilationMethod extends Method { - MvelScriptEngineCompilationMethod() { - getDeclaringType() instanceof MvelScriptEngine and - (hasName("compile") or hasName("compiledScript")) - } -} - -/** - * Methods in `CompiledScript` that evaluate a MVEL expression. - */ -class CompiledScriptEvaluationMethod extends Method { - CompiledScriptEvaluationMethod() { - getDeclaringType() instanceof CompiledScript and - hasName("eval") - } -} - -/** - * Methods in `TemplateRuntime` that evaluate a MVEL template. - */ -class TemplateRuntimeEvaluationMethod extends Method { - TemplateRuntimeEvaluationMethod() { - getDeclaringType() instanceof TemplateRuntime and - (hasName("eval") or hasName("execute")) - } -} - -/** - * `TemplateCompiler.compile()` method compiles a MVEL template. - */ -class TemplateCompilerCompileMethod extends Method { - TemplateCompilerCompileMethod() { - getDeclaringType() instanceof TemplateCompiler and - hasName("compile") - } -} - -/** - * `TemplateCompiler.compileTemplate(tainted)` static method compiles a MVEL template. - */ -class TemplateCompilerCompileTemplateMethod extends Method { - TemplateCompilerCompileTemplateMethod() { - getDeclaringType() instanceof TemplateCompiler and - hasName("compileTemplate") - } -} - -/** - * Methods in `MvelCompiledScript` that evaluate a MVEL expression. - */ -class MvelCompiledScriptEvaluationMethod extends Method { - MvelCompiledScriptEvaluationMethod() { - getDeclaringType() instanceof MvelCompiledScript and - hasName("eval") - } -} - -/** - * Methods in `MVELRuntime` that evaluate a MVEL expression. - */ -class MvelRuntimeEvaluationMethod extends Method { - MvelRuntimeEvaluationMethod() { - getDeclaringType() instanceof MVELRuntime and - hasName("execute") - } -} - -class MVEL extends RefType { - MVEL() { hasQualifiedName("org.mvel2", "MVEL") } -} - -class ExpressionCompiler extends RefType { - ExpressionCompiler() { hasQualifiedName("org.mvel2.compiler", "ExpressionCompiler") } -} - -class ExecutableStatement extends RefType { - ExecutableStatement() { hasQualifiedName("org.mvel2.compiler", "ExecutableStatement") } -} - -class CompiledExpression extends RefType { - CompiledExpression() { hasQualifiedName("org.mvel2.compiler", "CompiledExpression") } -} - -class CompiledAccExpression extends RefType { - CompiledAccExpression() { hasQualifiedName("org.mvel2.compiler", "CompiledAccExpression") } -} - -class Accessor extends RefType { - Accessor() { hasQualifiedName("org.mvel2.compiler", "Accessor") } -} - -class CompiledScript extends RefType { - CompiledScript() { hasQualifiedName("javax.script", "CompiledScript") } -} - -class MvelScriptEngine extends RefType { - MvelScriptEngine() { hasQualifiedName("org.mvel2.jsr223", "MvelScriptEngine") } -} - -class MvelCompiledScript extends RefType { - MvelCompiledScript() { hasQualifiedName("org.mvel2.jsr223", "MvelCompiledScript") } -} - -class TemplateRuntime extends RefType { - TemplateRuntime() { hasQualifiedName("org.mvel2.templates", "TemplateRuntime") } -} - -class TemplateCompiler extends RefType { - TemplateCompiler() { hasQualifiedName("org.mvel2.templates", "TemplateCompiler") } -} - -class MVELRuntime extends RefType { - MVELRuntime() { hasQualifiedName("org.mvel2", "MVELRuntime") } -} diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/UnsafeMvelExpressionEvaluation.java b/java/ql/src/experimental/Security/CWE/CWE-094/UnsafeMvelExpressionEvaluation.java deleted file mode 100644 index 4942bee79f6..00000000000 --- a/java/ql/src/experimental/Security/CWE/CWE-094/UnsafeMvelExpressionEvaluation.java +++ /dev/null @@ -1,8 +0,0 @@ -public void evaluate(Socket socket) throws IOException { - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(socket.getInputStream()))) { - - String expression = reader.readLine(); - MVEL.eval(expression); - } -} \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-295/JxBrowserWithoutCertValidation.qhelp b/java/ql/src/experimental/Security/CWE/CWE-295/JxBrowserWithoutCertValidation.qhelp index 7327573b8ba..31e42761140 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-295/JxBrowserWithoutCertValidation.qhelp +++ b/java/ql/src/experimental/Security/CWE/CWE-295/JxBrowserWithoutCertValidation.qhelp @@ -26,7 +26,7 @@ all certificate errors are ignored. In the 'GOOD' case, certificate errors are r
  • Teamdev: - + Changelog of JxBrowser 6.24.
  • diff --git a/java/ql/src/experimental/Security/CWE/CWE-470/UnsafeReflection.java b/java/ql/src/experimental/Security/CWE/CWE-470/UnsafeReflection.java new file mode 100644 index 00000000000..a6129ff8615 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-470/UnsafeReflection.java @@ -0,0 +1,103 @@ +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.multipart.MultipartFile; + +@Controller +public class UnsafeReflection { + + @RequestMapping(value = {"/service/{beanIdOrClassName}/{methodName}"}, method = {RequestMethod.POST}, consumes = {"application/json"}, produces = {"application/json"}) + public Object bad1(@PathVariable("beanIdOrClassName") String beanIdOrClassName, @PathVariable("methodName") String methodName, @RequestBody Map body) throws Exception { + List rawData = null; + try { + rawData = (List)body.get("methodInput"); + } catch (Exception e) { + return e; + } + return invokeService(beanIdOrClassName, methodName, null, rawData); + } + + @GetMapping(value = "uf1") + public void good1(HttpServletRequest request) throws Exception { + HashSet hashSet = new HashSet<>(); + hashSet.add("com.example.test1"); + hashSet.add("com.example.test2"); + String className = request.getParameter("className"); + String parameterValue = request.getParameter("parameterValue"); + if (!hashSet.contains(className)){ + throw new Exception("Class not valid: " + className); + } + try { + Class clazz = Class.forName(className); + Object object = clazz.getDeclaredConstructors()[0].newInstance(parameterValue); //good + } catch (Exception e) { + e.printStackTrace(); + } + } + + @GetMapping(value = "uf2") + public void good2(HttpServletRequest request) throws Exception { + String className = request.getParameter("className"); + String parameterValue = request.getParameter("parameterValue"); + if (!"com.example.test1".equals(className)){ + throw new Exception("Class not valid: " + className); + } + try { + Class clazz = Class.forName(className); + Object object = clazz.getDeclaredConstructors()[0].newInstance(parameterValue); //good + } catch (Exception e) { + e.printStackTrace(); + } + } + + private Object invokeService(String beanIdOrClassName, String methodName, MultipartFile[] files, List data) throws Exception { + BeanFactory beanFactory = new BeanFactory(); + try { + Object bean = null; + Class beanClass = Class.forName(beanIdOrClassName); + bean = beanFactory.getBean(beanClass); + byte b; + int i; + Method[] arrayOfMethod; + for (i = (arrayOfMethod = bean.getClass().getMethods()).length, b = 0; b < i; ) { + Method method = arrayOfMethod[b]; + if (!method.getName().equals(methodName)) { + b++; + continue; + } + Object result = method.invoke(bean, data); + Map map = new HashMap<>(); + return map; + } + } catch (Exception e) { + return e; + } + return null; + } +} + +class BeanFactory { + + private static HashMap classNameMap = new HashMap<>(); + + private static HashMap, Object> classMap = new HashMap<>(); + + static { + classNameMap.put("xxxx", Runtime.getRuntime()); + classMap.put(Runtime.class, Runtime.getRuntime()); + } + + public Object getBean(Class clzz) { + return classMap.get(clzz); + } +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-470/UnsafeReflection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-470/UnsafeReflection.qhelp new file mode 100644 index 00000000000..b4c66b9ef00 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-470/UnsafeReflection.qhelp @@ -0,0 +1,38 @@ + + + + +

    +Allowing users to freely choose the name of a class to instantiate could provide means to attack a vulnerable appplication. +

    +
    + + +

    +Create a list of classes that are allowed to load reflectively and strictly verify the input to ensure that +users can only instantiate classes or execute methods that ought to be allowed. +

    +
    + + +

    +The bad method shown below illustrate class loading with Class.forName without any check on the particular class being instantiated. +The good methods illustrate some different ways to restrict which classes can be instantiated. +

    + + +
    + + + +
  • +Unsafe use of Reflection | OWASP: +Unsafe use of Reflection. +
  • +
  • +Java owasp: Classes should not be loaded dynamically: +Classes should not be loaded dynamically. +
  • +
    + +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-470/UnsafeReflection.ql b/java/ql/src/experimental/Security/CWE/CWE-470/UnsafeReflection.ql new file mode 100644 index 00000000000..ca29a5544d3 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-470/UnsafeReflection.ql @@ -0,0 +1,99 @@ +/** + * @name Use of externally-controlled input to select classes or code ('unsafe reflection') + * @description Use external input with reflection function to select the class or code to + * be used, which brings serious security risks. + * @kind path-problem + * @problem.severity error + * @precision high + * @id java/unsafe-reflection + * @tags security + * external/cwe/cwe-470 + */ + +import java +import DataFlow +import UnsafeReflectionLib +import semmle.code.java.dataflow.DataFlow +import semmle.code.java.dataflow.FlowSources +import DataFlow::PathGraph + +private class ContainsSanitizer extends DataFlow::BarrierGuard { + ContainsSanitizer() { this.(MethodAccess).getMethod().hasName("contains") } + + override predicate checks(Expr e, boolean branch) { + e = this.(MethodAccess).getArgument(0) and branch = true + } +} + +private class EqualsSanitizer extends DataFlow::BarrierGuard { + EqualsSanitizer() { this.(MethodAccess).getMethod().hasName("equals") } + + override predicate checks(Expr e, boolean branch) { + e = [this.(MethodAccess).getArgument(0), this.(MethodAccess).getQualifier()] and + branch = true + } +} + +class UnsafeReflectionConfig extends TaintTracking::Configuration { + UnsafeReflectionConfig() { this = "UnsafeReflectionConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof UnsafeReflectionSink } + + override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { + // Argument -> return of Class.forName, ClassLoader.loadClass + exists(ReflectiveClassIdentifierMethodAccess rcimac | + rcimac.getArgument(0) = pred.asExpr() and rcimac = succ.asExpr() + ) + or + // Qualifier -> return of Class.getDeclaredConstructors/Methods and similar + exists(MethodAccess ma | + ( + ma instanceof ReflectiveConstructorsAccess or + ma instanceof ReflectiveMethodsAccess + ) and + ma.getQualifier() = pred.asExpr() and + ma = succ.asExpr() + ) + or + // Qualifier -> return of Object.getClass + exists(MethodAccess ma | + ma.getMethod().hasName("getClass") and + ma.getMethod().getDeclaringType().hasQualifiedName("java.lang", "Object") and + ma.getQualifier() = pred.asExpr() and + ma = succ.asExpr() + ) + or + // Argument -> return of methods that look like Class.forName + looksLikeResolveClassStep(pred, succ) + or + // Argument -> return of methods that look like `Object getInstance(Class c)` + looksLikeInstantiateClassStep(pred, succ) + or + // Qualifier -> return of Constructor.newInstance, Class.newInstance + exists(NewInstance ni | + ni.getQualifier() = pred.asExpr() and + ni = succ.asExpr() + ) + } + + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { + guard instanceof ContainsSanitizer or guard instanceof EqualsSanitizer + } +} + +private Expr getAMethodArgument(MethodAccess reflectiveCall) { + result = reflectiveCall.(NewInstance).getAnArgument() + or + result = reflectiveCall.(MethodInvokeCall).getAnArgument() +} + +from + DataFlow::PathNode source, DataFlow::PathNode sink, UnsafeReflectionConfig conf, + MethodAccess reflectiveCall +where + conf.hasFlowPath(source, sink) and + sink.getNode().asExpr() = reflectiveCall.getQualifier() and + conf.hasFlowToExpr(getAMethodArgument(reflectiveCall)) +select sink.getNode(), source, sink, "Unsafe reflection of $@.", source.getNode(), "user input" diff --git a/java/ql/src/experimental/Security/CWE/CWE-470/UnsafeReflectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-470/UnsafeReflectionLib.qll new file mode 100644 index 00000000000..803fe6107a1 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-470/UnsafeReflectionLib.qll @@ -0,0 +1,60 @@ +import java +import DataFlow +import semmle.code.java.Reflection +import semmle.code.java.dataflow.FlowSources + +/** + * A call to `java.lang.reflect.Method.invoke`. + */ +class MethodInvokeCall extends MethodAccess { + MethodInvokeCall() { this.getMethod().hasQualifiedName("java.lang.reflect", "Method", "invoke") } +} + +/** + * Unsafe reflection sink (the qualifier or method arguments to `Constructor.newInstance(...)` or `Method.invoke(...)`) + */ +class UnsafeReflectionSink extends DataFlow::ExprNode { + UnsafeReflectionSink() { + exists(MethodAccess ma | + ( + ma.getMethod().hasQualifiedName("java.lang.reflect", "Constructor<>", "newInstance") or + ma instanceof MethodInvokeCall + ) and + this.asExpr() = [ma.getQualifier(), ma.getAnArgument()] + ) + } +} + +/** + * Holds if `fromNode` to `toNode` is a dataflow step that looks like resolving a class. + * A method probably resolves a class if it takes a string, returns a Class + * and its name contains "resolve", "load", etc. + */ +predicate looksLikeResolveClassStep(DataFlow::Node fromNode, DataFlow::Node toNode) { + exists(MethodAccess ma, Method m, int i, Expr arg | + m = ma.getMethod() and arg = ma.getArgument(i) + | + m.getReturnType() instanceof TypeClass and + m.getName().toLowerCase().regexpMatch("resolve|load|class|type") and + arg.getType() instanceof TypeString and + arg = fromNode.asExpr() and + ma = toNode.asExpr() + ) +} + +/** + * Holds if `fromNode` to `toNode` is a dataflow step that looks like instantiating a class. + * A method probably instantiates a class if it is external, takes a Class, returns an Object + * and its name contains "instantiate" or similar terms. + */ +predicate looksLikeInstantiateClassStep(DataFlow::Node fromNode, DataFlow::Node toNode) { + exists(MethodAccess ma, Method m, int i, Expr arg | + m = ma.getMethod() and arg = ma.getArgument(i) + | + m.getReturnType() instanceof TypeObject and + m.getName().toLowerCase().regexpMatch("instantiate|instance|create|make|getbean") and + arg.getType() instanceof TypeClass and + arg = fromNode.asExpr() and + ma = toNode.asExpr() + ) +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.java b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.java index eba64aab6a8..cf13f9b51fa 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.java +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.java @@ -34,6 +34,29 @@ public class SpringUrlRedirect { } @GetMapping("url5") + public ResponseEntity bad5(String redirectUrl) { + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(redirectUrl)) + .build(); + } + + @GetMapping("url6") + public ResponseEntity bad6(String redirectUrl) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setLocation(URI.create(redirectUrl)); + + return new ResponseEntity<>(httpHeaders, HttpStatus.SEE_OTHER); + } + + @GetMapping("url7") + public ResponseEntity bad7(String redirectUrl) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add("Location", redirectUrl); + + return ResponseEntity.status(HttpStatus.SEE_OTHER).headers(httpHeaders).build(); + } + + @GetMapping("url8") public RedirectView good1(String redirectUrl) { RedirectView rv = new RedirectView(); if (redirectUrl.startsWith(VALID_REDIRECT)){ diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qhelp b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qhelp index 6fe70dfb113..ba475b27917 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qhelp +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qhelp @@ -21,10 +21,10 @@ redirects on the server; then choose from that list based on the user input prov

    The following examples show the bad case and the good case respectively. -In bad1 method and bad2 method and bad3 method and -bad4 method, shows an HTTP request parameter being used directly in a URL redirect -without validating the input, which facilitates phishing attacks. In good1 method, -shows how to solve this problem by verifying whether the user input is a known fixed string beginning. +The bad methods show an HTTP request parameter being used directly +in a URL redirect without validating the input, which facilitates phishing attacks. +In the good1 method, it is shown how to solve this problem by verifying whether +the user input is a known fixed string beginning.

    @@ -33,5 +33,6 @@ shows how to solve this problem by verifying whether the user input is a known f
  • A Guide To Spring Redirects: Spring Redirects.
  • Url redirection - attack and defense: Url Redirection.
  • +
  • How to redirect to an external URL from Spring Boot REST Controller (Post/Redirect/Get pattern)?: ResponseEntity Redirection.
  • diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql index b02bd3e4c30..849d317a9af 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql @@ -34,6 +34,10 @@ class SpringUrlRedirectFlowConfig extends TaintTracking::Configuration { override predicate isSink(DataFlow::Node sink) { sink instanceof SpringUrlRedirectSink } + override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) { + springUrlRedirectTaintStep(fromNode, toNode) + } + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { guard instanceof StartsWithSanitizer } @@ -57,6 +61,8 @@ class SpringUrlRedirectFlowConfig extends TaintTracking::Configuration { not ma.getArgument(0).(CompileTimeConstantExpr).getStringValue().regexpMatch("^%s.*") ) ) + or + nonLocationHeaderSanitizer(node) } } diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll index 4a86640d4d4..3791573eb9d 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll @@ -35,8 +35,13 @@ class RedirectAppendCall extends MethodAccess { } /** A URL redirection sink from spring controller method. */ -class SpringUrlRedirectSink extends DataFlow::Node { - SpringUrlRedirectSink() { +abstract class SpringUrlRedirectSink extends DataFlow::Node { } + +/** + * A sink for URL Redirection via the Spring View classes. + */ +private class SpringViewUrlRedirectSink extends SpringUrlRedirectSink { + SpringViewUrlRedirectSink() { exists(RedirectBuilderExpr rbe | rbe.getRightOperand() = this.asExpr() and any(SpringRequestMappingMethod sqmm).polyCalls*(this.getEnclosingCallable()) @@ -71,3 +76,64 @@ class SpringUrlRedirectSink extends DataFlow::Node { ) } } + +/** + * A sink for URL Redirection via the `ResponseEntity` class. + */ +private class SpringResponseEntityUrlRedirectSink extends SpringUrlRedirectSink { + SpringResponseEntityUrlRedirectSink() { + // Find `new ResponseEntity(httpHeaders, ...)` or + // `new ResponseEntity(..., httpHeaders, ...)` sinks + exists(ClassInstanceExpr cie, Argument argument | + cie.getConstructedType() instanceof SpringResponseEntity and + argument.getType() instanceof SpringHttpHeaders and + argument = cie.getArgument([0, 1]) and + this.asExpr() = argument + ) + or + // Find `ResponseEntity.status(...).headers(taintHeaders).build()` or + // `ResponseEntity.status(...).location(URI.create(taintURL)).build()` sinks + exists(MethodAccess ma | + ma.getMethod() + .getDeclaringType() + .hasQualifiedName("org.springframework.http", "ResponseEntity$HeadersBuilder") and + ma.getMethod().getName() in ["headers", "location"] and + this.asExpr() = ma.getArgument(0) + ) + } +} + +private class HttpHeadersMethodAccess extends MethodAccess { + HttpHeadersMethodAccess() { this.getMethod().getDeclaringType() instanceof SpringHttpHeaders } +} + +private class HttpHeadersAddSetMethodAccess extends HttpHeadersMethodAccess { + HttpHeadersAddSetMethodAccess() { this.getMethod().getName() in ["add", "set"] } +} + +private class HttpHeadersSetLocationMethodAccess extends HttpHeadersMethodAccess { + HttpHeadersSetLocationMethodAccess() { this.getMethod().hasName("setLocation") } +} + +/** + * Holds if `fromNode` to `toNode` is a dataflow step from a tainted argument to + * a `HttpHeaders` instance qualifier, i.e. `httpHeaders.setLocation(tainted)`. + */ +predicate springUrlRedirectTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) { + exists(HttpHeadersSetLocationMethodAccess ma | + fromNode.asExpr() = ma.getArgument(0) and + toNode.asExpr() = ma.getQualifier() + ) +} + +/** + * A sanitizer to exclude the cases where the `HttpHeaders.add` or `HttpHeaders.set` + * methods are called with a HTTP header other than "Location". + * E.g: `httpHeaders.add("X-Some-Header", taintedUrlString)` + */ +predicate nonLocationHeaderSanitizer(DataFlow::Node node) { + exists(HttpHeadersAddSetMethodAccess ma, Argument firstArg | node.asExpr() = ma.getArgument(1) | + firstArg = ma.getArgument(0) and + not firstArg.(CompileTimeConstantExpr).getStringValue().matches("Location") + ) +} diff --git a/java/ql/src/semmle/code/java/Collections.qll b/java/ql/src/semmle/code/java/Collections.qll index 9569dfc4c2c..d557d6281de 100644 --- a/java/ql/src/semmle/code/java/Collections.qll +++ b/java/ql/src/semmle/code/java/Collections.qll @@ -35,6 +35,7 @@ predicate instantiates(RefType t, GenericType g, int i, RefType arg) { * - a class `MyIntMap extends HashMap` instantiates `Map` (among others) * with the `0`-th type parameter being `Integer` and the `1`-th type parameter being `V`. */ +pragma[nomagic] predicate indirectlyInstantiates(RefType t, GenericType g, int i, RefType arg) { instantiates(t, g, i, arg) or diff --git a/java/ql/src/semmle/code/java/Dependency.qll b/java/ql/src/semmle/code/java/Dependency.qll index c9bc8cc44d4..a2bd8a84fdc 100755 --- a/java/ql/src/semmle/code/java/Dependency.qll +++ b/java/ql/src/semmle/code/java/Dependency.qll @@ -62,7 +62,7 @@ predicate depends(RefType t, RefType dep) { or // the type of a type literal accessed in `t`, exists(TypeLiteral l | l.getEnclosingCallable().getDeclaringType() = t | - usesType(l.getTypeName().getType(), dep) + usesType(l.getReferencedType(), dep) ) or // the type of an annotation (or one of its element values) that annotates `t` or one of its members, @@ -76,7 +76,7 @@ predicate depends(RefType t, RefType dep) { or // the type accessed in an `instanceof` expression in `t`. exists(InstanceOfExpr ioe | t = ioe.getEnclosingCallable().getDeclaringType() | - usesType(ioe.getTypeName().getType(), dep) + usesType(ioe.getCheckedType(), dep) ) ) } diff --git a/java/ql/src/semmle/code/java/DependencyCounts.qll b/java/ql/src/semmle/code/java/DependencyCounts.qll index 0794ceeabfc..b775e5fcf06 100644 --- a/java/ql/src/semmle/code/java/DependencyCounts.qll +++ b/java/ql/src/semmle/code/java/DependencyCounts.qll @@ -80,7 +80,7 @@ predicate numDepends(RefType t, RefType dep, int value) { elem = l and l.getEnclosingCallable().getDeclaringType() = t | - usesType(l.getTypeName().getType(), dep) + usesType(l.getReferencedType(), dep) ) or // the type of an annotation (or one of its element values) that annotates `t` or one of its members, @@ -100,7 +100,7 @@ predicate numDepends(RefType t, RefType dep, int value) { elem = ioe and t = ioe.getEnclosingCallable().getDeclaringType() | - usesType(ioe.getTypeName().getType(), dep) + usesType(ioe.getCheckedType(), dep) ) ) } diff --git a/java/ql/src/semmle/code/java/Expr.qll b/java/ql/src/semmle/code/java/Expr.qll index 88038f2b4d1..4c6ea652f2a 100755 --- a/java/ql/src/semmle/code/java/Expr.qll +++ b/java/ql/src/semmle/code/java/Expr.qll @@ -46,17 +46,6 @@ class Expr extends ExprParent, @expr { */ int getKind() { exprs(this, result, _, _, _) } - /** - * DEPRECATED: This is no longer necessary. See `Expr.isParenthesized()`. - * - * Gets this expression with any surrounding parentheses removed. - */ - deprecated Expr getProperExpr() { - result = this.(ParExpr).getExpr().getProperExpr() - or - result = this and not this instanceof ParExpr - } - /** Gets the statement containing this expression, if any. */ Stmt getEnclosingStmt() { statementEnclosingExpr(this, result) } @@ -1318,19 +1307,6 @@ class SwitchExpr extends Expr, StmtParent, @switchexpr { override string getAPrimaryQlClass() { result = "SwitchExpr" } } -/** - * DEPRECATED: Use `Expr.isParenthesized()` instead. - * - * A parenthesised expression. - */ -deprecated class ParExpr extends Expr, @parexpr { - /** Gets the expression inside the parentheses. */ - deprecated Expr getExpr() { result.getParent() = this } - - /** Gets a printable representation of this expression. */ - override string toString() { result = "(...)" } -} - /** An `instanceof` expression. */ class InstanceOfExpr extends Expr, @instanceofexpr { /** Gets the expression on the left-hand side of the `instanceof` operator. */ @@ -1357,6 +1333,9 @@ class InstanceOfExpr extends Expr, @instanceofexpr { /** Gets the access to the type on the right-hand side of the `instanceof` operator. */ Expr getTypeName() { result.isNthChildOf(this, 1) } + /** Gets the type this `instanceof` expression checks for. */ + RefType getCheckedType() { result = getTypeName().getType() } + /** Gets a printable representation of this expression. */ override string toString() { result = "...instanceof..." } @@ -1449,6 +1428,12 @@ class TypeLiteral extends Expr, @typeliteral { /** Gets the access to the type whose class is accessed. */ Expr getTypeName() { result.getParent() = this } + /** + * Gets the type this type literal refers to. For example for `String.class` the + * result is the type representing `String`. + */ + Type getReferencedType() { result = getTypeName().getType() } + /** Gets a printable representation of this expression. */ override string toString() { result = this.getTypeName().toString() + ".class" } diff --git a/java/ql/src/semmle/code/java/Reflection.qll b/java/ql/src/semmle/code/java/Reflection.qll index 5a46fcb8be2..ac6046824f6 100644 --- a/java/ql/src/semmle/code/java/Reflection.qll +++ b/java/ql/src/semmle/code/java/Reflection.qll @@ -47,19 +47,22 @@ private XMLElement elementReferencingType(RefType rt) { } abstract private class ReflectiveClassIdentifier extends Expr { + /** + * Gets the type of a class identified by this expression. + */ abstract RefType getReflectivelyIdentifiedClass(); } private class ReflectiveClassIdentifierLiteral extends ReflectiveClassIdentifier, TypeLiteral { override RefType getReflectivelyIdentifiedClass() { - result = getTypeName().getType().(RefType).getSourceDeclaration() + result = getReferencedType().(RefType).getSourceDeclaration() } } /** * A call to a Java standard library method which constructs or returns a `Class` from a `String`. */ -library class ReflectiveClassIdentifierMethodAccess extends ReflectiveClassIdentifier, MethodAccess { +class ReflectiveClassIdentifierMethodAccess extends ReflectiveClassIdentifier, MethodAccess { ReflectiveClassIdentifierMethodAccess() { // A call to `Class.forName(...)`, from which we can infer `T` in the returned type `Class`. getCallee().getDeclaringType() instanceof TypeClass and getCallee().hasName("forName") @@ -314,6 +317,26 @@ class ClassMethodAccess extends MethodAccess { } } +/** + * A call to `Class.getConstructors(..)` or `Class.getDeclaredConstructors(..)`. + */ +class ReflectiveConstructorsAccess extends ClassMethodAccess { + ReflectiveConstructorsAccess() { + this.getCallee().hasName("getConstructors") or + this.getCallee().hasName("getDeclaredConstructors") + } +} + +/** + * A call to `Class.getMethods(..)` or `Class.getDeclaredMethods(..)`. + */ +class ReflectiveMethodsAccess extends ClassMethodAccess { + ReflectiveMethodsAccess() { + this.getCallee().hasName("getMethods") or + this.getCallee().hasName("getDeclaredMethods") + } +} + /** * A call to `Class.getMethod(..)` or `Class.getDeclaredMethod(..)`. */ diff --git a/java/ql/src/semmle/code/java/UnitTests.qll b/java/ql/src/semmle/code/java/UnitTests.qll index 1f31564977f..1adc88d35f7 100644 --- a/java/ql/src/semmle/code/java/UnitTests.qll +++ b/java/ql/src/semmle/code/java/UnitTests.qll @@ -168,7 +168,7 @@ class TestNGTestMethod extends Method { or // Or the data provider class should be declared result.getDeclaringType() = - testAnnotation.getValue("dataProviderClass").(TypeLiteral).getTypeName().getType() + testAnnotation.getValue("dataProviderClass").(TypeLiteral).getReferencedType() ) } } @@ -227,7 +227,7 @@ class TestNGListenersAnnotation extends TestNGAnnotation { * Gets a listener defined in this annotation. */ TestNGListenerImpl getAListener() { - result = getAValue("value").(TypeLiteral).getTypeName().getType() + result = getAValue("value").(TypeLiteral).getReferencedType() } } @@ -303,7 +303,7 @@ class JUnitCategoryAnnotation extends Annotation { literal = value.(ArrayCreationExpr).getInit().getAnInit() ) | - result = literal.getTypeName().getType() + result = literal.getReferencedType() ) } } diff --git a/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll b/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll index b8b4b94f2de..641b80f5bd8 100644 --- a/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll +++ b/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll @@ -99,6 +99,7 @@ private module Frameworks { private import semmle.code.java.security.GroovyInjection private import semmle.code.java.security.JexlInjectionSinkModels private import semmle.code.java.security.LdapInjection + private import semmle.code.java.security.MvelInjection private import semmle.code.java.security.XPath private import semmle.code.java.frameworks.android.SQLite private import semmle.code.java.frameworks.Jdbc diff --git a/java/ql/src/semmle/code/java/dataflow/NullGuards.qll b/java/ql/src/semmle/code/java/dataflow/NullGuards.qll index 44f19ce72e4..849750783e7 100644 --- a/java/ql/src/semmle/code/java/dataflow/NullGuards.qll +++ b/java/ql/src/semmle/code/java/dataflow/NullGuards.qll @@ -25,8 +25,8 @@ Expr enumConstEquality(Expr e, boolean polarity, EnumConstant c) { } /** Gets an instanceof expression of `v` with type `type` */ -InstanceOfExpr instanceofExpr(SsaVariable v, Type type) { - result.getTypeName().getType() = type and +InstanceOfExpr instanceofExpr(SsaVariable v, RefType type) { + result.getCheckedType() = type and result.getExpr() = v.getAUse() } diff --git a/java/ql/src/semmle/code/java/dataflow/Nullness.qll b/java/ql/src/semmle/code/java/dataflow/Nullness.qll index b3c36767758..1e6fbe619d1 100644 --- a/java/ql/src/semmle/code/java/dataflow/Nullness.qll +++ b/java/ql/src/semmle/code/java/dataflow/Nullness.qll @@ -504,7 +504,7 @@ private predicate correlatedConditions( inverted = pol1.booleanXor(pol2) ) or - exists(SsaVariable v, Type type | + exists(SsaVariable v, RefType type | cond1.getCondition() = instanceofExpr(v, type) and cond2.getCondition() = instanceofExpr(v, type) and inverted = false diff --git a/java/ql/src/semmle/code/java/dataflow/TypeFlow.qll b/java/ql/src/semmle/code/java/dataflow/TypeFlow.qll index 7ff2c872111..0662293f00b 100644 --- a/java/ql/src/semmle/code/java/dataflow/TypeFlow.qll +++ b/java/ql/src/semmle/code/java/dataflow/TypeFlow.qll @@ -299,7 +299,7 @@ private predicate downcastSuccessor(VarAccess va, RefType t) { private predicate instanceOfGuarded(VarAccess va, RefType t) { exists(InstanceOfExpr ioe, BaseSsaVariable v | ioe.getExpr() = v.getAUse() and - t = ioe.getTypeName().getType() and + t = ioe.getCheckedType() and va = v.getAUse() and guardControls_v1(ioe, va.getBasicBlock(), true) ) @@ -311,7 +311,7 @@ private predicate instanceOfGuarded(VarAccess va, RefType t) { predicate arrayInstanceOfGuarded(ArrayAccess aa, RefType t) { exists(InstanceOfExpr ioe, BaseSsaVariable v1, BaseSsaVariable v2, ArrayAccess aa1 | ioe.getExpr() = aa1 and - t = ioe.getTypeName().getType() and + t = ioe.getCheckedType() and aa1.getArray() = v1.getAUse() and aa1.getIndexExpr() = v2.getAUse() and aa.getArray() = v1.getAUse() and diff --git a/java/ql/src/semmle/code/java/dataflow/internal/ContainerFlow.qll b/java/ql/src/semmle/code/java/dataflow/internal/ContainerFlow.qll index 6c92e16f0b1..47bdc2402d2 100644 --- a/java/ql/src/semmle/code/java/dataflow/internal/ContainerFlow.qll +++ b/java/ql/src/semmle/code/java/dataflow/internal/ContainerFlow.qll @@ -368,7 +368,44 @@ private class ContainerFlowSummaries extends SummaryModelCsv { "java.util;Collections;false;copy;(List,List);;Element of Argument[1];Element of Argument[0];value", "java.util;Collections;false;fill;(List,Object);;Argument[1];Element of Argument[0];value", "java.util;Arrays;false;asList;;;ArrayElement of Argument[0];Element of ReturnValue;value", - "java.util;Collections;false;addAll;(Collection,Object[]);;ArrayElement of Argument[1];Element of Argument[0];value" + "java.util;Collections;false;addAll;(Collection,Object[]);;ArrayElement of Argument[1];Element of Argument[0];value", + "java.util;AbstractMap$SimpleEntry;false;SimpleEntry;(Object,Object);;Argument[0];MapKey of Argument[-1];value", + "java.util;AbstractMap$SimpleEntry;false;SimpleEntry;(Object,Object);;Argument[1];MapValue of Argument[-1];value", + "java.util;AbstractMap$SimpleEntry;false;SimpleEntry;(Entry);;MapKey of Argument[0];MapKey of Argument[-1];value", + "java.util;AbstractMap$SimpleEntry;false;SimpleEntry;(Entry);;MapValue of Argument[0];MapValue of Argument[-1];value", + "java.util;AbstractMap$SimpleImmutableEntry;false;SimpleImmutableEntry;(Object,Object);;Argument[0];MapKey of Argument[-1];value", + "java.util;AbstractMap$SimpleImmutableEntry;false;SimpleImmutableEntry;(Object,Object);;Argument[1];MapValue of Argument[-1];value", + "java.util;AbstractMap$SimpleImmutableEntry;false;SimpleImmutableEntry;(Entry);;MapKey of Argument[0];MapKey of Argument[-1];value", + "java.util;AbstractMap$SimpleImmutableEntry;false;SimpleImmutableEntry;(Entry);;MapValue of Argument[0];MapValue of Argument[-1];value", + "java.util;ArrayDeque;false;ArrayDeque;(Collection);;Element of Argument[0];Element of Argument[-1];value", + "java.util;ArrayList;false;ArrayList;(Collection);;Element of Argument[0];Element of Argument[-1];value", + "java.util;EnumMap;false;EnumMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value", + "java.util;EnumMap;false;EnumMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value", + "java.util;EnumMap;false;EnumMap;(EnumMap);;MapKey of Argument[0];MapKey of Argument[-1];value", + "java.util;EnumMap;false;EnumMap;(EnumMap);;MapValue of Argument[0];MapValue of Argument[-1];value", + "java.util;HashMap;false;HashMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value", + "java.util;HashMap;false;HashMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value", + "java.util;HashSet;false;HashSet;(Collection);;Element of Argument[0];Element of Argument[-1];value", + "java.util;Hashtable;false;Hashtable;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value", + "java.util;Hashtable;false;Hashtable;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value", + "java.util;IdentityHashMap;false;IdentityHashMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value", + "java.util;IdentityHashMap;false;IdentityHashMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value", + "java.util;LinkedHashMap;false;LinkedHashMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value", + "java.util;LinkedHashMap;false;LinkedHashMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value", + "java.util;LinkedHashSet;false;LinkedHashSet;(Collection);;Element of Argument[0];Element of Argument[-1];value", + "java.util;LinkedList;false;LinkedList;(Collection);;Element of Argument[0];Element of Argument[-1];value", + "java.util;PriorityQueue;false;PriorityQueue;(Collection);;Element of Argument[0];Element of Argument[-1];value", + "java.util;PriorityQueue;false;PriorityQueue;(PriorityQueue);;Element of Argument[0];Element of Argument[-1];value", + "java.util;PriorityQueue;false;PriorityQueue;(SortedSet);;Element of Argument[0];Element of Argument[-1];value", + "java.util;TreeMap;false;TreeMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value", + "java.util;TreeMap;false;TreeMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value", + "java.util;TreeMap;false;TreeMap;(SortedMap);;MapKey of Argument[0];MapKey of Argument[-1];value", + "java.util;TreeMap;false;TreeMap;(SortedMap);;MapValue of Argument[0];MapValue of Argument[-1];value", + "java.util;TreeSet;false;TreeSet;(Collection);;Element of Argument[0];Element of Argument[-1];value", + "java.util;TreeSet;false;TreeSet;(SortedSet);;Element of Argument[0];Element of Argument[-1];value", + "java.util;Vector;false;Vector;(Collection);;Element of Argument[0];Element of Argument[-1];value", + "java.util;WeakHashMap;false;WeakHashMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value", + "java.util;WeakHashMap;false;WeakHashMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value" ] } } diff --git a/java/ql/src/semmle/code/java/dataflow/internal/DataFlowDispatch.qll b/java/ql/src/semmle/code/java/dataflow/internal/DataFlowDispatch.qll index 2b6179bfc96..734e07bafca 100644 --- a/java/ql/src/semmle/code/java/dataflow/internal/DataFlowDispatch.qll +++ b/java/ql/src/semmle/code/java/dataflow/internal/DataFlowDispatch.qll @@ -101,7 +101,7 @@ private module DispatchImpl { * restricted to those `ma`s for which a context might make a difference. */ Method viableImplInCallContext(MethodAccess ma, Call ctx) { - result = VirtualDispatch::viableImpl(ma) and + result = viableCallable(ma) and exists(int i, Callable c, Method def, RefType t, boolean exact | mayBenefitFromCallContext(ma, c, i) and c = viableCallable(ctx) and @@ -115,6 +115,8 @@ private module DispatchImpl { result = VirtualDispatch::viableMethodImpl(def, t.getSourceDeclaration(), t2) and not failsUnification(t, t2) ) + or + result = def and def instanceof SummarizedCallable ) } diff --git a/java/ql/src/semmle/code/java/dispatch/VirtualDispatch.qll b/java/ql/src/semmle/code/java/dispatch/VirtualDispatch.qll index a26ff92b704..46151f3b8f2 100644 --- a/java/ql/src/semmle/code/java/dispatch/VirtualDispatch.qll +++ b/java/ql/src/semmle/code/java/dispatch/VirtualDispatch.qll @@ -176,7 +176,7 @@ private module Dispatch { v.getAUse() = q and guardControls_v1(ioe, q.getBasicBlock(), false) and ioe.getExpr() = v.getAUse() and - ioe.getTypeName().getType().getErasure() = t and + ioe.getCheckedType().getErasure() = t and tgt.getDeclaringType().getSourceDeclaration().getASourceSupertype*() = t ) } diff --git a/java/ql/src/semmle/code/java/frameworks/JUnitAnnotations.qll b/java/ql/src/semmle/code/java/frameworks/JUnitAnnotations.qll index afcf661841b..18afe9403e2 100644 --- a/java/ql/src/semmle/code/java/frameworks/JUnitAnnotations.qll +++ b/java/ql/src/semmle/code/java/frameworks/JUnitAnnotations.qll @@ -64,5 +64,5 @@ class RunWithAnnotation extends Annotation { /** * Gets the runner that will be used. */ - Type getRunner() { result = getValue("value").(TypeLiteral).getTypeName().getType() } + Type getRunner() { result = getValue("value").(TypeLiteral).getReferencedType() } } diff --git a/java/ql/src/semmle/code/java/frameworks/Jackson.qll b/java/ql/src/semmle/code/java/frameworks/Jackson.qll new file mode 100644 index 00000000000..505a8e9cdf0 --- /dev/null +++ b/java/ql/src/semmle/code/java/frameworks/Jackson.qll @@ -0,0 +1,174 @@ +/** + * Provides classes and predicates for working with the Jackson serialization framework. + */ + +import java +private import semmle.code.java.Reflection +private import semmle.code.java.dataflow.DataFlow + +private class ObjectMapper extends RefType { + ObjectMapper() { + getASupertype*().hasQualifiedName("com.fasterxml.jackson.databind", "ObjectMapper") + } +} + +/** A builder for building Jackson's `JsonMapper`. */ +class MapperBuilder extends RefType { + MapperBuilder() { + hasQualifiedName("com.fasterxml.jackson.databind.cfg", "MapperBuilder") + } +} + +private class JsonFactory extends RefType { + JsonFactory() { hasQualifiedName("com.fasterxml.jackson.core", "JsonFactory") } +} + +private class JsonParser extends RefType { + JsonParser() { hasQualifiedName("com.fasterxml.jackson.core", "JsonParser") } +} + +/** A type descriptor in Jackson libraries. For example, `java.lang.Class`. */ +class JacksonTypeDescriptorType extends RefType { + JacksonTypeDescriptorType() { + this instanceof TypeClass or + hasQualifiedName("com.fasterxml.jackson.databind", "JavaType") or + hasQualifiedName("com.fasterxml.jackson.core.type", "TypeReference") + } +} + +/** A method in `ObjectMapper` that deserialize data. */ +class ObjectMapperReadMethod extends Method { + ObjectMapperReadMethod() { + this.getDeclaringType() instanceof ObjectMapper and + this.hasName(["readValue", "readValues", "treeToValue"]) + } +} + +/** A call that enables the default typing in `ObjectMapper`. */ +class EnableJacksonDefaultTyping extends MethodAccess { + EnableJacksonDefaultTyping() { + this.getMethod().getDeclaringType() instanceof ObjectMapper and + this.getMethod().hasName("enableDefaultTyping") + } +} + +/** A qualifier of a call to one of the methods in `ObjectMapper` that deserialize data. */ +class ObjectMapperReadQualifier extends DataFlow::ExprNode { + ObjectMapperReadQualifier() { + exists(MethodAccess ma | ma.getQualifier() = this.asExpr() | + ma.getMethod() instanceof ObjectMapperReadMethod + ) + } +} + +/** A source that sets a type validator. */ +class SetPolymorphicTypeValidatorSource extends DataFlow::ExprNode { + SetPolymorphicTypeValidatorSource() { + exists(MethodAccess ma, Method m | m = ma.getMethod() | + ( + m.getDeclaringType() instanceof ObjectMapper and + m.hasName("setPolymorphicTypeValidator") + or + m.getDeclaringType() instanceof MapperBuilder and + m.hasName("polymorphicTypeValidator") + ) and + this.asExpr() = ma.getQualifier() + ) + } +} + +/** Holds if `fromNode` to `toNode` is a dataflow step that resolves a class. */ +predicate resolveClassStep(DataFlow::Node fromNode, DataFlow::Node toNode) { + exists(ReflectiveClassIdentifierMethodAccess ma | + ma.getArgument(0) = fromNode.asExpr() and + ma = toNode.asExpr() + ) +} + +/** + * Holds if `fromNode` to `toNode` is a dataflow step that creates a Jackson parser. + * + * For example, a `createParser(userString)` call yields a `JsonParser`, which becomes dangerous + * if passed to an unsafely-configured `ObjectMapper`'s `readValue` method. + */ +predicate createJacksonJsonParserStep(DataFlow::Node fromNode, DataFlow::Node toNode) { + exists(MethodAccess ma, Method m | m = ma.getMethod() | + (m.getDeclaringType() instanceof ObjectMapper or m.getDeclaringType() instanceof JsonFactory) and + m.hasName("createParser") and + ma.getArgument(0) = fromNode.asExpr() and + ma = toNode.asExpr() + ) +} + +/** + * Holds if `fromNode` to `toNode` is a dataflow step that creates a Jackson `TreeNode`. + * + * These are parse trees of user-supplied JSON, which may lead to arbitrary code execution + * if passed to an unsafely-configured `ObjectMapper`'s `treeToValue` method. + */ +predicate createJacksonTreeNodeStep(DataFlow::Node fromNode, DataFlow::Node toNode) { + exists(MethodAccess ma, Method m | m = ma.getMethod() | + m.getDeclaringType() instanceof ObjectMapper and + m.hasName("readTree") and + ma.getArgument(0) = fromNode.asExpr() and + ma = toNode.asExpr() + ) + or + exists(MethodAccess ma, Method m | m = ma.getMethod() | + m.getDeclaringType() instanceof JsonParser and + m.hasName("readValueAsTree") and + ma.getQualifier() = fromNode.asExpr() and + ma = toNode.asExpr() + ) +} + +/** + * Holds if `type` or one of its supertypes has a field with `JsonTypeInfo` annotation + * that enables polymorphic type handling. + */ +private predicate hasJsonTypeInfoAnnotation(RefType type) { + hasFieldWithJsonTypeAnnotation(type.getASupertype*()) or + hasJsonTypeInfoAnnotation(type.getAField().getType()) +} + +/** + * Holds if `type` has a field with `JsonTypeInfo` annotation + * that enables polymorphic type handling. + */ +private predicate hasFieldWithJsonTypeAnnotation(RefType type) { + exists(Annotation a | + type.getAField().getAnAnnotation() = a and + a.getType().hasQualifiedName("com.fasterxml.jackson.annotation", "JsonTypeInfo") and + a.getValue("use").(VarAccess).getVariable().hasName(["CLASS", "MINIMAL_CLASS"]) + ) +} + +/** + * Holds if `call` is a method call to a Jackson deserialization method such as `ObjectMapper.readValue(String, Class)`, + * and the target deserialized class has a field with a `JsonTypeInfo` annotation that enables polymorphic typing. + */ +predicate hasArgumentWithUnsafeJacksonAnnotation(MethodAccess call) { + call.getMethod() instanceof ObjectMapperReadMethod and + exists(RefType argType, int i | i > 0 and argType = call.getArgument(i).getType() | + hasJsonTypeInfoAnnotation(argType.(ParameterizedType).getATypeArgument()) + ) +} + +/** + * Holds if `fromNode` to `toNode` is a dataflow step that looks like resolving a class. + * A method probably resolves a class if it takes a string, returns a type descriptor, + * and its name contains "resolve", "load", etc. + * + * Any method call that satisfies the rule above is assumed to propagate taint from its string arguments, + * so methods that accept user-controlled data but sanitize it or use it for some + * completely different purpose before returning a type descriptor could result in false positives. + */ +predicate looksLikeResolveClassStep(DataFlow::Node fromNode, DataFlow::Node toNode) { + exists(MethodAccess ma, Method m, Expr arg | m = ma.getMethod() and arg = ma.getAnArgument() | + m.getReturnType() instanceof JacksonTypeDescriptorType and + m.getName().toLowerCase().regexpMatch("(.*)(resolve|load|class|type)(.*)") and + arg.getType() instanceof TypeString and + arg = fromNode.asExpr() and + ma = toNode.asExpr() + ) +} diff --git a/java/ql/src/semmle/code/java/frameworks/google/GoogleHttpClientApi.qll b/java/ql/src/semmle/code/java/frameworks/google/GoogleHttpClientApi.qll index ccc446892f1..f7ff6f69ff3 100644 --- a/java/ql/src/semmle/code/java/frameworks/google/GoogleHttpClientApi.qll +++ b/java/ql/src/semmle/code/java/frameworks/google/GoogleHttpClientApi.qll @@ -33,7 +33,7 @@ class HttpResponseParseAsDeserializableField extends DeserializableField { HttpResponseParseAsDeserializableField() { exists(RefType decltype, TypeLiteralToParseAsFlowConfiguration conf | decltype = getDeclaringType() and - conf.getSourceWithFlowToParseAs().getTypeName().getType() = decltype and + conf.getSourceWithFlowToParseAs().getReferencedType() = decltype and decltype.fromSource() ) } diff --git a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll index 618fc1d2710..a11c471d852 100644 --- a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll +++ b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll @@ -112,7 +112,7 @@ private class TypeLiteralToJacksonDatabindFlowConfiguration extends DataFlow5::C private class ExplicitlyReadJacksonDeserializableType extends JacksonDeserializableType { ExplicitlyReadJacksonDeserializableType() { exists(TypeLiteralToJacksonDatabindFlowConfiguration conf | - usesType(conf.getSourceWithFlowToJacksonDatabind().getTypeName().getType(), this) + usesType(conf.getSourceWithFlowToJacksonDatabind().getReferencedType(), this) ) or exists(MethodAccess ma | diff --git a/java/ql/src/semmle/code/java/frameworks/javaee/ejb/EJB.qll b/java/ql/src/semmle/code/java/frameworks/javaee/ejb/EJB.qll index 6afcf78272f..54434d895f2 100644 --- a/java/ql/src/semmle/code/java/frameworks/javaee/ejb/EJB.qll +++ b/java/ql/src/semmle/code/java/frameworks/javaee/ejb/EJB.qll @@ -199,7 +199,7 @@ abstract class EjbInterfaceAnnotation extends Annotation { // within the "value" element of this annotation. // Uses `getAChildExpr*()` since the "value" element can have type `Class` or `Class[]`. exists(TypeLiteral tl | tl = getValue("value").getAChildExpr*() | - exists(TypeAccess ta | ta = tl.getTypeName() | result = ta.getType()) + result = tl.getReferencedType() ) } } diff --git a/java/ql/src/semmle/code/java/frameworks/spring/SpringComponentScan.qll b/java/ql/src/semmle/code/java/frameworks/spring/SpringComponentScan.qll index 7f1253f1f62..568987114f2 100644 --- a/java/ql/src/semmle/code/java/frameworks/spring/SpringComponentScan.qll +++ b/java/ql/src/semmle/code/java/frameworks/spring/SpringComponentScan.qll @@ -45,7 +45,7 @@ class SpringComponentScan extends Annotation { // Base package classes are type literals whose package should be considered a base package. typeLiteral = getAValue("basePackageClasses") | - result = typeLiteral.getTypeName().getType().(RefType).getPackage().getName() + result = typeLiteral.getReferencedType().(RefType).getPackage().getName() ) } } diff --git a/java/ql/src/semmle/code/java/security/MvelInjection.qll b/java/ql/src/semmle/code/java/security/MvelInjection.qll new file mode 100644 index 00000000000..984641fbd18 --- /dev/null +++ b/java/ql/src/semmle/code/java/security/MvelInjection.qll @@ -0,0 +1,233 @@ +/** Provides classes to reason about MVEL injection attacks. */ + +import java +private import semmle.code.java.dataflow.DataFlow +private import semmle.code.java.dataflow.ExternalFlow + +/** A data flow sink for unvalidated user input that is used to construct MVEL expressions. */ +abstract class MvelEvaluationSink extends DataFlow::Node { } + +/** A sanitizer that prevents MVEL injection attacks. */ +abstract class MvelInjectionSanitizer extends DataFlow::Node { } + +/** + * A unit class for adding additional taint steps. + * + * Extend this class to add additional taint steps that should apply to the `MvelInjectionFlowConfig`. + */ +class MvelInjectionAdditionalTaintStep extends Unit { + /** + * Holds if the step from `node1` to `node2` should be considered a taint + * step for the `MvelInjectionFlowConfig` configuration. + */ + abstract predicate step(DataFlow::Node n1, DataFlow::Node n2); +} + +/** Default sink for MVEL injection vulnerabilities. */ +private class DefaultMvelEvaluationSink extends MvelEvaluationSink { + DefaultMvelEvaluationSink() { sinkNode(this, "mvel") } +} + +private class DefaulMvelEvaluationSinkModel extends SinkModelCsv { + override predicate row(string row) { + row = + [ + "javax.script;CompiledScript;false;eval;;;Argument[-1];mvel", + "org.mvel2;MVEL;false;eval;;;Argument[0];mvel", + "org.mvel2;MVEL;false;executeExpression;;;Argument[0];mvel", + "org.mvel2;MVEL;false;evalToBoolean;;;Argument[0];mvel", + "org.mvel2;MVEL;false;evalToString;;;Argument[0];mvel", + "org.mvel2;MVEL;false;executeAllExpression;;;Argument[0];mvel", + "org.mvel2;MVEL;false;executeSetExpression;;;Argument[0];mvel", + "org.mvel2;MVELRuntime;false;execute;;;Argument[1];mvel", + "org.mvel2.templates;TemplateRuntime;false;eval;;;Argument[0];mvel", + "org.mvel2.templates;TemplateRuntime;false;execute;;;Argument[0];mvel", + "org.mvel2.jsr223;MvelScriptEngine;false;eval;;;Argument[0];mvel", + "org.mvel2.jsr223;MvelScriptEngine;false;evaluate;;;Argument[0];mvel", + "org.mvel2.jsr223;MvelCompiledScript;false;eval;;;Argument[-1];mvel", + "org.mvel2.compiler;ExecutableStatement;false;getValue;;;Argument[-1];mvel", + "org.mvel2.compiler;CompiledExpression;false;getDirectValue;;;Argument[-1];mvel", + "org.mvel2.compiler;CompiledAccExpression;false;getValue;;;Argument[-1];mvel", + "org.mvel2.compiler;Accessor;false;getValue;;;Argument[-1];mvel" + ] + } +} + +/** A default sanitizer that considers numeric and boolean typed data safe for building MVEL expressions */ +private class DefaultMvelInjectionSanitizer extends MvelInjectionSanitizer { + DefaultMvelInjectionSanitizer() { + this.getType() instanceof NumericType or this.getType() instanceof BooleanType + } +} + +/** A set of additional taint steps to consider when taint tracking MVEL related data flows. */ +private class DefaultMvelInjectionAdditionalTaintStep extends MvelInjectionAdditionalTaintStep { + override predicate step(DataFlow::Node node1, DataFlow::Node node2) { + expressionCompilationStep(node1, node2) or + createExpressionCompilerStep(node1, node2) or + expressionCompilerCompileStep(node1, node2) or + createCompiledAccExpressionStep(node1, node2) or + scriptCompileStep(node1, node2) or + createMvelCompiledScriptStep(node1, node2) or + templateCompileStep(node1, node2) or + createTemplateCompilerStep(node1, node2) + } +} + +/** + * Holds if `node1` to `node2` is a dataflow step that compiles a MVEL expression + * by callilng `MVEL.compileExpression(tainted)`. + */ +private predicate expressionCompilationStep(DataFlow::Node node1, DataFlow::Node node2) { + exists(StaticMethodAccess ma, Method m | ma.getMethod() = m | + m.getDeclaringType() instanceof MVEL and + m.hasName("compileExpression") and + ma.getAnArgument() = node1.asExpr() and + node2.asExpr() = ma + ) +} + +/** + * Holds if `node1` to `node2` is a dataflow step that creates `ExpressionCompiler` + * by calling `new ExpressionCompiler(tainted)`. + */ +private predicate createExpressionCompilerStep(DataFlow::Node node1, DataFlow::Node node2) { + exists(ConstructorCall cc | + cc.getConstructedType() instanceof ExpressionCompiler and + cc = node2.asExpr() and + cc.getArgument(0) = node1.asExpr() + ) +} + +/** + * Holds if `node1` to `node2` is a dataflow step creates `CompiledAccExpression` + * by calling `new CompiledAccExpression(tainted, ...)`. + */ +private predicate createCompiledAccExpressionStep(DataFlow::Node node1, DataFlow::Node node2) { + exists(ConstructorCall cc | + cc.getConstructedType() instanceof CompiledAccExpression and + cc = node2.asExpr() and + cc.getArgument(0) = node1.asExpr() + ) +} + +/** + * Holds if `node1` to `node2` is a dataflow step that compiles a MVEL expression + * by calling `ExpressionCompiler.compile()`. + */ +private predicate expressionCompilerCompileStep(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess ma, Method m | ma.getMethod() = m | + m.getDeclaringType() instanceof ExpressionCompiler and + m.hasName("compile") and + ma = node2.asExpr() and + ma.getQualifier() = node1.asExpr() + ) +} + +/** + * Holds if `node1` to `node2` is a dataflow step that compiles a script via `MvelScriptEngine` + * by calling `engine.compile(tainted)` or `engine.compiledScript(tainted)`. + */ +private predicate scriptCompileStep(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess ma, Method m | ma.getMethod() = m | + m instanceof MvelScriptEngineCompilationMethod and + ma = node2.asExpr() and + ma.getArgument(0) = node1.asExpr() + ) +} + +/** + * Holds if `node1` to `node2` is a dataflow step that creates `MvelCompiledScript` + * by calling `new MvelCompiledScript(engine, tainted)`. + */ +private predicate createMvelCompiledScriptStep(DataFlow::Node node1, DataFlow::Node node2) { + exists(ConstructorCall cc | + cc.getConstructedType() instanceof MvelCompiledScript and + cc = node2.asExpr() and + cc.getArgument(1) = node1.asExpr() + ) +} + +/** + * Holds if `node1` to `node2` is a dataflow step creates `TemplateCompiler` + * by calling `new TemplateCompiler(tainted)`. + */ +private predicate createTemplateCompilerStep(DataFlow::Node node1, DataFlow::Node node2) { + exists(ConstructorCall cc | + cc.getConstructedType() instanceof TemplateCompiler and + cc = node2.asExpr() and + cc.getArgument(0) = node1.asExpr() + ) +} + +/** + * Holds if `node1` to `node2` is a dataflow step that compiles a script via `TemplateCompiler` + * by calling `compiler.compile()` or `TemplateCompiler.compileTemplate(tainted)`. + */ +private predicate templateCompileStep(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess ma, Method m | ma.getMethod() = m | + m instanceof TemplateCompilerCompileMethod and + ma.getQualifier() = node1.asExpr() and + ma = node2.asExpr() + ) + or + exists(StaticMethodAccess ma, Method m | ma.getMethod() = m | + m instanceof TemplateCompilerCompileTemplateMethod and + ma = node2.asExpr() and + ma.getArgument(0) = node1.asExpr() + ) +} + +/** + * Methods in `MvelScriptEngine` that compile a MVEL expression. + */ +private class MvelScriptEngineCompilationMethod extends Method { + MvelScriptEngineCompilationMethod() { + getDeclaringType() instanceof MvelScriptEngine and + hasName(["compile", "compiledScript"]) + } +} + +/** + * `TemplateCompiler.compile()` method that compiles a MVEL template. + */ +private class TemplateCompilerCompileMethod extends Method { + TemplateCompilerCompileMethod() { + getDeclaringType() instanceof TemplateCompiler and + hasName("compile") + } +} + +/** + * `TemplateCompiler.compileTemplate(tainted)` static method that compiles a MVEL template. + */ +private class TemplateCompilerCompileTemplateMethod extends Method { + TemplateCompilerCompileTemplateMethod() { + getDeclaringType() instanceof TemplateCompiler and + hasName("compileTemplate") + } +} + +private class MVEL extends RefType { + MVEL() { hasQualifiedName("org.mvel2", "MVEL") } +} + +private class ExpressionCompiler extends RefType { + ExpressionCompiler() { hasQualifiedName("org.mvel2.compiler", "ExpressionCompiler") } +} + +private class CompiledAccExpression extends RefType { + CompiledAccExpression() { hasQualifiedName("org.mvel2.compiler", "CompiledAccExpression") } +} + +private class MvelScriptEngine extends RefType { + MvelScriptEngine() { hasQualifiedName("org.mvel2.jsr223", "MvelScriptEngine") } +} + +private class MvelCompiledScript extends RefType { + MvelCompiledScript() { hasQualifiedName("org.mvel2.jsr223", "MvelCompiledScript") } +} + +private class TemplateCompiler extends RefType { + TemplateCompiler() { hasQualifiedName("org.mvel2.templates", "TemplateCompiler") } +} diff --git a/java/ql/src/semmle/code/java/security/MvelInjectionQuery.qll b/java/ql/src/semmle/code/java/security/MvelInjectionQuery.qll new file mode 100644 index 00000000000..332ea387e6b --- /dev/null +++ b/java/ql/src/semmle/code/java/security/MvelInjectionQuery.qll @@ -0,0 +1,26 @@ +/** Provides taint tracking configurations to be used in MVEL injection related queries. */ + +import java +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.dataflow.TaintTracking +import semmle.code.java.security.MvelInjection + +/** + * A taint-tracking configuration for unsafe user input + * that is used to construct and evaluate a MVEL expression. + */ +class MvelInjectionFlowConfig extends TaintTracking::Configuration { + MvelInjectionFlowConfig() { this = "MvelInjectionFlowConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof MvelEvaluationSink } + + override predicate isSanitizer(DataFlow::Node sanitizer) { + sanitizer instanceof MvelInjectionSanitizer + } + + override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { + any(MvelInjectionAdditionalTaintStep c).step(node1, node2) + } +} diff --git a/java/ql/src/semmle/code/java/security/UnsafeDeserialization.qll b/java/ql/src/semmle/code/java/security/UnsafeDeserialization.qll deleted file mode 100644 index d035837ad6a..00000000000 --- a/java/ql/src/semmle/code/java/security/UnsafeDeserialization.qll +++ /dev/null @@ -1,172 +0,0 @@ -import semmle.code.java.frameworks.Kryo -import semmle.code.java.frameworks.XStream -import semmle.code.java.frameworks.SnakeYaml -import semmle.code.java.frameworks.FastJson -import semmle.code.java.frameworks.JYaml -import semmle.code.java.frameworks.JsonIo -import semmle.code.java.frameworks.YamlBeans -import semmle.code.java.frameworks.HessianBurlap -import semmle.code.java.frameworks.Castor -import semmle.code.java.frameworks.apache.Lang - -class ObjectInputStreamReadObjectMethod extends Method { - ObjectInputStreamReadObjectMethod() { - this.getDeclaringType().getASourceSupertype*().hasQualifiedName("java.io", "ObjectInputStream") and - (this.hasName("readObject") or this.hasName("readUnshared")) - } -} - -class XMLDecoderReadObjectMethod extends Method { - XMLDecoderReadObjectMethod() { - this.getDeclaringType().hasQualifiedName("java.beans", "XMLDecoder") and - this.hasName("readObject") - } -} - -class SafeXStream extends DataFlow2::Configuration { - SafeXStream() { this = "UnsafeDeserialization::SafeXStream" } - - override predicate isSource(DataFlow::Node src) { - any(XStreamEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() = - src.asExpr() - } - - override predicate isSink(DataFlow::Node sink) { - exists(MethodAccess ma | - sink.asExpr() = ma.getQualifier() and - ma.getMethod() instanceof XStreamReadObjectMethod - ) - } -} - -class SafeKryo extends DataFlow2::Configuration { - SafeKryo() { this = "UnsafeDeserialization::SafeKryo" } - - override predicate isSource(DataFlow::Node src) { - any(KryoEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() = - src.asExpr() - } - - override predicate isSink(DataFlow::Node sink) { - exists(MethodAccess ma | - sink.asExpr() = ma.getQualifier() and - ma.getMethod() instanceof KryoReadObjectMethod - ) - } - - override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) { - stepKryoPoolBuilderFactoryArgToConstructor(node1, node2) or - stepKryoPoolRunMethodAccessQualifierToFunctionalArgument(node1, node2) or - stepKryoPoolBuilderChainMethod(node1, node2) or - stepKryoPoolBorrowMethod(node1, node2) - } - - /** - * Holds when a functional expression is used to create a `KryoPool.Builder`. - * Eg. `new KryoPool.Builder(() -> new Kryo())` - */ - private predicate stepKryoPoolBuilderFactoryArgToConstructor( - DataFlow::Node node1, DataFlow::Node node2 - ) { - exists(ConstructorCall cc, FunctionalExpr fe | - cc.getConstructedType() instanceof KryoPoolBuilder and - fe.asMethod().getBody().getAStmt().(ReturnStmt).getResult() = node1.asExpr() and - node2.asExpr() = cc and - cc.getArgument(0) = fe - ) - } - - /** - * Holds when a `KryoPool.run` is called to use a `Kryo` instance. - * Eg. `pool.run(kryo -> ...)` - */ - private predicate stepKryoPoolRunMethodAccessQualifierToFunctionalArgument( - DataFlow::Node node1, DataFlow::Node node2 - ) { - exists(MethodAccess ma | - ma.getMethod() instanceof KryoPoolRunMethod and - node1.asExpr() = ma.getQualifier() and - ma.getArgument(0).(FunctionalExpr).asMethod().getParameter(0) = node2.asParameter() - ) - } - - /** - * Holds when a `KryoPool.Builder` method is called fluently. - */ - private predicate stepKryoPoolBuilderChainMethod(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodAccess ma | - ma.getMethod() instanceof KryoPoolBuilderMethod and - ma = node2.asExpr() and - ma.getQualifier() = node1.asExpr() - ) - } - - /** - * Holds when a `KryoPool.borrow` method is called. - */ - private predicate stepKryoPoolBorrowMethod(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodAccess ma | - ma.getMethod() = - any(Method m | m.getDeclaringType() instanceof KryoPool and m.hasName("borrow")) and - node1.asExpr() = ma.getQualifier() and - node2.asExpr() = ma - ) - } -} - -predicate unsafeDeserialization(MethodAccess ma, Expr sink) { - exists(Method m | m = ma.getMethod() | - m instanceof ObjectInputStreamReadObjectMethod and - sink = ma.getQualifier() and - not exists(DataFlow::ExprNode node | - node.getExpr() = sink and - node.getTypeBound() - .(RefType) - .hasQualifiedName("org.apache.commons.io.serialization", "ValidatingObjectInputStream") - ) - or - m instanceof XMLDecoderReadObjectMethod and - sink = ma.getQualifier() - or - m instanceof XStreamReadObjectMethod and - sink = ma.getAnArgument() and - not exists(SafeXStream sxs | sxs.hasFlowToExpr(ma.getQualifier())) - or - m instanceof KryoReadObjectMethod and - sink = ma.getAnArgument() and - not exists(SafeKryo sk | sk.hasFlowToExpr(ma.getQualifier())) - or - m instanceof MethodApacheSerializationUtilsDeserialize and - sink = ma.getArgument(0) - or - ma instanceof UnsafeSnakeYamlParse and - sink = ma.getArgument(0) - or - ma.getMethod() instanceof FastJsonParseMethod and - not fastJsonLooksSafe() and - sink = ma.getArgument(0) - or - ma.getMethod() instanceof JYamlLoaderUnsafeLoadMethod and - sink = ma.getArgument(0) - or - ma.getMethod() instanceof JsonIoJsonToJavaMethod and - sink = ma.getArgument(0) - or - ma.getMethod() instanceof JsonIoReadObjectMethod and - sink = ma.getQualifier() - or - ma.getMethod() instanceof YamlBeansReaderReadMethod and sink = ma.getQualifier() - or - ma.getMethod() instanceof UnsafeHessianInputReadObjectMethod and sink = ma.getQualifier() - or - ma.getMethod() instanceof CastorUnmarshalMethod and sink = ma.getAnArgument() - or - ma.getMethod() instanceof BurlapInputReadObjectMethod and sink = ma.getQualifier() - ) -} - -class UnsafeDeserializationSink extends DataFlow::ExprNode { - UnsafeDeserializationSink() { unsafeDeserialization(_, this.getExpr()) } - - MethodAccess getMethodAccess() { unsafeDeserialization(result, this.getExpr()) } -} diff --git a/java/ql/src/semmle/code/java/security/UnsafeDeserializationQuery.qll b/java/ql/src/semmle/code/java/security/UnsafeDeserializationQuery.qll new file mode 100644 index 00000000000..b9cfa1ddde8 --- /dev/null +++ b/java/ql/src/semmle/code/java/security/UnsafeDeserializationQuery.qll @@ -0,0 +1,318 @@ +/** + * Provides classes and predicates for finding deserialization vulnerabilities. + */ + +import semmle.code.java.dataflow.FlowSources +private import semmle.code.java.dataflow.TaintTracking2 +private import semmle.code.java.frameworks.Kryo +private import semmle.code.java.frameworks.XStream +private import semmle.code.java.frameworks.SnakeYaml +private import semmle.code.java.frameworks.FastJson +private import semmle.code.java.frameworks.JYaml +private import semmle.code.java.frameworks.JsonIo +private import semmle.code.java.frameworks.YamlBeans +private import semmle.code.java.frameworks.HessianBurlap +private import semmle.code.java.frameworks.Castor +private import semmle.code.java.frameworks.Jackson +private import semmle.code.java.frameworks.apache.Lang +private import semmle.code.java.Reflection + +private class ObjectInputStreamReadObjectMethod extends Method { + ObjectInputStreamReadObjectMethod() { + this.getDeclaringType().getASourceSupertype*().hasQualifiedName("java.io", "ObjectInputStream") and + (this.hasName("readObject") or this.hasName("readUnshared")) + } +} + +private class XMLDecoderReadObjectMethod extends Method { + XMLDecoderReadObjectMethod() { + this.getDeclaringType().hasQualifiedName("java.beans", "XMLDecoder") and + this.hasName("readObject") + } +} + +private class SafeXStream extends DataFlow2::Configuration { + SafeXStream() { this = "UnsafeDeserialization::SafeXStream" } + + override predicate isSource(DataFlow::Node src) { + any(XStreamEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() = + src.asExpr() + } + + override predicate isSink(DataFlow::Node sink) { + exists(MethodAccess ma | + sink.asExpr() = ma.getQualifier() and + ma.getMethod() instanceof XStreamReadObjectMethod + ) + } +} + +private class SafeKryo extends DataFlow2::Configuration { + SafeKryo() { this = "UnsafeDeserialization::SafeKryo" } + + override predicate isSource(DataFlow::Node src) { + any(KryoEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() = + src.asExpr() + } + + override predicate isSink(DataFlow::Node sink) { + exists(MethodAccess ma | + sink.asExpr() = ma.getQualifier() and + ma.getMethod() instanceof KryoReadObjectMethod + ) + } + + override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) { + stepKryoPoolBuilderFactoryArgToConstructor(node1, node2) or + stepKryoPoolRunMethodAccessQualifierToFunctionalArgument(node1, node2) or + stepKryoPoolBuilderChainMethod(node1, node2) or + stepKryoPoolBorrowMethod(node1, node2) + } + + /** + * Holds when a functional expression is used to create a `KryoPool.Builder`. + * Eg. `new KryoPool.Builder(() -> new Kryo())` + */ + private predicate stepKryoPoolBuilderFactoryArgToConstructor( + DataFlow::Node node1, DataFlow::Node node2 + ) { + exists(ConstructorCall cc, FunctionalExpr fe | + cc.getConstructedType() instanceof KryoPoolBuilder and + fe.asMethod().getBody().getAStmt().(ReturnStmt).getResult() = node1.asExpr() and + node2.asExpr() = cc and + cc.getArgument(0) = fe + ) + } + + /** + * Holds when a `KryoPool.run` is called to use a `Kryo` instance. + * Eg. `pool.run(kryo -> ...)` + */ + private predicate stepKryoPoolRunMethodAccessQualifierToFunctionalArgument( + DataFlow::Node node1, DataFlow::Node node2 + ) { + exists(MethodAccess ma | + ma.getMethod() instanceof KryoPoolRunMethod and + node1.asExpr() = ma.getQualifier() and + ma.getArgument(0).(FunctionalExpr).asMethod().getParameter(0) = node2.asParameter() + ) + } + + /** + * Holds when a `KryoPool.Builder` method is called fluently. + */ + private predicate stepKryoPoolBuilderChainMethod(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess ma | + ma.getMethod() instanceof KryoPoolBuilderMethod and + ma = node2.asExpr() and + ma.getQualifier() = node1.asExpr() + ) + } + + /** + * Holds when a `KryoPool.borrow` method is called. + */ + private predicate stepKryoPoolBorrowMethod(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess ma | + ma.getMethod() = + any(Method m | m.getDeclaringType() instanceof KryoPool and m.hasName("borrow")) and + node1.asExpr() = ma.getQualifier() and + node2.asExpr() = ma + ) + } +} + +/** + * Holds if `ma` is a call that deserializes data from `sink`. + */ +predicate unsafeDeserialization(MethodAccess ma, Expr sink) { + exists(Method m | m = ma.getMethod() | + m instanceof ObjectInputStreamReadObjectMethod and + sink = ma.getQualifier() and + not exists(DataFlow::ExprNode node | + node.getExpr() = sink and + node.getTypeBound() + .(RefType) + .hasQualifiedName("org.apache.commons.io.serialization", "ValidatingObjectInputStream") + ) + or + m instanceof XMLDecoderReadObjectMethod and + sink = ma.getQualifier() + or + m instanceof XStreamReadObjectMethod and + sink = ma.getAnArgument() and + not exists(SafeXStream sxs | sxs.hasFlowToExpr(ma.getQualifier())) + or + m instanceof KryoReadObjectMethod and + sink = ma.getAnArgument() and + not exists(SafeKryo sk | sk.hasFlowToExpr(ma.getQualifier())) + or + m instanceof MethodApacheSerializationUtilsDeserialize and + sink = ma.getArgument(0) + or + ma instanceof UnsafeSnakeYamlParse and + sink = ma.getArgument(0) + or + ma.getMethod() instanceof FastJsonParseMethod and + not fastJsonLooksSafe() and + sink = ma.getArgument(0) + or + ma.getMethod() instanceof JYamlLoaderUnsafeLoadMethod and + sink = ma.getArgument(0) + or + ma.getMethod() instanceof JsonIoJsonToJavaMethod and + sink = ma.getArgument(0) + or + ma.getMethod() instanceof JsonIoReadObjectMethod and + sink = ma.getQualifier() + or + ma.getMethod() instanceof YamlBeansReaderReadMethod and sink = ma.getQualifier() + or + ma.getMethod() instanceof UnsafeHessianInputReadObjectMethod and sink = ma.getQualifier() + or + ma.getMethod() instanceof CastorUnmarshalMethod and sink = ma.getAnArgument() + or + ma.getMethod() instanceof BurlapInputReadObjectMethod and sink = ma.getQualifier() + or + ma.getMethod() instanceof ObjectMapperReadMethod and + sink = ma.getArgument(0) and + ( + exists(UnsafeTypeConfig config | config.hasFlowToExpr(ma.getAnArgument())) + or + exists(EnableJacksonDefaultTypingConfig config | config.hasFlowToExpr(ma.getQualifier())) + or + hasArgumentWithUnsafeJacksonAnnotation(ma) + ) and + not exists(SafeObjectMapperConfig config | config.hasFlowToExpr(ma.getQualifier())) + ) +} + +/** A sink for unsafe deserialization. */ +class UnsafeDeserializationSink extends DataFlow::ExprNode { + UnsafeDeserializationSink() { unsafeDeserialization(_, this.getExpr()) } + + /** Gets a call that triggers unsafe deserialization. */ + MethodAccess getMethodAccess() { unsafeDeserialization(result, this.getExpr()) } +} + +/** + * Tracks flows from remote user input to a deserialization sink. + */ +class UnsafeDeserializationConfig extends TaintTracking::Configuration { + UnsafeDeserializationConfig() { this = "UnsafeDeserializationConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof UnsafeDeserializationSink } + + override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { + exists(ClassInstanceExpr cie | + cie.getArgument(0) = pred.asExpr() and + cie = succ.asExpr() and + ( + cie.getConstructor().getDeclaringType() instanceof JsonIoJsonReader or + cie.getConstructor().getDeclaringType() instanceof YamlBeansReader or + cie.getConstructor().getDeclaringType().getASupertype*() instanceof UnsafeHessianInput or + cie.getConstructor().getDeclaringType() instanceof BurlapInput + ) + ) + or + exists(MethodAccess ma | + ma.getMethod() instanceof BurlapInputInitMethod and + ma.getArgument(0) = pred.asExpr() and + ma.getQualifier() = succ.asExpr() + ) + or + createJacksonJsonParserStep(pred, succ) + or + createJacksonTreeNodeStep(pred, succ) + } + + override predicate isSanitizer(DataFlow::Node node) { + exists(ClassInstanceExpr cie | + cie.getConstructor().getDeclaringType() instanceof JsonIoJsonReader and + cie = node.asExpr() and + exists(SafeJsonIoConfig sji | sji.hasFlowToExpr(cie.getArgument(1))) + ) + or + exists(MethodAccess ma | + ma.getMethod() instanceof JsonIoJsonToJavaMethod and + ma.getArgument(0) = node.asExpr() and + exists(SafeJsonIoConfig sji | sji.hasFlowToExpr(ma.getArgument(1))) + ) + } +} + +/** + * Tracks flow from a remote source to a type descriptor (e.g. a `java.lang.Class` instance) + * passed to a Jackson deserialization method. + * + * If this is user-controlled, arbitrary code could be executed while instantiating the user-specified type. + */ +class UnsafeTypeConfig extends TaintTracking2::Configuration { + UnsafeTypeConfig() { this = "UnsafeTypeConfig" } + + override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + exists(MethodAccess ma, int i, Expr arg | i > 0 and ma.getArgument(i) = arg | + ma.getMethod() instanceof ObjectMapperReadMethod and + arg.getType() instanceof JacksonTypeDescriptorType and + arg = sink.asExpr() + ) + } + + /** + * Holds if `fromNode` to `toNode` is a dataflow step that resolves a class + * or at least looks like resolving a class. + */ + override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) { + resolveClassStep(fromNode, toNode) or + looksLikeResolveClassStep(fromNode, toNode) + } +} + +/** + * Tracks flow from `enableDefaultTyping` calls to a subsequent Jackson deserialization method call. + */ +class EnableJacksonDefaultTypingConfig extends DataFlow2::Configuration { + EnableJacksonDefaultTypingConfig() { this = "EnableJacksonDefaultTypingConfig" } + + override predicate isSource(DataFlow::Node src) { + any(EnableJacksonDefaultTyping ma).getQualifier() = src.asExpr() + } + + override predicate isSink(DataFlow::Node sink) { sink instanceof ObjectMapperReadQualifier } +} + +/** + * Tracks flow from calls that set a type validator to a subsequent Jackson deserialization method call, + * including across builder method calls. + * + * Such a Jackson deserialization method call is safe because validation will likely prevent instantiating unexpected types. + */ +class SafeObjectMapperConfig extends DataFlow2::Configuration { + SafeObjectMapperConfig() { this = "SafeObjectMapperConfig" } + + override predicate isSource(DataFlow::Node src) { + src instanceof SetPolymorphicTypeValidatorSource + } + + override predicate isSink(DataFlow::Node sink) { sink instanceof ObjectMapperReadQualifier } + + /** + * Holds if `fromNode` to `toNode` is a dataflow step + * that configures or creates an `ObjectMapper` via a builder. + */ + override predicate isAdditionalFlowStep(DataFlow::Node fromNode, DataFlow::Node toNode) { + exists(MethodAccess ma, Method m | m = ma.getMethod() | + m.getDeclaringType() instanceof MapperBuilder and + m.getReturnType() + .(RefType) + .hasQualifiedName("com.fasterxml.jackson.databind.json", + ["JsonMapper$Builder", "JsonMapper"]) and + fromNode.asExpr() = ma.getQualifier() and + ma = toNode.asExpr() + ) + } +} diff --git a/java/ql/src/utils/GenerateFlowTestCase.py b/java/ql/src/utils/GenerateFlowTestCase.py index 07da614c6b8..757a94fe92d 100755 --- a/java/ql/src/utils/GenerateFlowTestCase.py +++ b/java/ql/src/utils/GenerateFlowTestCase.py @@ -12,8 +12,8 @@ import sys import tempfile if any(s == "--help" for s in sys.argv): - print("""Usage: -GenerateFlowTestCase.py specsToTest.csv projectPom.xml outdir + print("""Usage: +GenerateFlowTestCase.py specsToTest.csv projectPom.xml outdir [--force] This generates test cases exercising function model specifications found in specsToTest.csv producing files Test.java, test.ql and test.expected in outdir. @@ -22,32 +22,41 @@ projectPom.xml should be a Maven pom sufficient to resolve the classes named in Typically this means supplying a skeleton POM section that retrieves whatever jars contain the needed classes. +If --force is present, existing files may be overwritten. + Requirements: `mvn` and `codeql` should both appear on your path. After test generation completes, any lines in specsToTest.csv that didn't produce tests are output. If this happens, check the spelling of class and method names, and the syntax of input and output specifications. """) - sys.exit(0) + sys.exit(0) + +force = False +if "--force" in sys.argv: + sys.argv.remove("--force") + force = True if len(sys.argv) != 4: - print("Usage: GenerateFlowTestCase.py specsToTest.csv projectPom.xml outdir", file=sys.stderr) - print("specsToTest.csv should contain CSV rows describing method taint-propagation specifications to test", file=sys.stderr) - print("projectPom.xml should import dependencies sufficient to resolve the types used in specsToTest.csv", file=sys.stderr) - sys.exit(1) + print( + "Usage: GenerateFlowTestCase.py specsToTest.csv projectPom.xml outdir [--force]", file=sys.stderr) + print("specsToTest.csv should contain CSV rows describing method taint-propagation specifications to test", file=sys.stderr) + print("projectPom.xml should import dependencies sufficient to resolve the types used in specsToTest.csv", file=sys.stderr) + sys.exit(1) try: - os.makedirs(sys.argv[3]) + os.makedirs(sys.argv[3]) except Exception as e: - if e.errno != errno.EEXIST: - print("Failed to create output directory %s: %s" % (sys.argv[3], e)) - sys.exit(1) + if e.errno != errno.EEXIST: + print("Failed to create output directory %s: %s" % (sys.argv[3], e)) + sys.exit(1) resultJava = os.path.join(sys.argv[3], "Test.java") resultQl = os.path.join(sys.argv[3], "test.ql") -if os.path.exists(resultJava) or os.path.exists(resultQl): - print("Won't overwrite existing files '%s' or '%s'" % (resultJava, resultQl), file = sys.stderr) - sys.exit(1) +if not force and (os.path.exists(resultJava) or os.path.exists(resultQl)): + print("Won't overwrite existing files '%s' or '%s'" % + (resultJava, resultQl), file=sys.stderr) + sys.exit(1) workDir = tempfile.mkdtemp() @@ -57,129 +66,159 @@ projectDir = os.path.join(workDir, "mavenProject") os.makedirs(projectDir) try: - shutil.copyfile(sys.argv[2], os.path.join(projectDir, "pom.xml")) + shutil.copyfile(sys.argv[2], os.path.join(projectDir, "pom.xml")) except Exception as e: - print("Failed to read project POM %s: %s" % (sys.argv[2], e), file = sys.stderr) - sys.exit(1) + print("Failed to read project POM %s: %s" % + (sys.argv[2], e), file=sys.stderr) + sys.exit(1) commentRegex = re.compile("^\s*(//|#)") + + def isComment(s): - return commentRegex.match(s) is not None + return commentRegex.match(s) is not None + try: - with open(sys.argv[1], "r") as f: - specs = [l for l in f if not isComment(l)] + with open(sys.argv[1], "r") as f: + specs = [l for l in f if not isComment(l)] except Exception as e: - print("Failed to open %s: %s\n" % (sys.argv[1], e)) - sys.exit(1) + print("Failed to open %s: %s\n" % (sys.argv[1], e)) + sys.exit(1) projectTestPkgDir = os.path.join(projectDir, "src", "main", "java", "test") projectTestFile = os.path.join(projectTestPkgDir, "Test.java") os.makedirs(projectTestPkgDir) + def qualifiedOuterNameFromCsvRow(row): - cells = row.split(";") - if len(cells) < 2: - return None - return cells[0] + "." + cells[1].replace("$", ".") + cells = row.split(";") + if len(cells) < 2: + return None + return cells[0] + "." + cells[1].replace("$", ".") + with open(projectTestFile, "w") as testJava: - testJava.write("package test;\n\npublic class Test {\n\n") + testJava.write("package test;\n\npublic class Test {\n\n") - for i, spec in enumerate(specs): - outerName = qualifiedOuterNameFromCsvRow(spec) - if outerName is None: - print("A taint specification has the wrong format: should be 'package;classname;methodname....'", file = sys.stderr) - print("Mis-formatted row: " + spec, file = sys.stderr) - sys.exit(1) - testJava.write("\t%s obj%d = null;\n" % (outerName, i)) + for i, spec in enumerate(specs): + outerName = qualifiedOuterNameFromCsvRow(spec) + if outerName is None: + print("A taint specification has the wrong format: should be 'package;classname;methodname....'", file=sys.stderr) + print("Mis-formatted row: " + spec, file=sys.stderr) + sys.exit(1) + testJava.write("\t%s obj%d = null;\n" % (outerName, i)) - testJava.write("}") + testJava.write("}") print("Creating project database") cmd = ["codeql", "database", "create", "--language=java", "db"] -ret = subprocess.call(cmd, cwd = projectDir) +ret = subprocess.call(cmd, cwd=projectDir) if ret != 0: - print("Failed to create project database. Check that '%s' is a valid POM that pulls in all necessary dependencies, and '%s' specifies valid classes and methods." % (sys.argv[2], sys.argv[1]), file = sys.stderr) - print("Failed command was: %s (cwd: %s)" % (shlex.join(cmd), projectDir), file = sys.stderr) - sys.exit(1) + print("Failed to create project database. Check that '%s' is a valid POM that pulls in all necessary dependencies, and '%s' specifies valid classes and methods." % ( + sys.argv[2], sys.argv[1]), file=sys.stderr) + print("Failed command was: %s (cwd: %s)" % + (shlex.join(cmd), projectDir), file=sys.stderr) + sys.exit(1) print("Creating test-generation query") queryDir = os.path.join(workDir, "query") os.makedirs(queryDir) qlFile = os.path.join(queryDir, "gen.ql") with open(os.path.join(queryDir, "qlpack.yml"), "w") as f: - f.write("name: test-generation-query\nversion: 0.0.0\nlibraryPathDependencies: codeql-java") + f.write("name: test-generation-query\nversion: 0.0.0\nlibraryPathDependencies: codeql-java") with open(qlFile, "w") as f: - f.write("import java\nimport utils.GenerateFlowTestCase\n\nclass GenRow extends TargetSummaryModelCsv {\n\n\toverride predicate row(string r) {\n\t\tr = [\n") - f.write(",\n".join('\t\t\t"%s"' % spec.strip() for spec in specs)) - f.write("\n\t\t]\n\t}\n}\n") + f.write( + "import java\nimport utils.GenerateFlowTestCase\n\nclass GenRow extends TargetSummaryModelCsv {\n\n\toverride predicate row(string r) {\n\t\tr = [\n") + f.write(",\n".join('\t\t\t"%s"' % spec.strip() for spec in specs)) + f.write("\n\t\t]\n\t}\n}\n") print("Generating tests") generatedBqrs = os.path.join(queryDir, "out.bqrs") -cmd = ['codeql', 'query', 'run', qlFile, '--database', os.path.join(projectDir, "db"), '--output', generatedBqrs] +cmd = ['codeql', 'query', 'run', qlFile, '--database', + os.path.join(projectDir, "db"), '--output', generatedBqrs] ret = subprocess.call(cmd) if ret != 0: - print("Failed to generate tests. Failed command was: " + shlex.join(cmd)) - sys.exit(1) + print("Failed to generate tests. Failed command was: " + shlex.join(cmd)) + sys.exit(1) generatedJson = os.path.join(queryDir, "out.json") -cmd = ['codeql', 'bqrs', 'decode', generatedBqrs, '--format=json', '--output', generatedJson] +cmd = ['codeql', 'bqrs', 'decode', generatedBqrs, + '--format=json', '--output', generatedJson] ret = subprocess.call(cmd) if ret != 0: - print("Failed to decode BQRS. Failed command was: " + shlex.join(cmd)) - sys.exit(1) + print("Failed to decode BQRS. Failed command was: " + shlex.join(cmd)) + sys.exit(1) + def getTuples(queryName, jsonResult, fname): - if queryName not in jsonResult or "tuples" not in jsonResult[queryName]: - print("Failed to read generated tests: expected key '%s' with a 'tuples' subkey in file '%s'" % (queryName, fname), file = sys.stderr) - sys.exit(1) - return jsonResult[queryName]["tuples"] + if queryName not in jsonResult or "tuples" not in jsonResult[queryName]: + print("Failed to read generated tests: expected key '%s' with a 'tuples' subkey in file '%s'" % ( + queryName, fname), file=sys.stderr) + sys.exit(1) + return jsonResult[queryName]["tuples"] + with open(generatedJson, "r") as f: - generateOutput = json.load(f) - expectedTables = ("getTestCase", "getASupportMethodModel", "missingSummaryModelCsv", "getAParseFailure") + generateOutput = json.load(f) + expectedTables = ("getTestCase", "getASupportMethodModel", + "missingSummaryModelCsv", "getAParseFailure", "noTestCaseGenerated") - testCaseRows, supportModelRows, missingSummaryModelCsvRows, parseFailureRows = \ - tuple([getTuples(k, generateOutput, generatedJson) for k in expectedTables]) + testCaseRows, supportModelRows, missingSummaryModelCsvRows, parseFailureRows, noTestCaseGeneratedRows = \ + tuple([getTuples(k, generateOutput, generatedJson) + for k in expectedTables]) - if len(testCaseRows) != 1 or len(testCaseRows[0]) != 1: - print("Expected exactly one getTestCase result with one column (got: %s)" % json.dumps(testCaseRows), file = sys.stderr) - if any(len(row) != 1 for row in supportModelRows): - print("Expected exactly one column in getASupportMethodModel relation (got: %s)" % json.dumps(supportModelRows), file = sys.stderr) - if any(len(row) != 2 for row in parseFailureRows): - print("Expected exactly two columns in parseFailureRows relation (got: %s)" % json.dumps(parseFailureRows), file = sys.stderr) + if len(testCaseRows) != 1 or len(testCaseRows[0]) != 1: + print("Expected exactly one getTestCase result with one column (got: %s)" % + json.dumps(testCaseRows), file=sys.stderr) + if any(len(row) != 1 for row in supportModelRows): + print("Expected exactly one column in getASupportMethodModel relation (got: %s)" % + json.dumps(supportModelRows), file=sys.stderr) + if any(len(row) != 2 for row in parseFailureRows): + print("Expected exactly two columns in parseFailureRows relation (got: %s)" % + json.dumps(parseFailureRows), file=sys.stderr) + if any(len(row) != 1 for row in noTestCaseGeneratedRows): + print("Expected exactly one column in noTestCaseGenerated relation (got: %s)" % + json.dumps(noTestCaseGeneratedRows), file=sys.stderr) - if len(missingSummaryModelCsvRows) != 0: - print("Tests for some CSV rows were requested that were not in scope (SummaryModelCsv.row does not hold):\n" + "\n".join(r[0] for r in missingSummaryModelCsvRows)) - sys.exit(1) - if len(parseFailureRows) != 0: - print("The following rows failed to generate any test case. Check package, class and method name spelling, and argument and result specifications:\n%s" % "\n".join(r[0] + ": " + r[1] for r in parseFailureRows), file = sys.stderr) - sys.exit(1) + if len(missingSummaryModelCsvRows) != 0: + print("Tests for some CSV rows were requested that were not in scope (SummaryModelCsv.row does not hold):\n" + + "\n".join(r[0] for r in missingSummaryModelCsvRows)) + sys.exit(1) + if len(parseFailureRows) != 0: + print("The following rows failed to generate any test case. Check package, class and method name spelling, and argument and result specifications:\n%s" % + "\n".join(r[0] + ": " + r[1] for r in parseFailureRows), file=sys.stderr) + sys.exit(1) + if len(noTestCaseGeneratedRows) != 0: + print("The following CSV rows failed to generate any test case due to a limitation of the query. Other test cases will still be generated:\n" + + "\n".join(r[0] for r in noTestCaseGeneratedRows)) with open(resultJava, "w") as f: - f.write(generateOutput["getTestCase"]["tuples"][0][0]) + f.write(generateOutput["getTestCase"]["tuples"][0][0]) scriptPath = os.path.dirname(sys.argv[0]) + def copyfile(fromName, toFileHandle): - with open(os.path.join(scriptPath, fromName), "r") as fromFileHandle: - shutil.copyfileobj(fromFileHandle, toFileHandle) + with open(os.path.join(scriptPath, fromName), "r") as fromFileHandle: + shutil.copyfileobj(fromFileHandle, toFileHandle) + with open(resultQl, "w") as f: - copyfile("testHeader.qlfrag", f) - if len(supportModelRows) != 0: - copyfile("testModelsHeader.qlfrag", f) - f.write(", ".join('"%s"' % modelSpecRow[0].strip() for modelSpecRow in supportModelRows)) - copyfile("testModelsFooter.qlfrag", f) - copyfile("testFooter.qlfrag", f) + copyfile("testHeader.qlfrag", f) + if len(supportModelRows) != 0: + copyfile("testModelsHeader.qlfrag", f) + f.write(", ".join('"%s"' % + modelSpecRow[0].strip() for modelSpecRow in supportModelRows)) + copyfile("testModelsFooter.qlfrag", f) + copyfile("testFooter.qlfrag", f) # Make an empty .expected file, since this is an inline-exectations test with open(os.path.join(sys.argv[3], "test.expected"), "w"): - pass + pass cmd = ['codeql', 'query', 'format', '-qq', '-i', resultQl] subprocess.call(cmd) -shutil.rmtree(workDir) \ No newline at end of file +shutil.rmtree(workDir) diff --git a/java/ql/src/utils/GenerateFlowTestCase.qll b/java/ql/src/utils/GenerateFlowTestCase.qll index 91d7dae4318..0f7f0328f46 100644 --- a/java/ql/src/utils/GenerateFlowTestCase.qll +++ b/java/ql/src/utils/GenerateFlowTestCase.qll @@ -37,6 +37,9 @@ query string getAParseFailure(string reason) { any(TargetSummaryModelCsv target).row(result) and any(SummaryModelCsv model).row(result) and ( + not summaryModel(_, _, _, _, _, _, _, _, _, result) and + reason = "row could not be parsed" + or exists( string namespace, string type, boolean subtypes, string name, string signature, string ext | @@ -59,13 +62,26 @@ query string getAParseFailure(string reason) { ) } +/** + * Gets a CSV row for which a test was requested and was correctly parsed, + * but for which no test case could be generated due to a limitation of the query. + */ +query string noTestCaseGenerated() { + any(TargetSummaryModelCsv target).row(result) and + any(SummaryModelCsv model).row(result) and + not exists(getAParseFailure(_)) and + not exists(any(TestCase tc).getATestSnippetForRow(result)) +} + private class CallableToTest extends Callable { CallableToTest() { exists( string namespace, string type, boolean subtypes, string name, string signature, string ext | summaryModel(namespace, type, subtypes, name, signature, ext, _, _, _) and - this = interpretElement(namespace, type, subtypes, name, signature, ext) + this = interpretElement(namespace, type, subtypes, name, signature, ext) and + this.isPublic() and + getRootType(this.getDeclaringType()).isPublic() ) } } @@ -124,6 +140,21 @@ string getFieldToken(FieldContent fc) { fc.getField().getName() } +/** + * Returns a valid Java token naming the synthetic field `fc`, + * assuming that the name of that field consists only of characters valid in a Java identifier and `.`. + */ +string getSyntheticFieldToken(SyntheticFieldContent fc) { + exists(string name, int parts | + name = fc.getField() and + parts = count(name.splitAt(".")) + | + if parts = 1 + then result = name + else result = name.splitAt(".", parts - 2) + "_" + name.splitAt(".", parts - 1) + ) +} + /** * Returns a token suitable for incorporation into a Java method name describing content `c`. */ @@ -137,6 +168,8 @@ string contentToken(Content c) { c instanceof MapValueContent and result = "MapValue" or result = getFieldToken(c) + or + result = getSyntheticFieldToken(c) } /** @@ -145,7 +178,10 @@ string contentToken(Content c) { RefType getRootType(RefType t) { if t instanceof NestedType then result = getRootType(t.(NestedType).getEnclosingType()) - else result = t + else + if t instanceof Array + then result = getRootType(t.(Array).getElementType()) + else result = t } /** @@ -406,6 +442,8 @@ class TestCase extends TTestCase { content instanceof CollectionContent and result = "Element" or result = "Field[" + content.(FieldContent).getField().getQualifiedName() + "]" + or + result = "SyntheticField[" + content.(SyntheticFieldContent).getField() + "]" ) ) } @@ -482,18 +520,22 @@ predicate isImportable(Type t) { * if we cannot import it due to a name clash. */ string getShortNameIfPossible(Type t) { - getRootSourceDeclaration(t) = any(TestCase tc).getADesiredImport() and - if t instanceof RefType - then - exists(RefType replaced, string nestedName | - replaced = replaceTypeVariable(t).getSourceDeclaration() and - nestedName = replaced.nestedName().replaceAll("$", ".") - | - if isImportable(getRootSourceDeclaration(t)) - then result = nestedName - else result = replaced.getPackage().getName() + "." + nestedName - ) - else result = t.getName() + if t instanceof Array + then result = getShortNameIfPossible(t.(Array).getComponentType()) + "[]" + else ( + getRootSourceDeclaration(t) = any(TestCase tc).getADesiredImport() and + if t instanceof RefType + then + exists(RefType replaced, string nestedName | + replaced = replaceTypeVariable(t).getSourceDeclaration() and + nestedName = replaced.nestedName().replaceAll("$", ".") + | + if isImportable(getRootSourceDeclaration(t)) + then result = nestedName + else result = replaced.getPackage().getName() + "." + nestedName + ) + else result = t.getName() + ) } /** @@ -530,7 +572,7 @@ query string getTestCase() { result = "package generatedtest;\n\n" + concat(getAnImportStatement() + "\n") + "\n// Test case generated by GenerateFlowTestCase.ql\npublic class Test {\n\n" + - concat("\t" + getASupportMethod() + "\n") + "\n\tpublic void test() {\n\n" + + concat("\t" + getASupportMethod() + "\n") + "\n\tpublic void test() throws Exception {\n\n" + concat(string row, string snippet | snippet = any(TestCase tc).getATestSnippetForRow(row) | diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/MvelInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-094/MvelInjection.expected deleted file mode 100644 index 545da956f8e..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-094/MvelInjection.expected +++ /dev/null @@ -1,67 +0,0 @@ -edges -| MvelInjection.java:29:54:29:65 | read(...) : String | MvelInjection.java:30:28:30:37 | expression | -| MvelInjection.java:34:58:34:69 | read(...) : String | MvelInjection.java:36:5:36:13 | statement | -| MvelInjection.java:34:58:34:69 | read(...) : String | MvelInjection.java:37:5:37:13 | statement | -| MvelInjection.java:41:58:41:69 | read(...) : String | MvelInjection.java:43:5:43:14 | expression | -| MvelInjection.java:48:7:48:18 | read(...) : String | MvelInjection.java:49:5:49:14 | expression | -| MvelInjection.java:53:20:53:31 | read(...) : String | MvelInjection.java:57:5:57:18 | compiledScript | -| MvelInjection.java:53:20:53:31 | read(...) : String | MvelInjection.java:60:21:60:26 | script | -| MvelInjection.java:65:58:65:69 | read(...) : String | MvelInjection.java:68:5:68:10 | script | -| MvelInjection.java:77:40:77:51 | read(...) : String | MvelInjection.java:77:7:77:52 | compileTemplate(...) | -| MvelInjection.java:81:54:81:65 | read(...) : String | MvelInjection.java:82:29:82:46 | compile(...) | -| MvelInjection.java:86:58:86:69 | read(...) : String | MvelInjection.java:88:32:88:41 | expression | -| MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:94:15:94:16 | is : InputStream | -| MvelInjection.java:94:15:94:16 | is : InputStream | MvelInjection.java:94:23:94:27 | bytes [post update] : byte[] | -| MvelInjection.java:94:23:94:27 | bytes [post update] : byte[] | MvelInjection.java:95:14:95:36 | new String(...) : String | -| MvelInjection.java:95:14:95:36 | new String(...) : String | MvelInjection.java:25:15:25:26 | read(...) | -| MvelInjection.java:95:14:95:36 | new String(...) : String | MvelInjection.java:29:54:29:65 | read(...) : String | -| MvelInjection.java:95:14:95:36 | new String(...) : String | MvelInjection.java:34:58:34:69 | read(...) : String | -| MvelInjection.java:95:14:95:36 | new String(...) : String | MvelInjection.java:41:58:41:69 | read(...) : String | -| MvelInjection.java:95:14:95:36 | new String(...) : String | MvelInjection.java:48:7:48:18 | read(...) : String | -| MvelInjection.java:95:14:95:36 | new String(...) : String | MvelInjection.java:53:20:53:31 | read(...) : String | -| MvelInjection.java:95:14:95:36 | new String(...) : String | MvelInjection.java:65:58:65:69 | read(...) : String | -| MvelInjection.java:95:14:95:36 | new String(...) : String | MvelInjection.java:72:26:72:37 | read(...) | -| MvelInjection.java:95:14:95:36 | new String(...) : String | MvelInjection.java:77:40:77:51 | read(...) : String | -| MvelInjection.java:95:14:95:36 | new String(...) : String | MvelInjection.java:81:54:81:65 | read(...) : String | -| MvelInjection.java:95:14:95:36 | new String(...) : String | MvelInjection.java:86:58:86:69 | read(...) : String | -nodes -| MvelInjection.java:25:15:25:26 | read(...) | semmle.label | read(...) | -| MvelInjection.java:29:54:29:65 | read(...) : String | semmle.label | read(...) : String | -| MvelInjection.java:30:28:30:37 | expression | semmle.label | expression | -| MvelInjection.java:34:58:34:69 | read(...) : String | semmle.label | read(...) : String | -| MvelInjection.java:36:5:36:13 | statement | semmle.label | statement | -| MvelInjection.java:37:5:37:13 | statement | semmle.label | statement | -| MvelInjection.java:41:58:41:69 | read(...) : String | semmle.label | read(...) : String | -| MvelInjection.java:43:5:43:14 | expression | semmle.label | expression | -| MvelInjection.java:48:7:48:18 | read(...) : String | semmle.label | read(...) : String | -| MvelInjection.java:49:5:49:14 | expression | semmle.label | expression | -| MvelInjection.java:53:20:53:31 | read(...) : String | semmle.label | read(...) : String | -| MvelInjection.java:57:5:57:18 | compiledScript | semmle.label | compiledScript | -| MvelInjection.java:60:21:60:26 | script | semmle.label | script | -| MvelInjection.java:65:58:65:69 | read(...) : String | semmle.label | read(...) : String | -| MvelInjection.java:68:5:68:10 | script | semmle.label | script | -| MvelInjection.java:72:26:72:37 | read(...) | semmle.label | read(...) | -| MvelInjection.java:77:7:77:52 | compileTemplate(...) | semmle.label | compileTemplate(...) | -| MvelInjection.java:77:40:77:51 | read(...) : String | semmle.label | read(...) : String | -| MvelInjection.java:81:54:81:65 | read(...) : String | semmle.label | read(...) : String | -| MvelInjection.java:82:29:82:46 | compile(...) | semmle.label | compile(...) | -| MvelInjection.java:86:58:86:69 | read(...) : String | semmle.label | read(...) : String | -| MvelInjection.java:88:32:88:41 | expression | semmle.label | expression | -| MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| MvelInjection.java:94:15:94:16 | is : InputStream | semmle.label | is : InputStream | -| MvelInjection.java:94:23:94:27 | bytes [post update] : byte[] | semmle.label | bytes [post update] : byte[] | -| MvelInjection.java:95:14:95:36 | new String(...) : String | semmle.label | new String(...) : String | -#select -| MvelInjection.java:25:15:25:26 | read(...) | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:25:15:25:26 | read(...) | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | -| MvelInjection.java:30:28:30:37 | expression | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:30:28:30:37 | expression | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | -| MvelInjection.java:36:5:36:13 | statement | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:36:5:36:13 | statement | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | -| MvelInjection.java:37:5:37:13 | statement | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:37:5:37:13 | statement | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | -| MvelInjection.java:43:5:43:14 | expression | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:43:5:43:14 | expression | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | -| MvelInjection.java:49:5:49:14 | expression | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:49:5:49:14 | expression | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | -| MvelInjection.java:57:5:57:18 | compiledScript | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:57:5:57:18 | compiledScript | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | -| MvelInjection.java:60:21:60:26 | script | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:60:21:60:26 | script | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | -| MvelInjection.java:68:5:68:10 | script | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:68:5:68:10 | script | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | -| MvelInjection.java:72:26:72:37 | read(...) | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:72:26:72:37 | read(...) | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | -| MvelInjection.java:77:7:77:52 | compileTemplate(...) | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:77:7:77:52 | compileTemplate(...) | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | -| MvelInjection.java:82:29:82:46 | compile(...) | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:82:29:82:46 | compile(...) | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | -| MvelInjection.java:88:32:88:41 | expression | MvelInjection.java:92:27:92:49 | getInputStream(...) : InputStream | MvelInjection.java:88:32:88:41 | expression | MVEL injection from $@. | MvelInjection.java:92:27:92:49 | getInputStream(...) | this user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/MvelInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-094/MvelInjection.qlref deleted file mode 100644 index 13d7cbd2295..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-094/MvelInjection.qlref +++ /dev/null @@ -1 +0,0 @@ -experimental/Security/CWE/CWE-094/MvelInjection.ql \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/options b/java/ql/test/experimental/query-tests/security/CWE-094/options index 1e69caed264..606b83a6dcc 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-094/options +++ b/java/ql/test/experimental/query-tests/security/CWE-094/options @@ -1 +1 @@ -//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.3.8:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2:${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jython-2.7.2:${testdir}/../../../../experimental/stubs/rhino-1.7.13:${testdir}/../../../../stubs/bsh-2.0b5:${testdir}/../../../../experimental/stubs/jshell +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.3.8:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2:${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jython-2.7.2:${testdir}/../../../../experimental/stubs/rhino-1.7.13:${testdir}/../../../../stubs/bsh-2.0b5:${testdir}/../../../../experimental/stubs/jshell diff --git a/java/ql/test/experimental/query-tests/security/CWE-470/UnsafeReflection.expected b/java/ql/test/experimental/query-tests/security/CWE-470/UnsafeReflection.expected new file mode 100644 index 00000000000..d38152c82b5 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-470/UnsafeReflection.expected @@ -0,0 +1,57 @@ +edges +| UnsafeReflection.java:21:28:21:60 | getParameter(...) : String | UnsafeReflection.java:25:29:25:59 | getDeclaredConstructors(...) : Constructor[] | +| UnsafeReflection.java:21:28:21:60 | getParameter(...) : String | UnsafeReflection.java:25:29:25:62 | ...[...] | +| UnsafeReflection.java:22:33:22:70 | getParameter(...) : String | UnsafeReflection.java:25:76:25:89 | parameterValue | +| UnsafeReflection.java:25:29:25:59 | getDeclaredConstructors(...) : Constructor[] | UnsafeReflection.java:25:29:25:62 | ...[...] | +| UnsafeReflection.java:33:28:33:60 | getParameter(...) : String | UnsafeReflection.java:39:13:39:38 | getDeclaredMethods(...) : Method[] | +| UnsafeReflection.java:33:28:33:60 | getParameter(...) : String | UnsafeReflection.java:39:13:39:41 | ...[...] | +| UnsafeReflection.java:33:28:33:60 | getParameter(...) : String | UnsafeReflection.java:39:50:39:55 | object | +| UnsafeReflection.java:34:33:34:70 | getParameter(...) : String | UnsafeReflection.java:39:58:39:71 | parameterValue | +| UnsafeReflection.java:39:13:39:38 | getDeclaredMethods(...) : Method[] | UnsafeReflection.java:39:13:39:41 | ...[...] | +| UnsafeReflection.java:46:24:46:82 | beanIdOrClassName : String | UnsafeReflection.java:53:30:53:46 | beanIdOrClassName : String | +| UnsafeReflection.java:46:132:46:168 | body : Map | UnsafeReflection.java:49:37:49:40 | body : Map | +| UnsafeReflection.java:49:23:49:59 | (...)... : Object | UnsafeReflection.java:53:67:53:73 | rawData : Object | +| UnsafeReflection.java:49:37:49:40 | body : Map | UnsafeReflection.java:49:37:49:59 | get(...) : Object | +| UnsafeReflection.java:49:37:49:59 | get(...) : Object | UnsafeReflection.java:49:23:49:59 | (...)... : Object | +| UnsafeReflection.java:53:30:53:46 | beanIdOrClassName : String | UnsafeReflection.java:104:34:104:57 | beanIdOrClassName : String | +| UnsafeReflection.java:53:67:53:73 | rawData : Object | UnsafeReflection.java:104:102:104:118 | data : Object | +| UnsafeReflection.java:62:33:62:70 | getParameter(...) : String | UnsafeReflection.java:68:76:68:89 | parameterValue | +| UnsafeReflection.java:77:33:77:70 | getParameter(...) : String | UnsafeReflection.java:83:76:83:89 | parameterValue | +| UnsafeReflection.java:92:33:92:70 | getParameter(...) : String | UnsafeReflection.java:98:76:98:89 | parameterValue | +| UnsafeReflection.java:104:34:104:57 | beanIdOrClassName : String | UnsafeReflection.java:119:21:119:26 | method | +| UnsafeReflection.java:104:34:104:57 | beanIdOrClassName : String | UnsafeReflection.java:119:35:119:38 | bean | +| UnsafeReflection.java:104:102:104:118 | data : Object | UnsafeReflection.java:119:41:119:44 | data | +nodes +| UnsafeReflection.java:21:28:21:60 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| UnsafeReflection.java:22:33:22:70 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| UnsafeReflection.java:25:29:25:59 | getDeclaredConstructors(...) : Constructor[] | semmle.label | getDeclaredConstructors(...) : Constructor[] | +| UnsafeReflection.java:25:29:25:62 | ...[...] | semmle.label | ...[...] | +| UnsafeReflection.java:25:76:25:89 | parameterValue | semmle.label | parameterValue | +| UnsafeReflection.java:33:28:33:60 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| UnsafeReflection.java:34:33:34:70 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| UnsafeReflection.java:39:13:39:38 | getDeclaredMethods(...) : Method[] | semmle.label | getDeclaredMethods(...) : Method[] | +| UnsafeReflection.java:39:13:39:41 | ...[...] | semmle.label | ...[...] | +| UnsafeReflection.java:39:50:39:55 | object | semmle.label | object | +| UnsafeReflection.java:39:58:39:71 | parameterValue | semmle.label | parameterValue | +| UnsafeReflection.java:46:24:46:82 | beanIdOrClassName : String | semmle.label | beanIdOrClassName : String | +| UnsafeReflection.java:46:132:46:168 | body : Map | semmle.label | body : Map | +| UnsafeReflection.java:49:23:49:59 | (...)... : Object | semmle.label | (...)... : Object | +| UnsafeReflection.java:49:37:49:40 | body : Map | semmle.label | body : Map | +| UnsafeReflection.java:49:37:49:59 | get(...) : Object | semmle.label | get(...) : Object | +| UnsafeReflection.java:53:30:53:46 | beanIdOrClassName : String | semmle.label | beanIdOrClassName : String | +| UnsafeReflection.java:53:67:53:73 | rawData : Object | semmle.label | rawData : Object | +| UnsafeReflection.java:62:33:62:70 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| UnsafeReflection.java:68:76:68:89 | parameterValue | semmle.label | parameterValue | +| UnsafeReflection.java:77:33:77:70 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| UnsafeReflection.java:83:76:83:89 | parameterValue | semmle.label | parameterValue | +| UnsafeReflection.java:92:33:92:70 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| UnsafeReflection.java:98:76:98:89 | parameterValue | semmle.label | parameterValue | +| UnsafeReflection.java:104:34:104:57 | beanIdOrClassName : String | semmle.label | beanIdOrClassName : String | +| UnsafeReflection.java:104:102:104:118 | data : Object | semmle.label | data : Object | +| UnsafeReflection.java:119:21:119:26 | method | semmle.label | method | +| UnsafeReflection.java:119:35:119:38 | bean | semmle.label | bean | +| UnsafeReflection.java:119:41:119:44 | data | semmle.label | data | +#select +| UnsafeReflection.java:25:29:25:62 | ...[...] | UnsafeReflection.java:21:28:21:60 | getParameter(...) : String | UnsafeReflection.java:25:29:25:62 | ...[...] | Unsafe reflection of $@. | UnsafeReflection.java:21:28:21:60 | getParameter(...) | user input | +| UnsafeReflection.java:39:13:39:41 | ...[...] | UnsafeReflection.java:33:28:33:60 | getParameter(...) : String | UnsafeReflection.java:39:13:39:41 | ...[...] | Unsafe reflection of $@. | UnsafeReflection.java:33:28:33:60 | getParameter(...) | user input | +| UnsafeReflection.java:119:21:119:26 | method | UnsafeReflection.java:46:24:46:82 | beanIdOrClassName : String | UnsafeReflection.java:119:21:119:26 | method | Unsafe reflection of $@. | UnsafeReflection.java:46:24:46:82 | beanIdOrClassName | user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-470/UnsafeReflection.java b/java/ql/test/experimental/query-tests/security/CWE-470/UnsafeReflection.java new file mode 100644 index 00000000000..d9dc0573660 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-470/UnsafeReflection.java @@ -0,0 +1,144 @@ +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.multipart.MultipartFile; + +@Controller +public class UnsafeReflection { + + @GetMapping(value = "uf1") + public void bad1(HttpServletRequest request) { + String className = request.getParameter("className"); + String parameterValue = request.getParameter("parameterValue"); + try { + Class clazz = Class.forName(className); + Object object = clazz.getDeclaredConstructors()[0].newInstance(parameterValue); //bad + } catch (Exception e) { + e.printStackTrace(); + } + } + + @GetMapping(value = "uf2") + public void bad2(HttpServletRequest request) { + String className = request.getParameter("className"); + String parameterValue = request.getParameter("parameterValue"); + try { + ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + Class clazz = classLoader.loadClass(className); + Object object = clazz.newInstance(); + clazz.getDeclaredMethods()[0].invoke(object, parameterValue); //bad + } catch (Exception e) { + e.printStackTrace(); + } + } + + @RequestMapping(value = {"/service/{beanIdOrClassName}/{methodName}"}, method = {RequestMethod.POST}, consumes = {"application/json"}, produces = {"application/json"}) + public Object bad3(@PathVariable("beanIdOrClassName") String beanIdOrClassName, @PathVariable("methodName") String methodName, @RequestBody Map body) throws Exception { + List rawData = null; + try { + rawData = (List)body.get("methodInput"); + } catch (Exception e) { + return e; + } + return invokeService(beanIdOrClassName, methodName, null, rawData); + } + + @GetMapping(value = "uf3") + public void good1(HttpServletRequest request) throws Exception { + HashSet hashSet = new HashSet<>(); + hashSet.add("com.example.test1"); + hashSet.add("com.example.test2"); + String className = request.getParameter("className"); + String parameterValue = request.getParameter("parameterValue"); + if (!hashSet.contains(className)){ + throw new Exception("Class not valid: " + className); + } + try { + Class clazz = Class.forName(className); + Object object = clazz.getDeclaredConstructors()[0].newInstance(parameterValue); //good + } catch (Exception e) { + e.printStackTrace(); + } + } + + @GetMapping(value = "uf4") + public void good2(HttpServletRequest request) throws Exception { + String className = request.getParameter("className"); + String parameterValue = request.getParameter("parameterValue"); + if (!"com.example.test1".equals(className)){ + throw new Exception("Class not valid: " + className); + } + try { + Class clazz = Class.forName(className); + Object object = clazz.getDeclaredConstructors()[0].newInstance(parameterValue); //good + } catch (Exception e) { + e.printStackTrace(); + } + } + + @GetMapping(value = "uf5") + public void good3(HttpServletRequest request) throws Exception { + String className = request.getParameter("className"); + String parameterValue = request.getParameter("parameterValue"); + if (!className.equals("com.example.test1")){ //good + throw new Exception("Class not valid: " + className); + } + try { + Class clazz = Class.forName(className); + Object object = clazz.getDeclaredConstructors()[0].newInstance(parameterValue); //good + } catch (Exception e) { + e.printStackTrace(); + } + } + + private Object invokeService(String beanIdOrClassName, String methodName, MultipartFile[] files, List data) throws Exception { + BeanFactory beanFactory = new BeanFactory(); + try { + Object bean = null; + Class beanClass = Class.forName(beanIdOrClassName); + bean = beanFactory.getBean(beanClass); + byte b; + int i; + Method[] arrayOfMethod; + for (i = (arrayOfMethod = bean.getClass().getMethods()).length, b = 0; b < i; ) { + Method method = arrayOfMethod[b]; + if (!method.getName().equals(methodName)) { + b++; + continue; + } + Object result = method.invoke(bean, data); + Map map = new HashMap<>(); + return map; + } + } catch (Exception e) { + return e; + } + return null; + } +} + +class BeanFactory { + + private static HashMap classNameMap = new HashMap<>(); + + private static HashMap, Object> classMap = new HashMap<>(); + + static { + classNameMap.put("xxxx", Runtime.getRuntime()); + classMap.put(Runtime.class, Runtime.getRuntime()); + } + + public Object getBean(Class clzz) { + return classMap.get(clzz); + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-470/UnsafeReflection.qlref b/java/ql/test/experimental/query-tests/security/CWE-470/UnsafeReflection.qlref new file mode 100644 index 00000000000..26de90ac7fa --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-470/UnsafeReflection.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-470/UnsafeReflection.ql diff --git a/java/ql/test/experimental/query-tests/security/CWE-470/options b/java/ql/test/experimental/query-tests/security/CWE-470/options new file mode 100644 index 00000000000..ba166b547a0 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-470/options @@ -0,0 +1 @@ +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/springframework-5.3.8/ \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected index bcf5e892e1b..61213583d8d 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected @@ -1,35 +1,90 @@ edges -| SpringUrlRedirect.java:13:30:13:47 | redirectUrl : String | SpringUrlRedirect.java:15:19:15:29 | redirectUrl | -| SpringUrlRedirect.java:20:24:20:41 | redirectUrl : String | SpringUrlRedirect.java:21:36:21:46 | redirectUrl | -| SpringUrlRedirect.java:26:30:26:47 | redirectUrl : String | SpringUrlRedirect.java:27:44:27:54 | redirectUrl | -| SpringUrlRedirect.java:32:30:32:47 | redirectUrl : String | SpringUrlRedirect.java:33:47:33:57 | redirectUrl | -| SpringUrlRedirect.java:37:24:37:41 | redirectUrl : String | SpringUrlRedirect.java:40:29:40:39 | redirectUrl | -| SpringUrlRedirect.java:45:24:45:41 | redirectUrl : String | SpringUrlRedirect.java:48:30:48:40 | redirectUrl | -| SpringUrlRedirect.java:53:24:53:41 | redirectUrl : String | SpringUrlRedirect.java:54:30:54:66 | format(...) | -| SpringUrlRedirect.java:58:24:58:41 | redirectUrl : String | SpringUrlRedirect.java:59:30:59:76 | format(...) | +| SpringUrlRedirect.java:17:30:17:47 | redirectUrl : String | SpringUrlRedirect.java:19:19:19:29 | redirectUrl | +| SpringUrlRedirect.java:24:24:24:41 | redirectUrl : String | SpringUrlRedirect.java:25:36:25:46 | redirectUrl | +| SpringUrlRedirect.java:30:30:30:47 | redirectUrl : String | SpringUrlRedirect.java:31:44:31:54 | redirectUrl | +| SpringUrlRedirect.java:36:30:36:47 | redirectUrl : String | SpringUrlRedirect.java:37:47:37:57 | redirectUrl | +| SpringUrlRedirect.java:41:24:41:41 | redirectUrl : String | SpringUrlRedirect.java:44:29:44:39 | redirectUrl | +| SpringUrlRedirect.java:49:24:49:41 | redirectUrl : String | SpringUrlRedirect.java:52:30:52:40 | redirectUrl | +| SpringUrlRedirect.java:57:24:57:41 | redirectUrl : String | SpringUrlRedirect.java:58:30:58:66 | format(...) | +| SpringUrlRedirect.java:62:24:62:41 | redirectUrl : String | SpringUrlRedirect.java:63:30:63:76 | format(...) | +| SpringUrlRedirect.java:89:38:89:55 | redirectUrl : String | SpringUrlRedirect.java:91:38:91:48 | redirectUrl : String | +| SpringUrlRedirect.java:91:38:91:48 | redirectUrl : String | SpringUrlRedirect.java:91:27:91:49 | create(...) | +| SpringUrlRedirect.java:96:39:96:56 | redirectUrl : String | SpringUrlRedirect.java:98:44:98:54 | redirectUrl : String | +| SpringUrlRedirect.java:98:33:98:55 | create(...) : URI | SpringUrlRedirect.java:100:37:100:47 | httpHeaders | +| SpringUrlRedirect.java:98:44:98:54 | redirectUrl : String | SpringUrlRedirect.java:98:33:98:55 | create(...) : URI | +| SpringUrlRedirect.java:104:39:104:56 | redirectUrl : String | SpringUrlRedirect.java:106:37:106:47 | redirectUrl : String | +| SpringUrlRedirect.java:106:9:106:19 | httpHeaders [post update] : HttpHeaders | SpringUrlRedirect.java:108:68:108:78 | httpHeaders | +| SpringUrlRedirect.java:106:9:106:19 | httpHeaders [post update] [, ] : String | SpringUrlRedirect.java:108:68:108:78 | httpHeaders | +| SpringUrlRedirect.java:106:37:106:47 | redirectUrl : String | SpringUrlRedirect.java:106:9:106:19 | httpHeaders [post update] : HttpHeaders | +| SpringUrlRedirect.java:106:37:106:47 | redirectUrl : String | SpringUrlRedirect.java:106:9:106:19 | httpHeaders [post update] [, ] : String | +| SpringUrlRedirect.java:112:39:112:56 | redirectUrl : String | SpringUrlRedirect.java:114:37:114:47 | redirectUrl : String | +| SpringUrlRedirect.java:114:9:114:19 | httpHeaders [post update] : HttpHeaders | SpringUrlRedirect.java:116:37:116:47 | httpHeaders | +| SpringUrlRedirect.java:114:9:114:19 | httpHeaders [post update] [, ] : String | SpringUrlRedirect.java:116:37:116:47 | httpHeaders | +| SpringUrlRedirect.java:114:37:114:47 | redirectUrl : String | SpringUrlRedirect.java:114:9:114:19 | httpHeaders [post update] : HttpHeaders | +| SpringUrlRedirect.java:114:37:114:47 | redirectUrl : String | SpringUrlRedirect.java:114:9:114:19 | httpHeaders [post update] [, ] : String | +| SpringUrlRedirect.java:120:33:120:50 | redirectUrl : String | SpringUrlRedirect.java:122:37:122:47 | redirectUrl : String | +| SpringUrlRedirect.java:122:9:122:19 | httpHeaders [post update] : HttpHeaders | SpringUrlRedirect.java:124:49:124:59 | httpHeaders | +| SpringUrlRedirect.java:122:9:122:19 | httpHeaders [post update] [, ] : String | SpringUrlRedirect.java:124:49:124:59 | httpHeaders | +| SpringUrlRedirect.java:122:37:122:47 | redirectUrl : String | SpringUrlRedirect.java:122:9:122:19 | httpHeaders [post update] : HttpHeaders | +| SpringUrlRedirect.java:122:37:122:47 | redirectUrl : String | SpringUrlRedirect.java:122:9:122:19 | httpHeaders [post update] [, ] : String | +| SpringUrlRedirect.java:128:33:128:50 | redirectUrl : String | SpringUrlRedirect.java:130:44:130:54 | redirectUrl : String | +| SpringUrlRedirect.java:130:33:130:55 | create(...) : URI | SpringUrlRedirect.java:132:49:132:59 | httpHeaders | +| SpringUrlRedirect.java:130:44:130:54 | redirectUrl : String | SpringUrlRedirect.java:130:33:130:55 | create(...) : URI | nodes -| SpringUrlRedirect.java:13:30:13:47 | redirectUrl : String | semmle.label | redirectUrl : String | -| SpringUrlRedirect.java:15:19:15:29 | redirectUrl | semmle.label | redirectUrl | -| SpringUrlRedirect.java:20:24:20:41 | redirectUrl : String | semmle.label | redirectUrl : String | -| SpringUrlRedirect.java:21:36:21:46 | redirectUrl | semmle.label | redirectUrl | -| SpringUrlRedirect.java:26:30:26:47 | redirectUrl : String | semmle.label | redirectUrl : String | -| SpringUrlRedirect.java:27:44:27:54 | redirectUrl | semmle.label | redirectUrl | -| SpringUrlRedirect.java:32:30:32:47 | redirectUrl : String | semmle.label | redirectUrl : String | -| SpringUrlRedirect.java:33:47:33:57 | redirectUrl | semmle.label | redirectUrl | -| SpringUrlRedirect.java:37:24:37:41 | redirectUrl : String | semmle.label | redirectUrl : String | -| SpringUrlRedirect.java:40:29:40:39 | redirectUrl | semmle.label | redirectUrl | -| SpringUrlRedirect.java:45:24:45:41 | redirectUrl : String | semmle.label | redirectUrl : String | -| SpringUrlRedirect.java:48:30:48:40 | redirectUrl | semmle.label | redirectUrl | -| SpringUrlRedirect.java:53:24:53:41 | redirectUrl : String | semmle.label | redirectUrl : String | -| SpringUrlRedirect.java:54:30:54:66 | format(...) | semmle.label | format(...) | -| SpringUrlRedirect.java:58:24:58:41 | redirectUrl : String | semmle.label | redirectUrl : String | -| SpringUrlRedirect.java:59:30:59:76 | format(...) | semmle.label | format(...) | +| SpringUrlRedirect.java:17:30:17:47 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:19:19:19:29 | redirectUrl | semmle.label | redirectUrl | +| SpringUrlRedirect.java:24:24:24:41 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:25:36:25:46 | redirectUrl | semmle.label | redirectUrl | +| SpringUrlRedirect.java:30:30:30:47 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:31:44:31:54 | redirectUrl | semmle.label | redirectUrl | +| SpringUrlRedirect.java:36:30:36:47 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:37:47:37:57 | redirectUrl | semmle.label | redirectUrl | +| SpringUrlRedirect.java:41:24:41:41 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:44:29:44:39 | redirectUrl | semmle.label | redirectUrl | +| SpringUrlRedirect.java:49:24:49:41 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:52:30:52:40 | redirectUrl | semmle.label | redirectUrl | +| SpringUrlRedirect.java:57:24:57:41 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:58:30:58:66 | format(...) | semmle.label | format(...) | +| SpringUrlRedirect.java:62:24:62:41 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:63:30:63:76 | format(...) | semmle.label | format(...) | +| SpringUrlRedirect.java:89:38:89:55 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:91:27:91:49 | create(...) | semmle.label | create(...) | +| SpringUrlRedirect.java:91:38:91:48 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:96:39:96:56 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:98:33:98:55 | create(...) : URI | semmle.label | create(...) : URI | +| SpringUrlRedirect.java:98:44:98:54 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:100:37:100:47 | httpHeaders | semmle.label | httpHeaders | +| SpringUrlRedirect.java:104:39:104:56 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:106:9:106:19 | httpHeaders [post update] : HttpHeaders | semmle.label | httpHeaders [post update] : HttpHeaders | +| SpringUrlRedirect.java:106:9:106:19 | httpHeaders [post update] [, ] : String | semmle.label | httpHeaders [post update] [, ] : String | +| SpringUrlRedirect.java:106:37:106:47 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:108:68:108:78 | httpHeaders | semmle.label | httpHeaders | +| SpringUrlRedirect.java:112:39:112:56 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:114:9:114:19 | httpHeaders [post update] : HttpHeaders | semmle.label | httpHeaders [post update] : HttpHeaders | +| SpringUrlRedirect.java:114:9:114:19 | httpHeaders [post update] [, ] : String | semmle.label | httpHeaders [post update] [, ] : String | +| SpringUrlRedirect.java:114:37:114:47 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:116:37:116:47 | httpHeaders | semmle.label | httpHeaders | +| SpringUrlRedirect.java:120:33:120:50 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:122:9:122:19 | httpHeaders [post update] : HttpHeaders | semmle.label | httpHeaders [post update] : HttpHeaders | +| SpringUrlRedirect.java:122:9:122:19 | httpHeaders [post update] [, ] : String | semmle.label | httpHeaders [post update] [, ] : String | +| SpringUrlRedirect.java:122:37:122:47 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:124:49:124:59 | httpHeaders | semmle.label | httpHeaders | +| SpringUrlRedirect.java:128:33:128:50 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:130:33:130:55 | create(...) : URI | semmle.label | create(...) : URI | +| SpringUrlRedirect.java:130:44:130:54 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:132:49:132:59 | httpHeaders | semmle.label | httpHeaders | #select -| SpringUrlRedirect.java:15:19:15:29 | redirectUrl | SpringUrlRedirect.java:13:30:13:47 | redirectUrl : String | SpringUrlRedirect.java:15:19:15:29 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:13:30:13:47 | redirectUrl | user-provided value | -| SpringUrlRedirect.java:21:36:21:46 | redirectUrl | SpringUrlRedirect.java:20:24:20:41 | redirectUrl : String | SpringUrlRedirect.java:21:36:21:46 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:20:24:20:41 | redirectUrl | user-provided value | -| SpringUrlRedirect.java:27:44:27:54 | redirectUrl | SpringUrlRedirect.java:26:30:26:47 | redirectUrl : String | SpringUrlRedirect.java:27:44:27:54 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:26:30:26:47 | redirectUrl | user-provided value | -| SpringUrlRedirect.java:33:47:33:57 | redirectUrl | SpringUrlRedirect.java:32:30:32:47 | redirectUrl : String | SpringUrlRedirect.java:33:47:33:57 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:32:30:32:47 | redirectUrl | user-provided value | -| SpringUrlRedirect.java:40:29:40:39 | redirectUrl | SpringUrlRedirect.java:37:24:37:41 | redirectUrl : String | SpringUrlRedirect.java:40:29:40:39 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:37:24:37:41 | redirectUrl | user-provided value | -| SpringUrlRedirect.java:48:30:48:40 | redirectUrl | SpringUrlRedirect.java:45:24:45:41 | redirectUrl : String | SpringUrlRedirect.java:48:30:48:40 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:45:24:45:41 | redirectUrl | user-provided value | -| SpringUrlRedirect.java:54:30:54:66 | format(...) | SpringUrlRedirect.java:53:24:53:41 | redirectUrl : String | SpringUrlRedirect.java:54:30:54:66 | format(...) | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:53:24:53:41 | redirectUrl | user-provided value | -| SpringUrlRedirect.java:59:30:59:76 | format(...) | SpringUrlRedirect.java:58:24:58:41 | redirectUrl : String | SpringUrlRedirect.java:59:30:59:76 | format(...) | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:58:24:58:41 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:19:19:19:29 | redirectUrl | SpringUrlRedirect.java:17:30:17:47 | redirectUrl : String | SpringUrlRedirect.java:19:19:19:29 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:17:30:17:47 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:25:36:25:46 | redirectUrl | SpringUrlRedirect.java:24:24:24:41 | redirectUrl : String | SpringUrlRedirect.java:25:36:25:46 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:24:24:24:41 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:31:44:31:54 | redirectUrl | SpringUrlRedirect.java:30:30:30:47 | redirectUrl : String | SpringUrlRedirect.java:31:44:31:54 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:30:30:30:47 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:37:47:37:57 | redirectUrl | SpringUrlRedirect.java:36:30:36:47 | redirectUrl : String | SpringUrlRedirect.java:37:47:37:57 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:36:30:36:47 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:44:29:44:39 | redirectUrl | SpringUrlRedirect.java:41:24:41:41 | redirectUrl : String | SpringUrlRedirect.java:44:29:44:39 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:41:24:41:41 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:52:30:52:40 | redirectUrl | SpringUrlRedirect.java:49:24:49:41 | redirectUrl : String | SpringUrlRedirect.java:52:30:52:40 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:49:24:49:41 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:58:30:58:66 | format(...) | SpringUrlRedirect.java:57:24:57:41 | redirectUrl : String | SpringUrlRedirect.java:58:30:58:66 | format(...) | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:57:24:57:41 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:63:30:63:76 | format(...) | SpringUrlRedirect.java:62:24:62:41 | redirectUrl : String | SpringUrlRedirect.java:63:30:63:76 | format(...) | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:62:24:62:41 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:91:27:91:49 | create(...) | SpringUrlRedirect.java:89:38:89:55 | redirectUrl : String | SpringUrlRedirect.java:91:27:91:49 | create(...) | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:89:38:89:55 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:100:37:100:47 | httpHeaders | SpringUrlRedirect.java:96:39:96:56 | redirectUrl : String | SpringUrlRedirect.java:100:37:100:47 | httpHeaders | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:96:39:96:56 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:108:68:108:78 | httpHeaders | SpringUrlRedirect.java:104:39:104:56 | redirectUrl : String | SpringUrlRedirect.java:108:68:108:78 | httpHeaders | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:104:39:104:56 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:116:37:116:47 | httpHeaders | SpringUrlRedirect.java:112:39:112:56 | redirectUrl : String | SpringUrlRedirect.java:116:37:116:47 | httpHeaders | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:112:39:112:56 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:124:49:124:59 | httpHeaders | SpringUrlRedirect.java:120:33:120:50 | redirectUrl : String | SpringUrlRedirect.java:124:49:124:59 | httpHeaders | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:120:33:120:50 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:132:49:132:59 | httpHeaders | SpringUrlRedirect.java:128:33:128:50 | redirectUrl : String | SpringUrlRedirect.java:132:49:132:59 | httpHeaders | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:128:33:128:50 | redirectUrl | user-provided value | diff --git a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java index f3958cba102..e5909b3478e 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java +++ b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java @@ -3,6 +3,10 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; +import org.springframework.http.ResponseEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import java.net.URI; @Controller public class SpringUrlRedirect { @@ -80,4 +84,51 @@ public class SpringUrlRedirect { public String good3(String status) { return "redirect:" + String.format("/stories/search/criteria?status=%s", status); } + + @GetMapping("url12") + public ResponseEntity bad9(String redirectUrl) { + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(redirectUrl)) + .build(); + } + + @GetMapping("url13") + public ResponseEntity bad10(String redirectUrl) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setLocation(URI.create(redirectUrl)); + + return new ResponseEntity<>(httpHeaders, HttpStatus.SEE_OTHER); + } + + @GetMapping("url14") + public ResponseEntity bad11(String redirectUrl) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add("Location", redirectUrl); + + return ResponseEntity.status(HttpStatus.SEE_OTHER).headers(httpHeaders).build(); + } + + @GetMapping("url15") + public ResponseEntity bad12(String redirectUrl) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add("Location", redirectUrl); + + return new ResponseEntity<>(httpHeaders, HttpStatus.SEE_OTHER); + } + + @GetMapping("url16") + public ResponseEntity bad13(String redirectUrl) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add("Location", redirectUrl); + + return new ResponseEntity<>("TestBody", httpHeaders, HttpStatus.SEE_OTHER); + } + + @GetMapping("url17") + public ResponseEntity bad14(String redirectUrl) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setLocation(URI.create(redirectUrl)); + + return new ResponseEntity<>("TestBody", httpHeaders, HttpStatus.SEE_OTHER); + } } diff --git a/java/ql/test/library-tests/UnsafeDeserialization/unsafeDeserialization.ql b/java/ql/test/library-tests/UnsafeDeserialization/unsafeDeserialization.ql index 9433eba7f7f..0e0217a2472 100644 --- a/java/ql/test/library-tests/UnsafeDeserialization/unsafeDeserialization.ql +++ b/java/ql/test/library-tests/UnsafeDeserialization/unsafeDeserialization.ql @@ -1,5 +1,5 @@ import default -import semmle.code.java.security.UnsafeDeserialization +import semmle.code.java.security.UnsafeDeserializationQuery from Method m, MethodAccess ma where ma.getMethod() = m and unsafeDeserialization(ma, _) diff --git a/java/ql/test/library-tests/dataflow/collections/Constructors.java b/java/ql/test/library-tests/dataflow/collections/Constructors.java new file mode 100644 index 00000000000..d003d6915b2 --- /dev/null +++ b/java/ql/test/library-tests/dataflow/collections/Constructors.java @@ -0,0 +1,286 @@ +package generatedtest; + +import java.util.AbstractMap; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.Vector; +import java.util.WeakHashMap; + +// Test case generated by GenerateFlowTestCase.ql +public class Constructors { + + Object getElement(Collection container) { return container.iterator().next(); } + Object getMapKey(Map container) { return container.keySet().iterator().next(); } + Object getMapValue(Map container) { return container.get(null); } + Object getMapKey(Map.Entry container) { return container.getKey(); } + Object getMapValue(Map.Entry container) { return container.getValue(); } + Object source() { return null; } + void sink(Object o) { } + + public void test() { + + { + // "java.util;AbstractMap$SimpleEntry;false;SimpleEntry;(Entry);;MapKey of Argument[0];MapKey of Argument[-1];value" + AbstractMap.SimpleEntry out = null; + Map.Entry in = new AbstractMap.SimpleEntry(source(), null); + out = new AbstractMap.SimpleEntry(in); + sink(getMapKey(out)); // $ hasValueFlow + } + { + // "java.util;AbstractMap$SimpleEntry;false;SimpleEntry;(Entry);;MapValue of Argument[0];MapValue of Argument[-1];value" + AbstractMap.SimpleEntry out = null; + Map.Entry in = new AbstractMap.SimpleEntry(null, source());; + out = new AbstractMap.SimpleEntry(in); + sink(getMapValue(out)); // $ hasValueFlow + } + { + // "java.util;AbstractMap$SimpleEntry;false;SimpleEntry;(Object,Object);;Argument[0];MapKey of Argument[-1];value" + AbstractMap.SimpleEntry out = null; + Object in = source(); + out = new AbstractMap.SimpleEntry(in, null); + sink(getMapKey(out)); // $ hasValueFlow + } + { + // "java.util;AbstractMap$SimpleEntry;false;SimpleEntry;(Object,Object);;Argument[1];MapValue of Argument[-1];value" + AbstractMap.SimpleEntry out = null; + Object in = source(); + out = new AbstractMap.SimpleEntry(null, in); + sink(getMapValue(out)); // $ hasValueFlow + } + { + // "java.util;AbstractMap$SimpleImmutableEntry;false;SimpleImmutableEntry;(Entry);;MapKey of Argument[0];MapKey of Argument[-1];value" + AbstractMap.SimpleImmutableEntry out = null; + Map.Entry in = new AbstractMap.SimpleEntry(source(), null); + out = new AbstractMap.SimpleImmutableEntry(in); + sink(getMapKey(out)); // $ hasValueFlow + } + { + // "java.util;AbstractMap$SimpleImmutableEntry;false;SimpleImmutableEntry;(Entry);;MapValue of Argument[0];MapValue of Argument[-1];value" + AbstractMap.SimpleImmutableEntry out = null; + Map.Entry in = new AbstractMap.SimpleEntry(null, source()); + out = new AbstractMap.SimpleImmutableEntry(in); + sink(getMapValue(out)); // $ hasValueFlow + } + { + // "java.util;ArrayDeque;false;ArrayDeque;(Collection);;Element of Argument[0];Element of Argument[-1];value" + ArrayDeque out = null; + Collection in = List.of(source()); + out = new ArrayDeque(in); + sink(getElement(out)); // $ hasValueFlow + } + { + // "java.util;ArrayList;false;ArrayList;(Collection);;Element of Argument[0];Element of Argument[-1];value" + ArrayList out = null; + Collection in = List.of(source()); + out = new ArrayList(in); + sink(getElement(out)); // $ hasValueFlow + } + { + // "java.util;EnumMap;false;EnumMap;(EnumMap);;MapKey of Argument[0];MapKey of Argument[-1];value" + EnumMap out = null; + EnumMap in = new EnumMap(Map.of(source(), null)); + out = new EnumMap(in); + sink(getMapKey(out)); // $ hasValueFlow + } + { + // "java.util;EnumMap;false;EnumMap;(EnumMap);;MapValue of Argument[0];MapValue of Argument[-1];value" + EnumMap out = null; + EnumMap in = new EnumMap(Map.of(null, source())); + out = new EnumMap(in); + sink(getMapValue(out)); // $ hasValueFlow + } + { + // "java.util;EnumMap;false;EnumMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value" + EnumMap out = null; + Map in = Map.of(source(), null); + out = new EnumMap(in); + sink(getMapKey(out)); // $ hasValueFlow + } + { + // "java.util;EnumMap;false;EnumMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value" + EnumMap out = null; + Map in = Map.of(null, source()); + out = new EnumMap(in); + sink(getMapValue(out)); // $ hasValueFlow + } + { + // "java.util;HashMap;false;HashMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value" + HashMap out = null; + Map in = Map.of(source(), null); + out = new HashMap(in); + sink(getMapKey(out)); // $ hasValueFlow + } + { + // "java.util;HashMap;false;HashMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value" + HashMap out = null; + Map in = Map.of(null, source()); + out = new HashMap(in); + sink(getMapValue(out)); // $ hasValueFlow + } + { + // "java.util;HashSet;false;HashSet;(Collection);;Element of Argument[0];Element of Argument[-1];value" + HashSet out = null; + Collection in = List.of(source()); + out = new HashSet(in); + sink(getElement(out)); // $ hasValueFlow + } + { + // "java.util;Hashtable;false;Hashtable;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value" + Hashtable out = null; + Map in = Map.of(source(), null); + out = new Hashtable(in); + sink(getMapKey(out)); // $ hasValueFlow + } + { + // "java.util;Hashtable;false;Hashtable;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value" + Hashtable out = null; + Map in = Map.of(null, source()); + out = new Hashtable(in); + sink(getMapValue(out)); // $ hasValueFlow + } + { + // "java.util;IdentityHashMap;false;IdentityHashMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value" + IdentityHashMap out = null; + Map in = Map.of(source(), null); + out = new IdentityHashMap(in); + sink(getMapKey(out)); // $ hasValueFlow + } + { + // "java.util;IdentityHashMap;false;IdentityHashMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value" + IdentityHashMap out = null; + Map in = Map.of(null, source()); + out = new IdentityHashMap(in); + sink(getMapValue(out)); // $ hasValueFlow + } + { + // "java.util;LinkedHashMap;false;LinkedHashMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value" + LinkedHashMap out = null; + Map in = Map.of(source(), null); + out = new LinkedHashMap(in); + sink(getMapKey(out)); // $ hasValueFlow + } + { + // "java.util;LinkedHashMap;false;LinkedHashMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value" + LinkedHashMap out = null; + Map in = Map.of(null, source()); + out = new LinkedHashMap(in); + sink(getMapValue(out)); // $ hasValueFlow + } + { + // "java.util;LinkedHashSet;false;LinkedHashSet;(Collection);;Element of Argument[0];Element of Argument[-1];value" + LinkedHashSet out = null; + Collection in = List.of(source()); + out = new LinkedHashSet(in); + sink(getElement(out)); // $ hasValueFlow + } + { + // "java.util;LinkedList;false;LinkedList;(Collection);;Element of Argument[0];Element of Argument[-1];value" + LinkedList out = null; + Collection in = List.of(source()); + out = new LinkedList(in); + sink(getElement(out)); // $ hasValueFlow + } + { + // "java.util;PriorityQueue;false;PriorityQueue;(Collection);;Element of Argument[0];Element of Argument[-1];value" + PriorityQueue out = null; + Collection in = List.of(source()); + out = new PriorityQueue(in); + sink(getElement(out)); // $ hasValueFlow + } + { + // "java.util;PriorityQueue;false;PriorityQueue;(PriorityQueue);;Element of Argument[0];Element of Argument[-1];value" + PriorityQueue out = null; + PriorityQueue in = new PriorityQueue(List.of(source())); + out = new PriorityQueue(in); + sink(getElement(out)); // $ hasValueFlow + } + { + // "java.util;PriorityQueue;false;PriorityQueue;(SortedSet);;Element of Argument[0];Element of Argument[-1];value" + PriorityQueue out = null; + SortedSet in = new TreeSet(List.of(source())); + out = new PriorityQueue(in); + sink(getElement(out)); // $ hasValueFlow + } + { + // "java.util;TreeMap;false;TreeMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value" + TreeMap out = null; + Map in = Map.of(source(), null); + out = new TreeMap(in); + sink(getMapKey(out)); // $ hasValueFlow + } + { + // "java.util;TreeMap;false;TreeMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value" + TreeMap out = null; + Map in = Map.of(null, source()); + out = new TreeMap(in); + sink(getMapValue(out)); // $ hasValueFlow + } + { + // "java.util;TreeMap;false;TreeMap;(SortedMap);;MapKey of Argument[0];MapKey of Argument[-1];value" + TreeMap out = null; + SortedMap in = new TreeMap(Map.of(source(), null)); + out = new TreeMap(in); + sink(getMapKey(out)); // $ hasValueFlow + } + { + // "java.util;TreeMap;false;TreeMap;(SortedMap);;MapValue of Argument[0];MapValue of Argument[-1];value" + TreeMap out = null; + SortedMap in = new TreeMap(Map.of(null, source())); + out = new TreeMap(in); + sink(getMapValue(out)); // $ hasValueFlow + } + { + // "java.util;TreeSet;false;TreeSet;(Collection);;Element of Argument[0];Element of Argument[-1];value" + TreeSet out = null; + Collection in = List.of(source()); + out = new TreeSet(in); + sink(getElement(out)); // $ hasValueFlow + } + { + // "java.util;TreeSet;false;TreeSet;(SortedSet);;Element of Argument[0];Element of Argument[-1];value" + TreeSet out = null; + SortedSet in = new TreeSet(List.of(source())); + out = new TreeSet(in); + sink(getElement(out)); // $ hasValueFlow + } + { + // "java.util;Vector;false;Vector;(Collection);;Element of Argument[0];Element of Argument[-1];value" + Vector out = null; + Collection in = List.of(source()); + out = new Vector(in); + sink(getElement(out)); // $ hasValueFlow + } + { + // "java.util;WeakHashMap;false;WeakHashMap;(Map);;MapKey of Argument[0];MapKey of Argument[-1];value" + WeakHashMap out = null; + Map in = Map.of(source(), null); + out = new WeakHashMap(in); + sink(getMapKey(out)); // $ hasValueFlow + } + { + // "java.util;WeakHashMap;false;WeakHashMap;(Map);;MapValue of Argument[0];MapValue of Argument[-1];value" + WeakHashMap out = null; + Map in = Map.of(null, source()); + out = new WeakHashMap(in); + sink(getMapValue(out)); // $ hasValueFlow + } + + } + +} diff --git a/java/ql/test/query-tests/security/CWE-079/semmle/tests/SpringXSS.java b/java/ql/test/query-tests/security/CWE-079/semmle/tests/SpringXSS.java new file mode 100644 index 00000000000..959125caa4c --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-079/semmle/tests/SpringXSS.java @@ -0,0 +1,160 @@ +import org.springframework.http.ResponseEntity; +import org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Optional; + +@RestController +public class SpringXSS { + + @GetMapping + public static ResponseEntity specificContentType(boolean safeContentType, boolean chainDirectly, String userControlled) { + + ResponseEntity.BodyBuilder builder = ResponseEntity.ok(); + + if(safeContentType) { + if(chainDirectly) { + return builder.contentType(MediaType.TEXT_HTML).body(userControlled); // $xss + } + else { + ResponseEntity.BodyBuilder builder2 = builder.contentType(MediaType.TEXT_HTML); + return builder2.body(userControlled); // $xss + } + } + else { + if(chainDirectly) { + return builder.contentType(MediaType.APPLICATION_JSON).body(userControlled); // $SPURIOUS: xss + } + else { + ResponseEntity.BodyBuilder builder2 = builder.contentType(MediaType.APPLICATION_JSON); + return builder2.body(userControlled); // $SPURIOUS: xss + } + } + + } + + @GetMapping(value = "/xyz", produces = MediaType.APPLICATION_JSON_VALUE) + public static ResponseEntity methodContentTypeSafe(String userControlled) { + return ResponseEntity.ok(userControlled); + } + + @PostMapping(value = "/xyz", produces = MediaType.APPLICATION_JSON_VALUE) + public static ResponseEntity methodContentTypeSafePost(String userControlled) { + return ResponseEntity.ok(userControlled); + } + + @RequestMapping(value = "/xyz", produces = MediaType.APPLICATION_JSON_VALUE) + public static ResponseEntity methodContentTypeSafeRequest(String userControlled) { + return ResponseEntity.ok(userControlled); + } + + @GetMapping(value = "/xyz", produces = "application/json") + public static ResponseEntity methodContentTypeSafeStringLiteral(String userControlled) { + return ResponseEntity.ok(userControlled); + } + + @GetMapping(value = "/xyz", produces = MediaType.TEXT_HTML_VALUE) + public static ResponseEntity methodContentTypeUnsafe(String userControlled) { + return ResponseEntity.ok(userControlled); // $MISSING: xss + } + + @GetMapping(value = "/xyz", produces = "text/html") + public static ResponseEntity methodContentTypeUnsafeStringLiteral(String userControlled) { + return ResponseEntity.ok(userControlled); // $xss + } + + @GetMapping(value = "/xyz", produces = {MediaType.TEXT_HTML_VALUE, MediaType.APPLICATION_JSON_VALUE}) + public static ResponseEntity methodContentTypeMaybeSafe(String userControlled) { + return ResponseEntity.ok(userControlled); // $xss + } + + @GetMapping(value = "/xyz", produces = MediaType.APPLICATION_JSON_VALUE) + public static ResponseEntity methodContentTypeSafeOverriddenWithUnsafe(String userControlled) { + return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(userControlled); // $MISSING: xss + } + + @GetMapping(value = "/xyz", produces = MediaType.TEXT_HTML_VALUE) + public static ResponseEntity methodContentTypeUnsafeOverriddenWithSafe(String userControlled) { + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(userControlled); + } + + @GetMapping(value = "/xyz", produces = {"text/html", "application/json"}) + public static ResponseEntity methodContentTypeMaybeSafeStringLiterals(String userControlled, int constructionMethod) { + // Also try out some alternative constructors for the ResponseEntity: + switch(constructionMethod) { + case 0: + return ResponseEntity.ok(userControlled); // $xss + case 1: + return ResponseEntity.of(Optional.of(userControlled)); // $xss + case 2: + return ResponseEntity.ok().body(userControlled); // $xss + case 3: + return new ResponseEntity(userControlled, HttpStatus.OK); // $xss + default: + return null; + } + } + + @RestController + @RequestMapping(produces = {"application/json"}) + private static class ClassContentTypeSafe { + @GetMapping(value = "/abc") + public ResponseEntity test(String userControlled) { + return ResponseEntity.ok(userControlled); // $SPURIOUS: xss + } + + @GetMapping(value = "/abc") + public String testDirectReturn(String userControlled) { + return userControlled; // $SPURIOUS: xss + } + + @GetMapping(value = "/xyz", produces = {"text/html"}) + public ResponseEntity overridesWithUnsafe(String userControlled) { + return ResponseEntity.ok(userControlled); // $xss + } + + @GetMapping(value = "/abc") + public ResponseEntity overridesWithUnsafe2(String userControlled) { + return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(userControlled); // $xss + } + } + + @RestController + @RequestMapping(produces = {"text/html"}) + private static class ClassContentTypeUnsafe { + @GetMapping(value = "/abc") + public ResponseEntity test(String userControlled) { + return ResponseEntity.ok(userControlled); // $xss + } + + @GetMapping(value = "/abc") + public String testDirectReturn(String userControlled) { + return userControlled; // $xss + } + + @GetMapping(value = "/xyz", produces = {"application/json"}) + public ResponseEntity overridesWithSafe(String userControlled) { + return ResponseEntity.ok(userControlled); // $SPURIOUS: xss + } + + @GetMapping(value = "/abc") + public ResponseEntity overridesWithSafe2(String userControlled) { + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(userControlled); // $SPURIOUS: xss + } + } + + @GetMapping(value = "/abc") + public static ResponseEntity entityWithNoMediaType(String userControlled) { + return ResponseEntity.ok(userControlled); // $xss + } + + @GetMapping(value = "/abc") + public static String stringWithNoMediaType(String userControlled) { + return userControlled; // $xss + } + +} \ No newline at end of file diff --git a/java/ql/test/query-tests/security/CWE-079/semmle/tests/options b/java/ql/test/query-tests/security/CWE-079/semmle/tests/options index 5fbdfa53a96..719c5e3dd57 100644 --- a/java/ql/test/query-tests/security/CWE-079/semmle/tests/options +++ b/java/ql/test/query-tests/security/CWE-079/semmle/tests/options @@ -1 +1 @@ -//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../../stubs/servlet-api-2.4:${testdir}/../../../../../stubs/javax-ws-rs-api-2.1.1/ +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../../stubs/servlet-api-2.4:${testdir}/../../../../../stubs/javax-ws-rs-api-2.1.1/:${testdir}/../../../../../stubs/springframework-5.3.8 diff --git a/java/ql/test/query-tests/security/CWE-094/MvelInjectionTest.expected b/java/ql/test/query-tests/security/CWE-094/MvelInjectionTest.expected new file mode 100644 index 00000000000..e69de29bb2d diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/MvelInjection.java b/java/ql/test/query-tests/security/CWE-094/MvelInjectionTest.java similarity index 78% rename from java/ql/test/experimental/query-tests/security/CWE-094/MvelInjection.java rename to java/ql/test/query-tests/security/CWE-094/MvelInjectionTest.java index 88f9b861cae..4013246eecd 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-094/MvelInjection.java +++ b/java/ql/test/query-tests/security/CWE-094/MvelInjectionTest.java @@ -15,38 +15,37 @@ import org.mvel2.compiler.ExpressionCompiler; import org.mvel2.integration.impl.ImmutableDefaultFactory; import org.mvel2.jsr223.MvelCompiledScript; import org.mvel2.jsr223.MvelScriptEngine; -import org.mvel2.templates.CompiledTemplate; import org.mvel2.templates.TemplateCompiler; import org.mvel2.templates.TemplateRuntime; -public class MvelInjection { +public class MvelInjectionTest { public static void testWithMvelEval(Socket socket) throws IOException { - MVEL.eval(read(socket)); + MVEL.eval(read(socket)); // $hasMvelInjection } public static void testWithMvelCompileAndExecute(Socket socket) throws IOException { Serializable expression = MVEL.compileExpression(read(socket)); - MVEL.executeExpression(expression); + MVEL.executeExpression(expression); // $hasMvelInjection } public static void testWithExpressionCompiler(Socket socket) throws IOException { ExpressionCompiler compiler = new ExpressionCompiler(read(socket)); ExecutableStatement statement = compiler.compile(); - statement.getValue(new Object(), new ImmutableDefaultFactory()); - statement.getValue(new Object(), new Object(), new ImmutableDefaultFactory()); + statement.getValue(new Object(), new ImmutableDefaultFactory()); // $hasMvelInjection + statement.getValue(new Object(), new Object(), new ImmutableDefaultFactory()); // $hasMvelInjection } public static void testWithCompiledExpressionGetDirectValue(Socket socket) throws IOException { ExpressionCompiler compiler = new ExpressionCompiler(read(socket)); CompiledExpression expression = compiler.compile(); - expression.getDirectValue(new Object(), new ImmutableDefaultFactory()); + expression.getDirectValue(new Object(), new ImmutableDefaultFactory()); // $hasMvelInjection } public static void testCompiledAccExpressionGetValue(Socket socket) throws IOException { - CompiledAccExpression expression = new CompiledAccExpression( - read(socket).toCharArray(), Object.class, new ParserContext()); - expression.getValue(new Object(), new ImmutableDefaultFactory()); + CompiledAccExpression expression = + new CompiledAccExpression(read(socket).toCharArray(), Object.class, new ParserContext()); + expression.getValue(new Object(), new ImmutableDefaultFactory()); // $hasMvelInjection } public static void testMvelScriptEngineCompileAndEvaluate(Socket socket) throws Exception { @@ -54,10 +53,10 @@ public class MvelInjection { MvelScriptEngine engine = new MvelScriptEngine(); CompiledScript compiledScript = engine.compile(input); - compiledScript.eval(); + compiledScript.eval(); // $hasMvelInjection Serializable script = engine.compiledScript(input); - engine.evaluate(script, new SimpleScriptContext()); + engine.evaluate(script, new SimpleScriptContext()); // $hasMvelInjection } public static void testMvelCompiledScriptCompileAndEvaluate(Socket socket) throws Exception { @@ -65,27 +64,26 @@ public class MvelInjection { ExpressionCompiler compiler = new ExpressionCompiler(read(socket)); ExecutableStatement statement = compiler.compile(); MvelCompiledScript script = new MvelCompiledScript(engine, statement); - script.eval(new SimpleScriptContext()); + script.eval(new SimpleScriptContext()); // $hasMvelInjection } public static void testTemplateRuntimeEval(Socket socket) throws Exception { - TemplateRuntime.eval(read(socket), new HashMap()); + TemplateRuntime.eval(read(socket), new HashMap()); // $hasMvelInjection } public static void testTemplateRuntimeCompileTemplateAndExecute(Socket socket) throws Exception { - TemplateRuntime.execute( - TemplateCompiler.compileTemplate(read(socket)), new HashMap()); + TemplateRuntime.execute(TemplateCompiler.compileTemplate(read(socket)), new HashMap()); // $hasMvelInjection } public static void testTemplateRuntimeCompileAndExecute(Socket socket) throws Exception { TemplateCompiler compiler = new TemplateCompiler(read(socket)); - TemplateRuntime.execute(compiler.compile(), new HashMap()); + TemplateRuntime.execute(compiler.compile(), new HashMap()); // $hasMvelInjection } public static void testMvelRuntimeExecute(Socket socket) throws Exception { ExpressionCompiler compiler = new ExpressionCompiler(read(socket)); CompiledExpression expression = compiler.compile(); - MVELRuntime.execute(false, expression, new Object(), new ImmutableDefaultFactory()); + MVELRuntime.execute(false, expression, new Object(), new ImmutableDefaultFactory()); // $hasMvelInjection } public static String read(Socket socket) throws IOException { diff --git a/java/ql/test/query-tests/security/CWE-094/MvelInjectionTest.ql b/java/ql/test/query-tests/security/CWE-094/MvelInjectionTest.ql new file mode 100644 index 00000000000..71146acfcf9 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-094/MvelInjectionTest.ql @@ -0,0 +1,22 @@ +import java +import semmle.code.java.dataflow.TaintTracking +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.security.MvelInjectionQuery +import TestUtilities.InlineExpectationsTest + +class HasMvelInjectionTest extends InlineExpectationsTest { + HasMvelInjectionTest() { this = "HasMvelInjectionTest" } + + override string getARelevantTag() { result = "hasMvelInjection" } + + override predicate hasActualResult(Location location, string element, string tag, string value) { + tag = "hasMvelInjection" and + exists(DataFlow::Node src, DataFlow::Node sink, MvelInjectionFlowConfig conf | + conf.hasFlow(src, sink) + | + sink.getLocation() = location and + element = sink.toString() and + value = "" + ) + } +} diff --git a/java/ql/test/query-tests/security/CWE-094/options b/java/ql/test/query-tests/security/CWE-094/options index ac2e0698a62..1b4bd12d17f 100644 --- a/java/ql/test/query-tests/security/CWE-094/options +++ b/java/ql/test/query-tests/security/CWE-094/options @@ -1 +1 @@ -//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/validation-api-2.0.1.Final:${testdir}/../../../stubs/springframework-5.3.8:${testdir}/../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../stubs/apache-commons-logging-1.2:${testdir}/../../../stubs/groovy-all-3.0.7:${testdir}/../../../stubs/servlet-api-2.4 +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/validation-api-2.0.1.Final:${testdir}/../../../stubs/springframework-5.3.8:${testdir}/../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../stubs/apache-commons-logging-1.2:${testdir}/../../../stubs/mvel2-2.4.7:${testdir}/../../../stubs/groovy-all-3.0.7:${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/scriptengine:${testdir}/../../../stubs/jsr223-api diff --git a/java/ql/test/query-tests/security/CWE-502/A.java b/java/ql/test/query-tests/security/CWE-502/A.java index 50090f69132..e95f15bff4f 100644 --- a/java/ql/test/query-tests/security/CWE-502/A.java +++ b/java/ql/test/query-tests/security/CWE-502/A.java @@ -12,34 +12,34 @@ public class A { public Object deserialize1(Socket sock) throws java.io.IOException, ClassNotFoundException { InputStream inputStream = sock.getInputStream(); ObjectInputStream in = new ObjectInputStream(inputStream); - return in.readObject(); // unsafe + return in.readObject(); // $unsafeDeserialization } public Object deserialize2(Socket sock) throws java.io.IOException, ClassNotFoundException { InputStream inputStream = sock.getInputStream(); ObjectInputStream in = new ObjectInputStream(inputStream); - return in.readUnshared(); // unsafe + return in.readUnshared(); // $unsafeDeserialization } public Object deserialize3(Socket sock) throws java.io.IOException { InputStream inputStream = sock.getInputStream(); XMLDecoder d = new XMLDecoder(inputStream); - return d.readObject(); // unsafe + return d.readObject(); // $unsafeDeserialization } public Object deserialize4(Socket sock) throws java.io.IOException { XStream xs = new XStream(); InputStream inputStream = sock.getInputStream(); Reader reader = new InputStreamReader(inputStream); - return xs.fromXML(reader); // unsafe + return xs.fromXML(reader); // $unsafeDeserialization } public void deserialize5(Socket sock) throws java.io.IOException { Kryo kryo = new Kryo(); Input input = new Input(sock.getInputStream()); - A a1 = kryo.readObject(input, A.class); // unsafe - A a2 = kryo.readObjectOrNull(input, A.class); // unsafe - Object o = kryo.readClassAndObject(input); // unsafe + A a1 = kryo.readObject(input, A.class); // $unsafeDeserialization + A a2 = kryo.readObjectOrNull(input, A.class); // $unsafeDeserialization + Object o = kryo.readClassAndObject(input); // $unsafeDeserialization } private Kryo getSafeKryo() throws java.io.IOException { @@ -58,21 +58,21 @@ public class A { public void deserializeSnakeYaml(Socket sock) throws java.io.IOException { Yaml yaml = new Yaml(); InputStream input = sock.getInputStream(); - Object o = yaml.load(input); //unsafe - Object o2 = yaml.loadAll(input); //unsafe - Object o3 = yaml.parse(new InputStreamReader(input)); //unsafe - A o4 = yaml.loadAs(input, A.class); //unsafe - A o5 = yaml.loadAs(new InputStreamReader(input), A.class); //unsafe + Object o = yaml.load(input); // $unsafeDeserialization + Object o2 = yaml.loadAll(input); // $unsafeDeserialization + Object o3 = yaml.parse(new InputStreamReader(input)); // $unsafeDeserialization + A o4 = yaml.loadAs(input, A.class); // $unsafeDeserialization + A o5 = yaml.loadAs(new InputStreamReader(input), A.class); // $unsafeDeserialization } public void deserializeSnakeYaml2(Socket sock) throws java.io.IOException { Yaml yaml = new Yaml(new Constructor()); InputStream input = sock.getInputStream(); - Object o = yaml.load(input); //unsafe - Object o2 = yaml.loadAll(input); //unsafe - Object o3 = yaml.parse(new InputStreamReader(input)); //unsafe - A o4 = yaml.loadAs(input, A.class); //unsafe - A o5 = yaml.loadAs(new InputStreamReader(input), A.class); //unsafe + Object o = yaml.load(input); // $unsafeDeserialization + Object o2 = yaml.loadAll(input); // $unsafeDeserialization + Object o3 = yaml.parse(new InputStreamReader(input)); // $unsafeDeserialization + A o4 = yaml.loadAs(input, A.class); // $unsafeDeserialization + A o5 = yaml.loadAs(new InputStreamReader(input), A.class); // $unsafeDeserialization } public void deserializeSnakeYaml3(Socket sock) throws java.io.IOException { @@ -88,10 +88,10 @@ public class A { public void deserializeSnakeYaml4(Socket sock) throws java.io.IOException { Yaml yaml = new Yaml(new Constructor(A.class)); InputStream input = sock.getInputStream(); - Object o = yaml.load(input); //unsafe - Object o2 = yaml.loadAll(input); //unsafe - Object o3 = yaml.parse(new InputStreamReader(input)); //unsafe - A o4 = yaml.loadAs(input, A.class); //unsafe - A o5 = yaml.loadAs(new InputStreamReader(input), A.class); //unsafe + Object o = yaml.load(input); // $unsafeDeserialization + Object o2 = yaml.loadAll(input); // $unsafeDeserialization + Object o3 = yaml.parse(new InputStreamReader(input)); // $unsafeDeserialization + A o4 = yaml.loadAs(input, A.class); // $unsafeDeserialization + A o5 = yaml.loadAs(new InputStreamReader(input), A.class); // $unsafeDeserialization } } diff --git a/java/ql/test/query-tests/security/CWE-502/B.java b/java/ql/test/query-tests/security/CWE-502/B.java index a12555edd70..d97a44cfd58 100644 --- a/java/ql/test/query-tests/security/CWE-502/B.java +++ b/java/ql/test/query-tests/security/CWE-502/B.java @@ -5,14 +5,14 @@ import com.alibaba.fastjson.JSON; public class B { public Object deserializeJson1(Socket sock) throws java.io.IOException { InputStream inputStream = sock.getInputStream(); - return JSON.parseObject(inputStream, null); // unsafe + return JSON.parseObject(inputStream, null); // $unsafeDeserialization } public Object deserializeJson2(Socket sock) throws java.io.IOException { InputStream inputStream = sock.getInputStream(); byte[] bytes = new byte[100]; inputStream.read(bytes); - return JSON.parse(bytes); // unsafe + return JSON.parse(bytes); // $unsafeDeserialization } public Object deserializeJson3(Socket sock) throws java.io.IOException { @@ -20,7 +20,7 @@ public class B { byte[] bytes = new byte[100]; inputStream.read(bytes); String s = new String(bytes); - return JSON.parseObject(s); // unsafe + return JSON.parseObject(s); // $unsafeDeserialization } public Object deserializeJson4(Socket sock) throws java.io.IOException { @@ -28,6 +28,6 @@ public class B { byte[] bytes = new byte[100]; inputStream.read(bytes); String s = new String(bytes); - return JSON.parse(s); // unsafe + return JSON.parse(s); // $unsafeDeserialization } } diff --git a/java/ql/test/query-tests/security/CWE-502/C.java b/java/ql/test/query-tests/security/CWE-502/C.java index bae0ca8ceae..e13ae98ead5 100644 --- a/java/ql/test/query-tests/security/CWE-502/C.java +++ b/java/ql/test/query-tests/security/CWE-502/C.java @@ -21,16 +21,16 @@ public class C { @GetMapping(value = "jyaml") public void bad1(HttpServletRequest request) throws Exception { String data = request.getParameter("data"); - Yaml.load(data); //bad - Yaml.loadStream(data); //bad - Yaml.loadStreamOfType(data, Object.class); //bad - Yaml.loadType(data, Object.class); //bad + Yaml.load(data); // $unsafeDeserialization + Yaml.loadStream(data); // $unsafeDeserialization + Yaml.loadStreamOfType(data, Object.class); // $unsafeDeserialization + Yaml.loadType(data, Object.class); // $unsafeDeserialization org.ho.yaml.YamlConfig yamlConfig = new YamlConfig(); - yamlConfig.load(data); //bad - yamlConfig.loadStream(data); //bad - yamlConfig.loadStreamOfType(data, Object.class); //bad - yamlConfig.loadType(data, Object.class); //bad + yamlConfig.load(data); // $unsafeDeserialization + yamlConfig.loadStream(data); // $unsafeDeserialization + yamlConfig.loadStreamOfType(data, Object.class); // $unsafeDeserialization + yamlConfig.loadType(data, Object.class); // $unsafeDeserialization } @GetMapping(value = "jsonio") @@ -40,19 +40,19 @@ public class C { HashMap hashMap = new HashMap(); hashMap.put("USE_MAPS", true); - JsonReader.jsonToJava(data); //bad + JsonReader.jsonToJava(data); // $unsafeDeserialization - JsonReader jr = new JsonReader(data, null); //bad - jr.readObject(); + JsonReader jr = new JsonReader(data, null); + jr.readObject(); // $unsafeDeserialization } @GetMapping(value = "yamlbeans") public void bad3(HttpServletRequest request) throws Exception { String data = request.getParameter("data"); YamlReader r = new YamlReader(data); - r.read(); //bad - r.read(Object.class); //bad - r.read(Object.class, Object.class); //bad + r.read(); // $unsafeDeserialization + r.read(Object.class); // $unsafeDeserialization + r.read(Object.class, Object.class); // $unsafeDeserialization } @GetMapping(value = "hessian") @@ -60,8 +60,8 @@ public class C { byte[] bytes = request.getParameter("data").getBytes(); ByteArrayInputStream bis = new ByteArrayInputStream(bytes); HessianInput hessianInput = new HessianInput(bis); - hessianInput.readObject(); //bad - hessianInput.readObject(Object.class); //bad + hessianInput.readObject(); // $unsafeDeserialization + hessianInput.readObject(Object.class); // $unsafeDeserialization } @GetMapping(value = "hessian2") @@ -69,14 +69,14 @@ public class C { byte[] bytes = request.getParameter("data").getBytes(); ByteArrayInputStream bis = new ByteArrayInputStream(bytes); Hessian2Input hessianInput = new Hessian2Input(bis); - hessianInput.readObject(); //bad - hessianInput.readObject(Object.class); //bad + hessianInput.readObject(); // $unsafeDeserialization + hessianInput.readObject(Object.class); // $unsafeDeserialization } @GetMapping(value = "castor") public void bad6(HttpServletRequest request) throws Exception { Unmarshaller unmarshaller = new Unmarshaller(); - unmarshaller.unmarshal(new StringReader(request.getParameter("data"))); //bad + unmarshaller.unmarshal(new StringReader(request.getParameter("data"))); // $unsafeDeserialization } @GetMapping(value = "burlap") @@ -84,11 +84,11 @@ public class C { byte[] serializedData = request.getParameter("data").getBytes(); ByteArrayInputStream is = new ByteArrayInputStream(serializedData); BurlapInput burlapInput = new BurlapInput(is); - burlapInput.readObject(); //bad + burlapInput.readObject(); // $unsafeDeserialization BurlapInput burlapInput1 = new BurlapInput(); burlapInput1.init(is); - burlapInput1.readObject(); //bad + burlapInput1.readObject(); // $unsafeDeserialization } @GetMapping(value = "jsonio1") diff --git a/java/ql/test/query-tests/security/CWE-502/JacksonTest.java b/java/ql/test/query-tests/security/CWE-502/JacksonTest.java new file mode 100644 index 00000000000..3520e4eaa11 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-502/JacksonTest.java @@ -0,0 +1,209 @@ +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; +import java.io.IOException; +import java.io.Serializable; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.List; + +public class JacksonTest { + + public static void withSocket(Action action) throws Exception { + try (ServerSocket serverSocket = new ServerSocket(0)) { + try (Socket socket = serverSocket.accept()) { + byte[] bytes = new byte[1024]; + int n = socket.getInputStream().read(bytes); + String jexlExpr = new String(bytes, 0, n); + action.run(jexlExpr); + } + } + } +} + +interface Action { + void run(T object) throws Exception; +} + +abstract class PhoneNumber implements Serializable { + public int areaCode; + public int local; +} + +class DomesticNumber extends PhoneNumber { +} + +class InternationalNumber extends PhoneNumber { + public int countryCode; +} + +class Employee extends Person { +} + +class Person { + public String name; + public int age; + + // this annotation enables polymorphic type handling + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) + public Object phone; +} + +class Task { + public Person assignee; +} + +class Tag implements Serializable { + public String title; +} + +class Cat { + public String name; + public Serializable tag; +} + +class UnsafePersonDeserialization { + + // BAD: Person has a field with an annotation that enables polymorphic type + // handling + private static void testUnsafeDeserialization() throws Exception { + JacksonTest.withSocket(string -> { + ObjectMapper mapper = new ObjectMapper(); + mapper.readValue(string, Person.class); // $unsafeDeserialization + }); + } + + // BAD: Employee extends Person that has a field with an annotation that enables + // polymorphic type handling + private static void testUnsafeDeserializationWithExtendedClass() throws Exception { + JacksonTest.withSocket(string -> { + ObjectMapper mapper = new ObjectMapper(); + mapper.readValue(string, Employee.class); // $unsafeDeserialization + }); + } + + // BAD: Task has a Person field that has a field with an annotation that enables + // polymorphic type handling + private static void testUnsafeDeserializationWithWrapper() throws Exception { + JacksonTest.withSocket(string -> { + ObjectMapper mapper = new ObjectMapper(); + mapper.readValue(string, Task.class); // $unsafeDeserialization + }); + } +} + +class SaferPersonDeserialization { + + // GOOD: Despite enabled polymorphic type handling, this is safe because ObjectMapper + // has a validator + private static void testSafeDeserializationWithValidator() throws Exception { + JacksonTest.withSocket(string -> { + PolymorphicTypeValidator ptv = + BasicPolymorphicTypeValidator.builder() + .allowIfSubType("only.allowed.package") + .build(); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setPolymorphicTypeValidator(ptv); + + mapper.readValue(string, Person.class); + }); + } + + // GOOD: Despite enabled polymorphic type handling, this is safe because ObjectMapper + // has a validator + private static void testSafeDeserializationWithValidatorAndBuilder() throws Exception { + JacksonTest.withSocket(string -> { + PolymorphicTypeValidator ptv = + BasicPolymorphicTypeValidator.builder() + .allowIfSubType("only.allowed.package") + .build(); + + ObjectMapper mapper = JsonMapper.builder() + .polymorphicTypeValidator(ptv) + .build(); + + mapper.readValue(string, Person.class); + }); + } +} + +class UnsafeCatDeserialization { + + // BAD: deserializing untrusted input while polymorphic type handling is on + private static void testUnsafeDeserialization() throws Exception { + JacksonTest.withSocket(string -> { + ObjectMapper mapper = new ObjectMapper(); + mapper.enableDefaultTyping(); // this enables polymorphic type handling + mapper.readValue(string, Cat.class); // $unsafeDeserialization + }); + } + + // BAD: deserializing untrusted input while polymorphic type handling is on + private static void testUnsafeDeserializationWithObjectMapperReadValues() throws Exception { + JacksonTest.withSocket(string -> { + ObjectMapper mapper = new ObjectMapper(); + mapper.enableDefaultTyping(); + mapper.readValues(new JsonFactory().createParser(string), Cat.class).readAll(); // $unsafeDeserialization + }); + } + + // BAD: deserializing untrusted input while polymorphic type handling is on + private static void testUnsafeDeserializationWithObjectMapperTreeToValue() throws Exception { + JacksonTest.withSocket(string -> { + ObjectMapper mapper = new ObjectMapper(); + mapper.enableDefaultTyping(); + mapper.treeToValue(mapper.readTree(string), Cat.class); // $unsafeDeserialization + }); + } + + // BAD: an attacker can control both data and type of deserialized object + private static void testUnsafeDeserializationWithUnsafeClass() throws Exception { + JacksonTest.withSocket(input -> { + String[] parts = input.split(";"); + String data = parts[0]; + String type = parts[1]; + Class clazz = Class.forName(type); + ObjectMapper mapper = new ObjectMapper(); + mapper.readValue(data, clazz); // $unsafeDeserialization + }); + } + + // BAD: an attacker can control both data and type of deserialized object + private static void testUnsafeDeserializationWithUnsafeClassAndCustomTypeResolver() throws Exception { + JacksonTest.withSocket(input -> { + String[] parts = input.split(";"); + String data = parts[0]; + String type = parts[1]; + ObjectMapper mapper = new ObjectMapper(); + mapper.readValue(data, resolveImpl(type, mapper)); // $unsafeDeserialization + }); + } + + private static JavaType resolveImpl(String type, ObjectMapper mapper) throws Exception { + return mapper.constructType(Class.forName(type)); + } +} + +class SaferCatDeserialization { + + // GOOD: Despite enabled polymorphic type handling, this is safe because ObjectMapper + // has a validator + private static void testUnsafeDeserialization() throws Exception { + JacksonTest.withSocket(string -> { + PolymorphicTypeValidator ptv = + BasicPolymorphicTypeValidator.builder() + .allowIfSubType("only.allowed.pachage") + .build(); + + ObjectMapper mapper = JsonMapper.builder().polymorphicTypeValidator(ptv).build(); + mapper.enableDefaultTyping(); // this enables polymorphic type handling + + mapper.readValue(string, Cat.class); + }); + } +} \ No newline at end of file diff --git a/java/ql/test/query-tests/security/CWE-502/TestMessageBodyReader.java b/java/ql/test/query-tests/security/CWE-502/TestMessageBodyReader.java index 7940b9c7701..2132041254d 100644 --- a/java/ql/test/query-tests/security/CWE-502/TestMessageBodyReader.java +++ b/java/ql/test/query-tests/security/CWE-502/TestMessageBodyReader.java @@ -19,7 +19,7 @@ public class TestMessageBodyReader implements MessageBodyReader { public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException { try { - return new ObjectInputStream(entityStream).readObject(); + return new ObjectInputStream(entityStream).readObject(); // $unsafeDeserialization } catch (ClassNotFoundException e) { e.printStackTrace(); } diff --git a/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.expected b/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.expected index 7b02131cd73..e69de29bb2d 100644 --- a/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.expected +++ b/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.expected @@ -1,229 +0,0 @@ -edges -| A.java:13:31:13:51 | getInputStream(...) : InputStream | A.java:14:50:14:60 | inputStream : InputStream | -| A.java:13:31:13:51 | getInputStream(...) : InputStream | A.java:15:12:15:13 | in | -| A.java:14:28:14:61 | new ObjectInputStream(...) : ObjectInputStream | A.java:15:12:15:13 | in | -| A.java:14:50:14:60 | inputStream : InputStream | A.java:14:28:14:61 | new ObjectInputStream(...) : ObjectInputStream | -| A.java:19:31:19:51 | getInputStream(...) : InputStream | A.java:20:50:20:60 | inputStream : InputStream | -| A.java:19:31:19:51 | getInputStream(...) : InputStream | A.java:21:12:21:13 | in | -| A.java:20:28:20:61 | new ObjectInputStream(...) : ObjectInputStream | A.java:21:12:21:13 | in | -| A.java:20:50:20:60 | inputStream : InputStream | A.java:20:28:20:61 | new ObjectInputStream(...) : ObjectInputStream | -| A.java:25:31:25:51 | getInputStream(...) : InputStream | A.java:26:35:26:45 | inputStream : InputStream | -| A.java:26:20:26:46 | new XMLDecoder(...) : XMLDecoder | A.java:27:12:27:12 | d | -| A.java:26:35:26:45 | inputStream : InputStream | A.java:26:20:26:46 | new XMLDecoder(...) : XMLDecoder | -| A.java:32:31:32:51 | getInputStream(...) : InputStream | A.java:33:43:33:53 | inputStream : InputStream | -| A.java:33:21:33:54 | new InputStreamReader(...) : InputStreamReader | A.java:34:23:34:28 | reader | -| A.java:33:43:33:53 | inputStream : InputStream | A.java:33:21:33:54 | new InputStreamReader(...) : InputStreamReader | -| A.java:39:19:39:50 | new Input(...) : Input | A.java:40:28:40:32 | input | -| A.java:39:19:39:50 | new Input(...) : Input | A.java:41:34:41:38 | input | -| A.java:39:19:39:50 | new Input(...) : Input | A.java:42:40:42:44 | input | -| A.java:39:29:39:49 | getInputStream(...) : InputStream | A.java:39:19:39:50 | new Input(...) : Input | -| A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:61:26:61:30 | input | -| A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:62:30:62:34 | input | -| A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:63:50:63:54 | input : InputStream | -| A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:64:24:64:28 | input | -| A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:65:46:65:50 | input : InputStream | -| A.java:63:50:63:54 | input : InputStream | A.java:63:28:63:55 | new InputStreamReader(...) | -| A.java:65:46:65:50 | input : InputStream | A.java:65:24:65:51 | new InputStreamReader(...) | -| A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:71:26:71:30 | input | -| A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:72:30:72:34 | input | -| A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:73:50:73:54 | input : InputStream | -| A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:74:24:74:28 | input | -| A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:75:46:75:50 | input : InputStream | -| A.java:73:50:73:54 | input : InputStream | A.java:73:28:73:55 | new InputStreamReader(...) | -| A.java:75:46:75:50 | input : InputStream | A.java:75:24:75:51 | new InputStreamReader(...) | -| A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:91:26:91:30 | input | -| A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:92:30:92:34 | input | -| A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:93:50:93:54 | input : InputStream | -| A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:94:24:94:28 | input | -| A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:95:46:95:50 | input : InputStream | -| A.java:93:50:93:54 | input : InputStream | A.java:93:28:93:55 | new InputStreamReader(...) | -| A.java:95:46:95:50 | input : InputStream | A.java:95:24:95:51 | new InputStreamReader(...) | -| B.java:7:31:7:51 | getInputStream(...) : InputStream | B.java:8:29:8:39 | inputStream | -| B.java:12:31:12:51 | getInputStream(...) : InputStream | B.java:14:5:14:15 | inputStream : InputStream | -| B.java:14:5:14:15 | inputStream : InputStream | B.java:14:22:14:26 | bytes [post update] : byte[] | -| B.java:14:22:14:26 | bytes [post update] : byte[] | B.java:15:23:15:27 | bytes | -| B.java:19:31:19:51 | getInputStream(...) : InputStream | B.java:21:5:21:15 | inputStream : InputStream | -| B.java:21:5:21:15 | inputStream : InputStream | B.java:21:22:21:26 | bytes [post update] : byte[] | -| B.java:21:22:21:26 | bytes [post update] : byte[] | B.java:23:29:23:29 | s | -| B.java:27:31:27:51 | getInputStream(...) : InputStream | B.java:29:5:29:15 | inputStream : InputStream | -| B.java:29:5:29:15 | inputStream : InputStream | B.java:29:22:29:26 | bytes [post update] : byte[] | -| B.java:29:22:29:26 | bytes [post update] : byte[] | B.java:31:23:31:23 | s | -| C.java:23:17:23:44 | getParameter(...) : String | C.java:24:13:24:16 | data | -| C.java:23:17:23:44 | getParameter(...) : String | C.java:25:19:25:22 | data | -| C.java:23:17:23:44 | getParameter(...) : String | C.java:26:25:26:28 | data | -| C.java:23:17:23:44 | getParameter(...) : String | C.java:27:17:27:20 | data | -| C.java:23:17:23:44 | getParameter(...) : String | C.java:30:19:30:22 | data | -| C.java:23:17:23:44 | getParameter(...) : String | C.java:31:25:31:28 | data | -| C.java:23:17:23:44 | getParameter(...) : String | C.java:32:31:32:34 | data | -| C.java:23:17:23:44 | getParameter(...) : String | C.java:33:23:33:26 | data | -| C.java:38:17:38:44 | getParameter(...) : String | C.java:43:25:43:28 | data | -| C.java:38:17:38:44 | getParameter(...) : String | C.java:46:3:46:4 | jr | -| C.java:51:17:51:44 | getParameter(...) : String | C.java:53:3:53:3 | r | -| C.java:51:17:51:44 | getParameter(...) : String | C.java:54:3:54:3 | r | -| C.java:51:17:51:44 | getParameter(...) : String | C.java:55:3:55:3 | r | -| C.java:60:18:60:45 | getParameter(...) : String | C.java:61:55:61:59 | bytes : byte[] | -| C.java:60:18:60:45 | getParameter(...) : String | C.java:63:3:63:14 | hessianInput | -| C.java:60:18:60:45 | getParameter(...) : String | C.java:64:3:64:14 | hessianInput | -| C.java:61:30:61:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | C.java:63:3:63:14 | hessianInput | -| C.java:61:30:61:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | C.java:64:3:64:14 | hessianInput | -| C.java:61:55:61:59 | bytes : byte[] | C.java:61:30:61:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | -| C.java:69:18:69:45 | getParameter(...) : String | C.java:70:55:70:59 | bytes : byte[] | -| C.java:69:18:69:45 | getParameter(...) : String | C.java:72:3:72:14 | hessianInput | -| C.java:69:18:69:45 | getParameter(...) : String | C.java:73:3:73:14 | hessianInput | -| C.java:70:30:70:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | C.java:72:3:72:14 | hessianInput | -| C.java:70:30:70:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | C.java:73:3:73:14 | hessianInput | -| C.java:70:55:70:59 | bytes : byte[] | C.java:70:30:70:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | -| C.java:79:43:79:70 | getParameter(...) : String | C.java:79:26:79:71 | new StringReader(...) | -| C.java:84:27:84:54 | getParameter(...) : String | C.java:85:54:85:67 | serializedData : byte[] | -| C.java:84:27:84:54 | getParameter(...) : String | C.java:87:3:87:13 | burlapInput | -| C.java:84:27:84:54 | getParameter(...) : String | C.java:91:3:91:14 | burlapInput1 | -| C.java:85:29:85:68 | new ByteArrayInputStream(...) : ByteArrayInputStream | C.java:87:3:87:13 | burlapInput | -| C.java:85:29:85:68 | new ByteArrayInputStream(...) : ByteArrayInputStream | C.java:91:3:91:14 | burlapInput1 | -| C.java:85:54:85:67 | serializedData : byte[] | C.java:85:29:85:68 | new ByteArrayInputStream(...) : ByteArrayInputStream | -| TestMessageBodyReader.java:20:55:20:78 | entityStream : InputStream | TestMessageBodyReader.java:22:18:22:52 | new ObjectInputStream(...) | -| TestMessageBodyReader.java:20:55:20:78 | entityStream : InputStream | TestMessageBodyReader.java:22:40:22:51 | entityStream : InputStream | -| TestMessageBodyReader.java:22:40:22:51 | entityStream : InputStream | TestMessageBodyReader.java:22:18:22:52 | new ObjectInputStream(...) | -nodes -| A.java:13:31:13:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| A.java:14:28:14:61 | new ObjectInputStream(...) : ObjectInputStream | semmle.label | new ObjectInputStream(...) : ObjectInputStream | -| A.java:14:50:14:60 | inputStream : InputStream | semmle.label | inputStream : InputStream | -| A.java:15:12:15:13 | in | semmle.label | in | -| A.java:19:31:19:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| A.java:20:28:20:61 | new ObjectInputStream(...) : ObjectInputStream | semmle.label | new ObjectInputStream(...) : ObjectInputStream | -| A.java:20:50:20:60 | inputStream : InputStream | semmle.label | inputStream : InputStream | -| A.java:21:12:21:13 | in | semmle.label | in | -| A.java:25:31:25:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| A.java:26:20:26:46 | new XMLDecoder(...) : XMLDecoder | semmle.label | new XMLDecoder(...) : XMLDecoder | -| A.java:26:35:26:45 | inputStream : InputStream | semmle.label | inputStream : InputStream | -| A.java:27:12:27:12 | d | semmle.label | d | -| A.java:32:31:32:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| A.java:33:21:33:54 | new InputStreamReader(...) : InputStreamReader | semmle.label | new InputStreamReader(...) : InputStreamReader | -| A.java:33:43:33:53 | inputStream : InputStream | semmle.label | inputStream : InputStream | -| A.java:34:23:34:28 | reader | semmle.label | reader | -| A.java:39:19:39:50 | new Input(...) : Input | semmle.label | new Input(...) : Input | -| A.java:39:29:39:49 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| A.java:40:28:40:32 | input | semmle.label | input | -| A.java:41:34:41:38 | input | semmle.label | input | -| A.java:42:40:42:44 | input | semmle.label | input | -| A.java:60:25:60:45 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| A.java:61:26:61:30 | input | semmle.label | input | -| A.java:62:30:62:34 | input | semmle.label | input | -| A.java:63:28:63:55 | new InputStreamReader(...) | semmle.label | new InputStreamReader(...) | -| A.java:63:50:63:54 | input : InputStream | semmle.label | input : InputStream | -| A.java:64:24:64:28 | input | semmle.label | input | -| A.java:65:24:65:51 | new InputStreamReader(...) | semmle.label | new InputStreamReader(...) | -| A.java:65:46:65:50 | input : InputStream | semmle.label | input : InputStream | -| A.java:70:25:70:45 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| A.java:71:26:71:30 | input | semmle.label | input | -| A.java:72:30:72:34 | input | semmle.label | input | -| A.java:73:28:73:55 | new InputStreamReader(...) | semmle.label | new InputStreamReader(...) | -| A.java:73:50:73:54 | input : InputStream | semmle.label | input : InputStream | -| A.java:74:24:74:28 | input | semmle.label | input | -| A.java:75:24:75:51 | new InputStreamReader(...) | semmle.label | new InputStreamReader(...) | -| A.java:75:46:75:50 | input : InputStream | semmle.label | input : InputStream | -| A.java:90:25:90:45 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| A.java:91:26:91:30 | input | semmle.label | input | -| A.java:92:30:92:34 | input | semmle.label | input | -| A.java:93:28:93:55 | new InputStreamReader(...) | semmle.label | new InputStreamReader(...) | -| A.java:93:50:93:54 | input : InputStream | semmle.label | input : InputStream | -| A.java:94:24:94:28 | input | semmle.label | input | -| A.java:95:24:95:51 | new InputStreamReader(...) | semmle.label | new InputStreamReader(...) | -| A.java:95:46:95:50 | input : InputStream | semmle.label | input : InputStream | -| B.java:7:31:7:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| B.java:8:29:8:39 | inputStream | semmle.label | inputStream | -| B.java:12:31:12:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| B.java:14:5:14:15 | inputStream : InputStream | semmle.label | inputStream : InputStream | -| B.java:14:22:14:26 | bytes [post update] : byte[] | semmle.label | bytes [post update] : byte[] | -| B.java:15:23:15:27 | bytes | semmle.label | bytes | -| B.java:19:31:19:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| B.java:21:5:21:15 | inputStream : InputStream | semmle.label | inputStream : InputStream | -| B.java:21:22:21:26 | bytes [post update] : byte[] | semmle.label | bytes [post update] : byte[] | -| B.java:23:29:23:29 | s | semmle.label | s | -| B.java:27:31:27:51 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | -| B.java:29:5:29:15 | inputStream : InputStream | semmle.label | inputStream : InputStream | -| B.java:29:22:29:26 | bytes [post update] : byte[] | semmle.label | bytes [post update] : byte[] | -| B.java:31:23:31:23 | s | semmle.label | s | -| C.java:23:17:23:44 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| C.java:24:13:24:16 | data | semmle.label | data | -| C.java:25:19:25:22 | data | semmle.label | data | -| C.java:26:25:26:28 | data | semmle.label | data | -| C.java:27:17:27:20 | data | semmle.label | data | -| C.java:30:19:30:22 | data | semmle.label | data | -| C.java:31:25:31:28 | data | semmle.label | data | -| C.java:32:31:32:34 | data | semmle.label | data | -| C.java:33:23:33:26 | data | semmle.label | data | -| C.java:38:17:38:44 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| C.java:43:25:43:28 | data | semmle.label | data | -| C.java:46:3:46:4 | jr | semmle.label | jr | -| C.java:51:17:51:44 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| C.java:53:3:53:3 | r | semmle.label | r | -| C.java:54:3:54:3 | r | semmle.label | r | -| C.java:55:3:55:3 | r | semmle.label | r | -| C.java:60:18:60:45 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| C.java:61:30:61:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | semmle.label | new ByteArrayInputStream(...) : ByteArrayInputStream | -| C.java:61:55:61:59 | bytes : byte[] | semmle.label | bytes : byte[] | -| C.java:63:3:63:14 | hessianInput | semmle.label | hessianInput | -| C.java:64:3:64:14 | hessianInput | semmle.label | hessianInput | -| C.java:69:18:69:45 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| C.java:70:30:70:60 | new ByteArrayInputStream(...) : ByteArrayInputStream | semmle.label | new ByteArrayInputStream(...) : ByteArrayInputStream | -| C.java:70:55:70:59 | bytes : byte[] | semmle.label | bytes : byte[] | -| C.java:72:3:72:14 | hessianInput | semmle.label | hessianInput | -| C.java:73:3:73:14 | hessianInput | semmle.label | hessianInput | -| C.java:79:26:79:71 | new StringReader(...) | semmle.label | new StringReader(...) | -| C.java:79:43:79:70 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| C.java:84:27:84:54 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| C.java:85:29:85:68 | new ByteArrayInputStream(...) : ByteArrayInputStream | semmle.label | new ByteArrayInputStream(...) : ByteArrayInputStream | -| C.java:85:54:85:67 | serializedData : byte[] | semmle.label | serializedData : byte[] | -| C.java:87:3:87:13 | burlapInput | semmle.label | burlapInput | -| C.java:91:3:91:14 | burlapInput1 | semmle.label | burlapInput1 | -| TestMessageBodyReader.java:20:55:20:78 | entityStream : InputStream | semmle.label | entityStream : InputStream | -| TestMessageBodyReader.java:22:18:22:52 | new ObjectInputStream(...) | semmle.label | new ObjectInputStream(...) | -| TestMessageBodyReader.java:22:40:22:51 | entityStream : InputStream | semmle.label | entityStream : InputStream | -#select -| A.java:15:12:15:26 | readObject(...) | A.java:13:31:13:51 | getInputStream(...) : InputStream | A.java:15:12:15:13 | in | Unsafe deserialization of $@. | A.java:13:31:13:51 | getInputStream(...) | user input | -| A.java:21:12:21:28 | readUnshared(...) | A.java:19:31:19:51 | getInputStream(...) : InputStream | A.java:21:12:21:13 | in | Unsafe deserialization of $@. | A.java:19:31:19:51 | getInputStream(...) | user input | -| A.java:27:12:27:25 | readObject(...) | A.java:25:31:25:51 | getInputStream(...) : InputStream | A.java:27:12:27:12 | d | Unsafe deserialization of $@. | A.java:25:31:25:51 | getInputStream(...) | user input | -| A.java:34:12:34:29 | fromXML(...) | A.java:32:31:32:51 | getInputStream(...) : InputStream | A.java:34:23:34:28 | reader | Unsafe deserialization of $@. | A.java:32:31:32:51 | getInputStream(...) | user input | -| A.java:40:12:40:42 | readObject(...) | A.java:39:29:39:49 | getInputStream(...) : InputStream | A.java:40:28:40:32 | input | Unsafe deserialization of $@. | A.java:39:29:39:49 | getInputStream(...) | user input | -| A.java:41:12:41:48 | readObjectOrNull(...) | A.java:39:29:39:49 | getInputStream(...) : InputStream | A.java:41:34:41:38 | input | Unsafe deserialization of $@. | A.java:39:29:39:49 | getInputStream(...) | user input | -| A.java:42:16:42:45 | readClassAndObject(...) | A.java:39:29:39:49 | getInputStream(...) : InputStream | A.java:42:40:42:44 | input | Unsafe deserialization of $@. | A.java:39:29:39:49 | getInputStream(...) | user input | -| A.java:61:16:61:31 | load(...) | A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:61:26:61:30 | input | Unsafe deserialization of $@. | A.java:60:25:60:45 | getInputStream(...) | user input | -| A.java:62:17:62:35 | loadAll(...) | A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:62:30:62:34 | input | Unsafe deserialization of $@. | A.java:60:25:60:45 | getInputStream(...) | user input | -| A.java:63:17:63:56 | parse(...) | A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:63:28:63:55 | new InputStreamReader(...) | Unsafe deserialization of $@. | A.java:60:25:60:45 | getInputStream(...) | user input | -| A.java:64:12:64:38 | loadAs(...) | A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:64:24:64:28 | input | Unsafe deserialization of $@. | A.java:60:25:60:45 | getInputStream(...) | user input | -| A.java:65:12:65:61 | loadAs(...) | A.java:60:25:60:45 | getInputStream(...) : InputStream | A.java:65:24:65:51 | new InputStreamReader(...) | Unsafe deserialization of $@. | A.java:60:25:60:45 | getInputStream(...) | user input | -| A.java:71:16:71:31 | load(...) | A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:71:26:71:30 | input | Unsafe deserialization of $@. | A.java:70:25:70:45 | getInputStream(...) | user input | -| A.java:72:17:72:35 | loadAll(...) | A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:72:30:72:34 | input | Unsafe deserialization of $@. | A.java:70:25:70:45 | getInputStream(...) | user input | -| A.java:73:17:73:56 | parse(...) | A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:73:28:73:55 | new InputStreamReader(...) | Unsafe deserialization of $@. | A.java:70:25:70:45 | getInputStream(...) | user input | -| A.java:74:12:74:38 | loadAs(...) | A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:74:24:74:28 | input | Unsafe deserialization of $@. | A.java:70:25:70:45 | getInputStream(...) | user input | -| A.java:75:12:75:61 | loadAs(...) | A.java:70:25:70:45 | getInputStream(...) : InputStream | A.java:75:24:75:51 | new InputStreamReader(...) | Unsafe deserialization of $@. | A.java:70:25:70:45 | getInputStream(...) | user input | -| A.java:91:16:91:31 | load(...) | A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:91:26:91:30 | input | Unsafe deserialization of $@. | A.java:90:25:90:45 | getInputStream(...) | user input | -| A.java:92:17:92:35 | loadAll(...) | A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:92:30:92:34 | input | Unsafe deserialization of $@. | A.java:90:25:90:45 | getInputStream(...) | user input | -| A.java:93:17:93:56 | parse(...) | A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:93:28:93:55 | new InputStreamReader(...) | Unsafe deserialization of $@. | A.java:90:25:90:45 | getInputStream(...) | user input | -| A.java:94:12:94:38 | loadAs(...) | A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:94:24:94:28 | input | Unsafe deserialization of $@. | A.java:90:25:90:45 | getInputStream(...) | user input | -| A.java:95:12:95:61 | loadAs(...) | A.java:90:25:90:45 | getInputStream(...) : InputStream | A.java:95:24:95:51 | new InputStreamReader(...) | Unsafe deserialization of $@. | A.java:90:25:90:45 | getInputStream(...) | user input | -| B.java:8:12:8:46 | parseObject(...) | B.java:7:31:7:51 | getInputStream(...) : InputStream | B.java:8:29:8:39 | inputStream | Unsafe deserialization of $@. | B.java:7:31:7:51 | getInputStream(...) | user input | -| B.java:15:12:15:28 | parse(...) | B.java:12:31:12:51 | getInputStream(...) : InputStream | B.java:15:23:15:27 | bytes | Unsafe deserialization of $@. | B.java:12:31:12:51 | getInputStream(...) | user input | -| B.java:23:12:23:30 | parseObject(...) | B.java:19:31:19:51 | getInputStream(...) : InputStream | B.java:23:29:23:29 | s | Unsafe deserialization of $@. | B.java:19:31:19:51 | getInputStream(...) | user input | -| B.java:31:12:31:24 | parse(...) | B.java:27:31:27:51 | getInputStream(...) : InputStream | B.java:31:23:31:23 | s | Unsafe deserialization of $@. | B.java:27:31:27:51 | getInputStream(...) | user input | -| C.java:24:3:24:17 | load(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:24:13:24:16 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input | -| C.java:25:3:25:23 | loadStream(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:25:19:25:22 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input | -| C.java:26:3:26:43 | loadStreamOfType(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:26:25:26:28 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input | -| C.java:27:3:27:35 | loadType(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:27:17:27:20 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input | -| C.java:30:3:30:23 | load(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:30:19:30:22 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input | -| C.java:31:3:31:29 | loadStream(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:31:25:31:28 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input | -| C.java:32:3:32:49 | loadStreamOfType(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:32:31:32:34 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input | -| C.java:33:3:33:41 | loadType(...) | C.java:23:17:23:44 | getParameter(...) : String | C.java:33:23:33:26 | data | Unsafe deserialization of $@. | C.java:23:17:23:44 | getParameter(...) | user input | -| C.java:43:3:43:29 | jsonToJava(...) | C.java:38:17:38:44 | getParameter(...) : String | C.java:43:25:43:28 | data | Unsafe deserialization of $@. | C.java:38:17:38:44 | getParameter(...) | user input | -| C.java:46:3:46:17 | readObject(...) | C.java:38:17:38:44 | getParameter(...) : String | C.java:46:3:46:4 | jr | Unsafe deserialization of $@. | C.java:38:17:38:44 | getParameter(...) | user input | -| C.java:53:3:53:10 | read(...) | C.java:51:17:51:44 | getParameter(...) : String | C.java:53:3:53:3 | r | Unsafe deserialization of $@. | C.java:51:17:51:44 | getParameter(...) | user input | -| C.java:54:3:54:22 | read(...) | C.java:51:17:51:44 | getParameter(...) : String | C.java:54:3:54:3 | r | Unsafe deserialization of $@. | C.java:51:17:51:44 | getParameter(...) | user input | -| C.java:55:3:55:36 | read(...) | C.java:51:17:51:44 | getParameter(...) : String | C.java:55:3:55:3 | r | Unsafe deserialization of $@. | C.java:51:17:51:44 | getParameter(...) | user input | -| C.java:63:3:63:27 | readObject(...) | C.java:60:18:60:45 | getParameter(...) : String | C.java:63:3:63:14 | hessianInput | Unsafe deserialization of $@. | C.java:60:18:60:45 | getParameter(...) | user input | -| C.java:64:3:64:39 | readObject(...) | C.java:60:18:60:45 | getParameter(...) : String | C.java:64:3:64:14 | hessianInput | Unsafe deserialization of $@. | C.java:60:18:60:45 | getParameter(...) | user input | -| C.java:72:3:72:27 | readObject(...) | C.java:69:18:69:45 | getParameter(...) : String | C.java:72:3:72:14 | hessianInput | Unsafe deserialization of $@. | C.java:69:18:69:45 | getParameter(...) | user input | -| C.java:73:3:73:39 | readObject(...) | C.java:69:18:69:45 | getParameter(...) : String | C.java:73:3:73:14 | hessianInput | Unsafe deserialization of $@. | C.java:69:18:69:45 | getParameter(...) | user input | -| C.java:79:3:79:72 | unmarshal(...) | C.java:79:43:79:70 | getParameter(...) : String | C.java:79:26:79:71 | new StringReader(...) | Unsafe deserialization of $@. | C.java:79:43:79:70 | getParameter(...) | user input | -| C.java:87:3:87:26 | readObject(...) | C.java:84:27:84:54 | getParameter(...) : String | C.java:87:3:87:13 | burlapInput | Unsafe deserialization of $@. | C.java:84:27:84:54 | getParameter(...) | user input | -| C.java:91:3:91:27 | readObject(...) | C.java:84:27:84:54 | getParameter(...) : String | C.java:91:3:91:14 | burlapInput1 | Unsafe deserialization of $@. | C.java:84:27:84:54 | getParameter(...) | user input | -| TestMessageBodyReader.java:22:18:22:65 | readObject(...) | TestMessageBodyReader.java:20:55:20:78 | entityStream : InputStream | TestMessageBodyReader.java:22:18:22:52 | new ObjectInputStream(...) | Unsafe deserialization of $@. | TestMessageBodyReader.java:20:55:20:78 | entityStream | user input | diff --git a/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.ql b/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.ql new file mode 100644 index 00000000000..a2ba654a540 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.ql @@ -0,0 +1,18 @@ +import java +import semmle.code.java.security.UnsafeDeserializationQuery +import TestUtilities.InlineExpectationsTest + +class UnsafeDeserializationTest extends InlineExpectationsTest { + UnsafeDeserializationTest() { this = "UnsafeDeserializationTest" } + + override string getARelevantTag() { result = "unsafeDeserialization" } + + override predicate hasActualResult(Location location, string element, string tag, string value) { + tag = "unsafeDeserialization" and + exists(DataFlow::Node sink, UnsafeDeserializationConfig conf | conf.hasFlowTo(sink) | + sink.getLocation() = location and + element = sink.toString() and + value = "" + ) + } +} diff --git a/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.qlref b/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.qlref deleted file mode 100644 index 30612dac5a5..00000000000 --- a/java/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.qlref +++ /dev/null @@ -1 +0,0 @@ -Security/CWE/CWE-502/UnsafeDeserialization.ql diff --git a/java/ql/test/query-tests/security/CWE-502/options b/java/ql/test/query-tests/security/CWE-502/options index 03027487dce..fc5cac9e843 100644 --- a/java/ql/test/query-tests/security/CWE-502/options +++ b/java/ql/test/query-tests/security/CWE-502/options @@ -1 +1 @@ -//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/snakeyaml-1.21:${testdir}/../../../stubs/xstream-1.4.10:${testdir}/../../../stubs/kryo-4.0.2:${testdir}/../../../stubs/jsr311-api-1.1.1:${testdir}/../../../stubs/fastjson-1.2.74:${testdir}/../../../stubs/springframework-5.3.8:${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/jyaml-1.3:${testdir}/../../../stubs/json-io-4.10.0:${testdir}/../../../stubs/yamlbeans-1.09:${testdir}/../../../stubs/hessian-4.0.38:${testdir}/../../../stubs/castor-1.4.1 +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/snakeyaml-1.21:${testdir}/../../../stubs/xstream-1.4.10:${testdir}/../../../stubs/kryo-4.0.2:${testdir}/../../../stubs/jsr311-api-1.1.1:${testdir}/../../../stubs/fastjson-1.2.74:${testdir}/../../../stubs/springframework-5.3.8:${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/jyaml-1.3:${testdir}/../../../stubs/json-io-4.10.0:${testdir}/../../../stubs/yamlbeans-1.09:${testdir}/../../../stubs/hessian-4.0.38:${testdir}/../../../stubs/castor-1.4.1:${testdir}/../../../stubs/jackson-databind-2.10 diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/annotation/JsonTypeInfo.java new file mode 100644 index 00000000000..fb6733f4019 --- /dev/null +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -0,0 +1,21 @@ +package com.fasterxml.jackson.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonTypeInfo { + JsonTypeInfo.Id use(); + + public static enum Id { + CLASS("@class"), + MINIMAL_CLASS("@c"); + + private Id(String defProp) { } + + public String getDefaultPropertyName() { return null; } + } +} diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonFactory.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonFactory.java index 06f71ab187d..12696cd4397 100644 --- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonFactory.java +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonFactory.java @@ -9,4 +9,8 @@ public class JsonFactory { public JsonGenerator createGenerator(Writer writer) { return new JsonGenerator(); } + + public JsonParser createParser(String content) { + return null; + } } diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonParser.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonParser.java new file mode 100644 index 00000000000..2c5527d50ab --- /dev/null +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/JsonParser.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.core; + +public abstract class JsonParser {} diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/TreeNode.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/TreeNode.java new file mode 100644 index 00000000000..0d89838457a --- /dev/null +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/core/TreeNode.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.core; + +public interface TreeNode {} diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JavaType.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JavaType.java new file mode 100644 index 00000000000..51fe62ca4b1 --- /dev/null +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JavaType.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.databind; + +public class JavaType {} \ No newline at end of file diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java index b04572cd4da..06602e943f5 100644 --- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java @@ -1,8 +1,8 @@ package com.fasterxml.jackson.databind; import java.util.*; +import com.fasterxml.jackson.core.TreeNode; -public abstract class JsonNode implements Iterable { - public JsonNode() { - } +public abstract class JsonNode implements TreeNode, Iterable { + public JsonNode() {} } diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java index ac427ef01c9..929676e6456 100644 --- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java @@ -25,4 +25,8 @@ public class MappingIterator implements Iterator, Closeable { public void close() throws IOException { } + + public List readAll() { + return null; + } } diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java index 71dc99a351d..754fcc43144 100644 --- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java @@ -1,5 +1,9 @@ package com.fasterxml.jackson.databind; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; +import java.lang.reflect.Type; import java.io.*; import java.util.*; @@ -38,4 +42,36 @@ public class ObjectMapper { public T convertValue(Object fromValue, Class toValueType) throws IllegalArgumentException { return null; } + + public ObjectMapper setPolymorphicTypeValidator(PolymorphicTypeValidator ptv) { + return null; + } + + public ObjectMapper enableDefaultTyping() { + return null; + } + + public T readValue(String content, Class valueType) { + return null; + } + + public T readValue(String content, JavaType valueType) { + return null; + } + + public MappingIterator readValues(JsonParser p, Class valueType) { + return null; + } + + public T treeToValue(TreeNode n, Class valueType) { + return null; + } + + public JsonNode readTree(String content) { + return null; + } + + public JavaType constructType(Type t) { + return null; + } } diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/cfg/MapperBuilder.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/cfg/MapperBuilder.java new file mode 100644 index 00000000000..db2c24c4362 --- /dev/null +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/cfg/MapperBuilder.java @@ -0,0 +1,9 @@ +package com.fasterxml.jackson.databind.cfg; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; + +public abstract class MapperBuilder> { + public M build() { return null; } + public B polymorphicTypeValidator(PolymorphicTypeValidator ptv) { return null; } +} diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/json/JsonMapper.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/json/JsonMapper.java new file mode 100644 index 00000000000..adec92a9210 --- /dev/null +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/json/JsonMapper.java @@ -0,0 +1,9 @@ +package com.fasterxml.jackson.databind.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.cfg.MapperBuilder; + +public class JsonMapper extends ObjectMapper { + public static JsonMapper.Builder builder() { return null; } + public static class Builder extends MapperBuilder {} +} \ No newline at end of file diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/jsontype/BasicPolymorphicTypeValidator.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/jsontype/BasicPolymorphicTypeValidator.java new file mode 100644 index 00000000000..243cd467b4b --- /dev/null +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/jsontype/BasicPolymorphicTypeValidator.java @@ -0,0 +1,10 @@ +package com.fasterxml.jackson.databind.jsontype; + +public class BasicPolymorphicTypeValidator extends PolymorphicTypeValidator { + public static BasicPolymorphicTypeValidator.Builder builder() { return null; } + + public static class Builder { + public BasicPolymorphicTypeValidator.Builder allowIfSubType(final String prefixForSubType) { return null; } + public BasicPolymorphicTypeValidator build() { return null; } + } +} diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/jsontype/PolymorphicTypeValidator.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/jsontype/PolymorphicTypeValidator.java new file mode 100644 index 00000000000..37e68d2c429 --- /dev/null +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/jsontype/PolymorphicTypeValidator.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.databind.jsontype; + +public abstract class PolymorphicTypeValidator {} diff --git a/java/ql/test/stubs/mvel2-2.4.7/org/mvel2/jsr223/MvelCompiledScript.java b/java/ql/test/stubs/mvel2-2.4.7/org/mvel2/jsr223/MvelCompiledScript.java index a4be37ada32..771f8b83345 100644 --- a/java/ql/test/stubs/mvel2-2.4.7/org/mvel2/jsr223/MvelCompiledScript.java +++ b/java/ql/test/stubs/mvel2-2.4.7/org/mvel2/jsr223/MvelCompiledScript.java @@ -3,9 +3,17 @@ package org.mvel2.jsr223; import java.io.Serializable; import javax.script.CompiledScript; import javax.script.ScriptContext; +import javax.script.ScriptEngine; import javax.script.ScriptException; public class MvelCompiledScript extends CompiledScript { public MvelCompiledScript(MvelScriptEngine engine, Serializable compiledScript) {} - public Object eval(ScriptContext context) throws ScriptException { return null; } -} \ No newline at end of file + + public Object eval(ScriptContext context) throws ScriptException { + return null; + } + + public ScriptEngine getEngine() { + return null; + } +} diff --git a/java/ql/test/stubs/springframework-5.3.8/org/springframework/http/MediaType.java b/java/ql/test/stubs/springframework-5.3.8/org/springframework/http/MediaType.java index 08309d8012d..7c0c1e18ffa 100644 --- a/java/ql/test/stubs/springframework-5.3.8/org/springframework/http/MediaType.java +++ b/java/ql/test/stubs/springframework-5.3.8/org/springframework/http/MediaType.java @@ -62,33 +62,33 @@ public class MediaType extends MimeType implements Serializable public static MediaType asMediaType(MimeType p0){ return null; } public static MediaType parseMediaType(String p0){ return null; } public static MediaType valueOf(String p0){ return null; } - public static String ALL_VALUE = null; - public static String APPLICATION_ATOM_XML_VALUE = null; - public static String APPLICATION_CBOR_VALUE = null; - public static String APPLICATION_FORM_URLENCODED_VALUE = null; - public static String APPLICATION_JSON_UTF8_VALUE = null; - public static String APPLICATION_JSON_VALUE = null; - public static String APPLICATION_NDJSON_VALUE = null; - public static String APPLICATION_OCTET_STREAM_VALUE = null; - public static String APPLICATION_PDF_VALUE = null; - public static String APPLICATION_PROBLEM_JSON_UTF8_VALUE = null; - public static String APPLICATION_PROBLEM_JSON_VALUE = null; - public static String APPLICATION_PROBLEM_XML_VALUE = null; - public static String APPLICATION_RSS_XML_VALUE = null; - public static String APPLICATION_STREAM_JSON_VALUE = null; - public static String APPLICATION_XHTML_XML_VALUE = null; - public static String APPLICATION_XML_VALUE = null; - public static String IMAGE_GIF_VALUE = null; - public static String IMAGE_JPEG_VALUE = null; - public static String IMAGE_PNG_VALUE = null; - public static String MULTIPART_FORM_DATA_VALUE = null; - public static String MULTIPART_MIXED_VALUE = null; - public static String MULTIPART_RELATED_VALUE = null; - public static String TEXT_EVENT_STREAM_VALUE = null; - public static String TEXT_HTML_VALUE = null; - public static String TEXT_MARKDOWN_VALUE = null; - public static String TEXT_PLAIN_VALUE = null; - public static String TEXT_XML_VALUE = null; + public static final String ALL_VALUE = ""; + public static final String APPLICATION_ATOM_XML_VALUE = ""; + public static final String APPLICATION_CBOR_VALUE = ""; + public static final String APPLICATION_FORM_URLENCODED_VALUE = ""; + public static final String APPLICATION_JSON_UTF8_VALUE = ""; + public static final String APPLICATION_JSON_VALUE = ""; + public static final String APPLICATION_NDJSON_VALUE = ""; + public static final String APPLICATION_OCTET_STREAM_VALUE = ""; + public static final String APPLICATION_PDF_VALUE = ""; + public static final String APPLICATION_PROBLEM_JSON_UTF8_VALUE = ""; + public static final String APPLICATION_PROBLEM_JSON_VALUE = ""; + public static final String APPLICATION_PROBLEM_XML_VALUE = ""; + public static final String APPLICATION_RSS_XML_VALUE = ""; + public static final String APPLICATION_STREAM_JSON_VALUE = ""; + public static final String APPLICATION_XHTML_XML_VALUE = ""; + public static final String APPLICATION_XML_VALUE = ""; + public static final String IMAGE_GIF_VALUE = ""; + public static final String IMAGE_JPEG_VALUE = ""; + public static final String IMAGE_PNG_VALUE = ""; + public static final String MULTIPART_FORM_DATA_VALUE = ""; + public static final String MULTIPART_MIXED_VALUE = ""; + public static final String MULTIPART_RELATED_VALUE = ""; + public static final String TEXT_EVENT_STREAM_VALUE = ""; + public static final String TEXT_HTML_VALUE = ""; + public static final String TEXT_MARKDOWN_VALUE = ""; + public static final String TEXT_PLAIN_VALUE = ""; + public static final String TEXT_XML_VALUE = ""; public static String toString(Collection p0){ return null; } public static void sortByQualityValue(List p0){} public static void sortBySpecificity(List p0){} diff --git a/java/ql/test/stubs/springframework-5.3.8/org/springframework/stereotype/Component.java b/java/ql/test/stubs/springframework-5.3.8/org/springframework/stereotype/Component.java index ef4dd7726e0..bd6f1e61695 100644 --- a/java/ql/test/stubs/springframework-5.3.8/org/springframework/stereotype/Component.java +++ b/java/ql/test/stubs/springframework-5.3.8/org/springframework/stereotype/Component.java @@ -6,4 +6,4 @@ import java.lang.annotation.*; @Retention(value=RetentionPolicy.RUNTIME) @Documented @Indexed -public @interface Component { } +public @interface Component { } \ No newline at end of file diff --git a/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/PathVariable.java b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/PathVariable.java index 52411ebce00..1de6872d089 100644 --- a/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/PathVariable.java +++ b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/PathVariable.java @@ -11,4 +11,5 @@ import java.lang.annotation.Target; @Documented public @interface PathVariable { + String value() default ""; } \ No newline at end of file diff --git a/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/PostMapping.java b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/PostMapping.java index 960d715cecd..f5a304038b3 100644 --- a/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/PostMapping.java +++ b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/PostMapping.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.web.bind.annotation; import java.lang.annotation.Documented; @@ -6,11 +22,69 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation for mapping HTTP {@code POST} requests onto specific handler + * methods. + * + *

    Specifically, {@code @PostMapping} is a composed annotation that + * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.POST)}. + * + * @author Sam Brannen + * @since 4.3 + * @see GetMapping + * @see PutMapping + * @see DeleteMapping + * @see PatchMapping + * @see RequestMapping + */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @RequestMapping(method = RequestMethod.POST) public @interface PostMapping { - String[] value() default {}; -} \ No newline at end of file + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/RequestMapping.java b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/RequestMapping.java index 093065ca5f1..eb3f0d962e1 100644 --- a/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/RequestMapping.java +++ b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/RequestMapping.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.web.bind.annotation; import java.lang.annotation.Documented; @@ -5,28 +21,189 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; + import org.springframework.core.annotation.AliasFor; -@Target(value={ElementType.METHOD,ElementType.TYPE}) -@Retention(value=RetentionPolicy.RUNTIME) +/** + * Annotation for mapping web requests onto methods in request-handling classes + * with flexible method signatures. + * + *

    Both Spring MVC and Spring WebFlux support this annotation through a + * {@code RequestMappingHandlerMapping} and {@code RequestMappingHandlerAdapter} + * in their respective modules and package structure. For the exact list of + * supported handler method arguments and return types in each, please use the + * reference documentation links below: + *

    + * + *

    Note: This annotation can be used both at the class and + * at the method level. In most cases, at the method level applications will + * prefer to use one of the HTTP method specific variants + * {@link GetMapping @GetMapping}, {@link PostMapping @PostMapping}, + * {@link PutMapping @PutMapping}, {@link DeleteMapping @DeleteMapping}, or + * {@link PatchMapping @PatchMapping}.

    + * + *

    NOTE: When using controller interfaces (e.g. for AOP proxying), + * make sure to consistently put all your mapping annotations - such as + * {@code @RequestMapping} and {@code @SessionAttributes} - on + * the controller interface rather than on the implementation class. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @author Sam Brannen + * @since 2.5 + * @see GetMapping + * @see PostMapping + * @see PutMapping + * @see DeleteMapping + * @see PatchMapping + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface RequestMapping { - String name() default ""; - @AliasFor("path") - String[] value() default {}; + /** + * Assign a name to this mapping. + *

    Supported at the type level as well as at the method level! + * When used on both levels, a combined name is derived by concatenation + * with "#" as separator. + * @see org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder + * @see org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy + */ + String name() default ""; - @AliasFor("value") - String[] path() default {}; + /** + * The primary mapping expressed by this annotation. + *

    This is an alias for {@link #path}. For example, + * {@code @RequestMapping("/foo")} is equivalent to + * {@code @RequestMapping(path="/foo")}. + *

    Supported at the type level as well as at the method level! + * When used at the type level, all method-level mappings inherit + * this primary mapping, narrowing it for a specific handler method. + *

    NOTE: A handler method that is not mapped to any path + * explicitly is effectively mapped to an empty path. + */ + @AliasFor("path") + String[] value() default {}; - RequestMethod[] method() default {}; + /** + * The path mapping URIs (e.g. {@code "/profile"}). + *

    Ant-style path patterns are also supported (e.g. {@code "/profile/**"}). + * At the method level, relative paths (e.g. {@code "edit"}) are supported + * within the primary mapping expressed at the type level. + * Path mapping URIs may contain placeholders (e.g. "/${profile_path}"). + *

    Supported at the type level as well as at the method level! + * When used at the type level, all method-level mappings inherit + * this primary mapping, narrowing it for a specific handler method. + *

    NOTE: A handler method that is not mapped to any path + * explicitly is effectively mapped to an empty path. + * @since 4.2 + */ + @AliasFor("value") + String[] path() default {}; - String[] params() default {}; + /** + * The HTTP request methods to map to, narrowing the primary mapping: + * GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE. + *

    Supported at the type level as well as at the method level! + * When used at the type level, all method-level mappings inherit this + * HTTP method restriction. + */ + RequestMethod[] method() default {}; - String[] headers() default {}; + /** + * The parameters of the mapped request, narrowing the primary mapping. + *

    Same format for any environment: a sequence of "myParam=myValue" style + * expressions, with a request only mapped if each such parameter is found + * to have the given value. Expressions can be negated by using the "!=" operator, + * as in "myParam!=myValue". "myParam" style expressions are also supported, + * with such parameters having to be present in the request (allowed to have + * any value). Finally, "!myParam" style expressions indicate that the + * specified parameter is not supposed to be present in the request. + *

    Supported at the type level as well as at the method level! + * When used at the type level, all method-level mappings inherit this + * parameter restriction. + */ + String[] params() default {}; - String[] consumes() default {}; + /** + * The headers of the mapped request, narrowing the primary mapping. + *

    Same format for any environment: a sequence of "My-Header=myValue" style + * expressions, with a request only mapped if each such header is found + * to have the given value. Expressions can be negated by using the "!=" operator, + * as in "My-Header!=myValue". "My-Header" style expressions are also supported, + * with such headers having to be present in the request (allowed to have + * any value). Finally, "!My-Header" style expressions indicate that the + * specified header is not supposed to be present in the request. + *

    Also supports media type wildcards (*), for headers such as Accept + * and Content-Type. For instance, + *

    +	 * @RequestMapping(value = "/something", headers = "content-type=text/*")
    +	 * 
    + * will match requests with a Content-Type of "text/html", "text/plain", etc. + *

    Supported at the type level as well as at the method level! + * When used at the type level, all method-level mappings inherit this + * header restriction. + * @see org.springframework.http.MediaType + */ + String[] headers() default {}; + + /** + * Narrows the primary mapping by media types that can be consumed by the + * mapped handler. Consists of one or more media types one of which must + * match to the request {@code Content-Type} header. Examples: + *

    +	 * consumes = "text/plain"
    +	 * consumes = {"text/plain", "application/*"}
    +	 * consumes = MediaType.TEXT_PLAIN_VALUE
    +	 * 
    + * Expressions can be negated by using the "!" operator, as in + * "!text/plain", which matches all requests with a {@code Content-Type} + * other than "text/plain". + *

    Supported at the type level as well as at the method level! + * If specified at both levels, the method level consumes condition overrides + * the type level condition. + * @see org.springframework.http.MediaType + * @see javax.servlet.http.HttpServletRequest#getContentType() + */ + String[] consumes() default {}; + + /** + * Narrows the primary mapping by media types that can be produced by the + * mapped handler. Consists of one or more media types one of which must + * be chosen via content negotiation against the "acceptable" media types + * of the request. Typically those are extracted from the {@code "Accept"} + * header but may be derived from query parameters, or other. Examples: + *

    +	 * produces = "text/plain"
    +	 * produces = {"text/plain", "application/*"}
    +	 * produces = MediaType.TEXT_PLAIN_VALUE
    +	 * produces = "text/plain;charset=UTF-8"
    +	 * 
    + *

    If a declared media type contains a parameter (e.g. "charset=UTF-8", + * "type=feed", "type=entry") and if a compatible media type from the request + * has that parameter too, then the parameter values must match. Otherwise + * if the media type from the request does not contain the parameter, it is + * assumed the client accepts any value. + *

    Expressions can be negated by using the "!" operator, as in "!text/plain", + * which matches all requests with a {@code Accept} other than "text/plain". + *

    Supported at the type level as well as at the method level! + * If specified at both levels, the method level produces condition overrides + * the type level condition. + * @see org.springframework.http.MediaType + */ + String[] produces() default {}; - String[] produces() default {}; } diff --git a/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/RestController.java b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/RestController.java new file mode 100644 index 00000000000..68abaf310f5 --- /dev/null +++ b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/bind/annotation/RestController.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Controller; + +/** + * A convenience annotation that is itself annotated with + * {@link Controller @Controller} and {@link ResponseBody @ResponseBody}. + *

    + * Types that carry this annotation are treated as controllers where + * {@link RequestMapping @RequestMapping} methods assume + * {@link ResponseBody @ResponseBody} semantics by default. + * + *

    NOTE: {@code @RestController} is processed if an appropriate + * {@code HandlerMapping}-{@code HandlerAdapter} pair is configured such as the + * {@code RequestMappingHandlerMapping}-{@code RequestMappingHandlerAdapter} + * pair which are the default in the MVC Java config and the MVC namespace. + * + * @author Rossen Stoyanchev + * @author Sam Brannen + * @since 4.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Controller +@ResponseBody +public @interface RestController { + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any (or empty String otherwise) + * @since 4.0.1 + */ + @AliasFor(annotation = Controller.class) + String value() default ""; + +} diff --git a/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/reactive/function/client/MimeType.java b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/reactive/function/client/MimeType.java new file mode 100644 index 00000000000..1c77c099eef --- /dev/null +++ b/java/ql/test/stubs/springframework-5.3.8/org/springframework/web/reactive/function/client/MimeType.java @@ -0,0 +1,329 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.util; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeSet; + +import org.springframework.lang.Nullable; + +/** + * Represents a MIME Type, as originally defined in RFC 2046 and subsequently + * used in other Internet protocols including HTTP. + * + *

    This class, however, does not contain support for the q-parameters used + * in HTTP content negotiation. Those can be found in the subclass + * {@code org.springframework.http.MediaType} in the {@code spring-web} module. + * + *

    Consists of a {@linkplain #getType() type} and a {@linkplain #getSubtype() subtype}. + * Also has functionality to parse MIME Type values from a {@code String} using + * {@link #valueOf(String)}. For more parsing options see {@link MimeTypeUtils}. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @author Rossen Stoyanchev + * @author Sam Brannen + * @since 4.0 + * @see MimeTypeUtils + */ +public class MimeType implements Comparable, Serializable { + + protected static final String WILDCARD_TYPE = ""; + + /** + * Create a new {@code MimeType} for the given primary type. + *

    The {@linkplain #getSubtype() subtype} is set to "*", + * and the parameters are empty. + * @param type the primary type + * @throws IllegalArgumentException if any of the parameters contains illegal characters + */ + public MimeType(String type) { + } + + /** + * Create a new {@code MimeType} for the given primary type and subtype. + *

    The parameters are empty. + * @param type the primary type + * @param subtype the subtype + * @throws IllegalArgumentException if any of the parameters contains illegal characters + */ + public MimeType(String type, String subtype) { + } + + /** + * Create a new {@code MimeType} for the given type, subtype, and character set. + * @param type the primary type + * @param subtype the subtype + * @param charset the character set + * @throws IllegalArgumentException if any of the parameters contains illegal characters + */ + public MimeType(String type, String subtype, Charset charset) { + } + + /** + * Copy-constructor that copies the type, subtype, parameters of the given {@code MimeType}, + * and allows to set the specified character set. + * @param other the other MimeType + * @param charset the character set + * @throws IllegalArgumentException if any of the parameters contains illegal characters + * @since 4.3 + */ + public MimeType(MimeType other, Charset charset) { + } + + /** + * Copy-constructor that copies the type and subtype of the given {@code MimeType}, + * and allows for different parameter. + * @param other the other MimeType + * @param parameters the parameters (may be {@code null}) + * @throws IllegalArgumentException if any of the parameters contains illegal characters + */ + public MimeType(MimeType other, @Nullable Map parameters) { + } + + /** + * Create a new {@code MimeType} for the given type, subtype, and parameters. + * @param type the primary type + * @param subtype the subtype + * @param parameters the parameters (may be {@code null}) + * @throws IllegalArgumentException if any of the parameters contains illegal characters + */ + public MimeType(String type, String subtype, @Nullable Map parameters) { + } + + /** + * Copy-constructor that copies the type, subtype and parameters of the given {@code MimeType}, + * skipping checks performed in other constructors. + * @param other the other MimeType + * @since 5.3 + */ + protected MimeType(MimeType other) { + } + + protected void checkParameters(String parameter, String value) { + } + + protected String unquote(String s) { + return null; + } + + /** + * Indicates whether the {@linkplain #getType() type} is the wildcard character + * * or not. + */ + public boolean isWildcardType() { + return false; + } + + /** + * Indicates whether the {@linkplain #getSubtype() subtype} is the wildcard + * character * or the wildcard character followed by a suffix + * (e.g. *+xml). + * @return whether the subtype is a wildcard + */ + public boolean isWildcardSubtype() { + return false; + } + + /** + * Indicates whether this MIME Type is concrete, i.e. whether neither the type + * nor the subtype is a wildcard character *. + * @return whether this MIME Type is concrete + */ + public boolean isConcrete() { + return false; + } + + /** + * Return the primary type. + */ + public String getType() { + return null; + } + + /** + * Return the subtype. + */ + public String getSubtype() { + return null; + } + + /** + * Return the subtype suffix as defined in RFC 6839. + * @since 5.3 + */ + @Nullable + public String getSubtypeSuffix() { + return null; + } + + /** + * Return the character set, as indicated by a {@code charset} parameter, if any. + * @return the character set, or {@code null} if not available + * @since 4.3 + */ + @Nullable + public Charset getCharset() { + return null; + } + + /** + * Return a generic parameter value, given a parameter name. + * @param name the parameter name + * @return the parameter value, or {@code null} if not present + */ + @Nullable + public String getParameter(String name) { + return null; + } + + /** + * Return all generic parameter values. + * @return a read-only map (possibly empty, never {@code null}) + */ + public Map getParameters() { + return null; + } + + /** + * Indicate whether this MIME Type includes the given MIME Type. + *

    For instance, {@code text/*} includes {@code text/plain} and {@code text/html}, + * and {@code application/*+xml} includes {@code application/soap+xml}, etc. + * This method is not symmetric. + * @param other the reference MIME Type with which to compare + * @return {@code true} if this MIME Type includes the given MIME Type; + * {@code false} otherwise + */ + public boolean includes(@Nullable MimeType other) { + return false; + } + + /** + * Indicate whether this MIME Type is compatible with the given MIME Type. + *

    For instance, {@code text/*} is compatible with {@code text/plain}, + * {@code text/html}, and vice versa. In effect, this method is similar to + * {@link #includes}, except that it is symmetric. + * @param other the reference MIME Type with which to compare + * @return {@code true} if this MIME Type is compatible with the given MIME Type; + * {@code false} otherwise + */ + public boolean isCompatibleWith(@Nullable MimeType other) { + return false; + } + + /** + * Similar to {@link #equals(Object)} but based on the type and subtype + * only, i.e. ignoring parameters. + * @param other the other mime type to compare to + * @return whether the two mime types have the same type and subtype + * @since 5.1.4 + */ + public boolean equalsTypeAndSubtype(@Nullable MimeType other) { + return false; + } + + /** + * Unlike {@link Collection#contains(Object)} which relies on + * {@link MimeType#equals(Object)}, this method only checks the type and the + * subtype, but otherwise ignores parameters. + * @param mimeTypes the list of mime types to perform the check against + * @return whether the list contains the given mime type + * @since 5.1.4 + */ + public boolean isPresentIn(Collection mimeTypes) { + return false; + } + + + @Override + public boolean equals(@Nullable Object other) { + return false; + } + + /** + * Determine if the parameters in this {@code MimeType} and the supplied + * {@code MimeType} are equal, performing case-insensitive comparisons + * for {@link Charset Charsets}. + * @since 4.2 + */ + private boolean parametersAreEqual(MimeType other) { + return true; + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public String toString() { + return null; + } + + protected void appendTo(StringBuilder builder) { + } + + /** + * Compares this MIME Type to another alphabetically. + * @param other the MIME Type to compare to + * @see MimeTypeUtils#sortBySpecificity(List) + */ + @Override + public int compareTo(MimeType other) { + return 0; + } + + /** + * Parse the given String value into a {@code MimeType} object, + * with this method name following the 'valueOf' naming convention + * (as supported by {@link org.springframework.core.convert.ConversionService}. + * @see MimeTypeUtils#parseMimeType(String) + */ + public static MimeType valueOf(String value) { + return null; + } + + /** + * Comparator to sort {@link MimeType MimeTypes} in order of specificity. + * + * @param the type of mime types that may be compared by this comparator + */ + public static class SpecificityComparator implements Comparator { + + @Override + public int compare(T mimeType1, T mimeType2) { + return 0; + } + + protected int compareParameters(T mimeType1, T mimeType2) { + return 0; + } + } + +} diff --git a/javascript/ql/src/DOM/TargetBlank.ql b/javascript/ql/src/DOM/TargetBlank.ql index 8d16a60bc83..ee3093b71bb 100644 --- a/javascript/ql/src/DOM/TargetBlank.ql +++ b/javascript/ql/src/DOM/TargetBlank.ql @@ -43,8 +43,8 @@ predicate hasDynamicHrefHostAttributeValue(DOM::ElementDefinition elem) { url.regexpMatch(Templating::getDelimiterMatchingRegexpWithPrefix("[^?#]*")) and // ... that does not start with a fixed host or a relative path (common formats) not url.regexpMatch("(?i)((https?:)?//)?[-a-z0-9.]*/.*") and - // .. that is not a call to `url_for` in a Flask application - not url.regexpMatch("\\{\\{\\s*url_for.*") + // .. that is not a call to `url_for` in a Flask / nunjucks application + not url.regexpMatch("\\{\\{\\s*url(_for)?\\(.+\\).*") ) ) } diff --git a/javascript/ql/test/query-tests/DOM/TargetBlank/tst.js b/javascript/ql/test/query-tests/DOM/TargetBlank/tst.js index 95412acac39..32c35e454be 100644 --- a/javascript/ql/test/query-tests/DOM/TargetBlank/tst.js +++ b/javascript/ql/test/query-tests/DOM/TargetBlank/tst.js @@ -62,4 +62,7 @@ function f() { // OK, Flask application with internal links Example; Example; -Example; \ No newline at end of file +Example; + +// OK, nunjucks template +Example \ No newline at end of file diff --git a/misc/suite-helpers/security-and-quality-selectors.yml b/misc/suite-helpers/security-and-quality-selectors.yml index 881bcdfa3ac..61466f53886 100644 --- a/misc/suite-helpers/security-and-quality-selectors.yml +++ b/misc/suite-helpers/security-and-quality-selectors.yml @@ -14,6 +14,14 @@ problem.severity: - error - warning +- include: + kind: + - diagnostic +- include: + kind: + - metric + tags contain: + - summary - exclude: deprecated: // - exclude: diff --git a/misc/suite-helpers/security-extended-selectors.yml b/misc/suite-helpers/security-extended-selectors.yml index a19639c1eb3..c3a82de14f2 100644 --- a/misc/suite-helpers/security-extended-selectors.yml +++ b/misc/suite-helpers/security-extended-selectors.yml @@ -19,6 +19,14 @@ - warning tags contain: - security +- include: + kind: + - diagnostic +- include: + kind: + - metric + tags contain: + - summary - exclude: deprecated: // - exclude: diff --git a/python/change-notes/2021-07-12-add-typetrackingnode.md b/python/change-notes/2021-07-12-add-typetrackingnode.md index 70e723e976e..ffb005cc260 100644 --- a/python/change-notes/2021-07-12-add-typetrackingnode.md +++ b/python/change-notes/2021-07-12-add-typetrackingnode.md @@ -1,3 +1,2 @@ lgtm,codescanning -* The `track` and `backtrack` methods on `LocalSourceNode` have been deprecated. When writing - type trackers, the corresponding methods on `TypeTrackingNode` should be used instead. +* The `track` and `backtrack` methods on `LocalSourceNode` are in the process of being deprecated. When using type trackers, the corresponding methods on `TypeTrackingNode` should be used instead. diff --git a/python/change-notes/2021-07-28-port-RoDoS-queries.md b/python/change-notes/2021-07-28-port-RoDoS-queries.md index 9d628ad4a28..35974d6e0fc 100644 --- a/python/change-notes/2021-07-28-port-RoDoS-queries.md +++ b/python/change-notes/2021-07-28-port-RoDoS-queries.md @@ -1,3 +1,3 @@ lgtm,codescanning -* Added _Inefficient regular expression_ (`py/redos`) query, which is already available in JavaScript. -* Added _Polynomial regular expression used on uncontrolled data_ (`py/polynomial-redos`), which is already available in JavaScript. +* Added an experimental _Inefficient regular expression_ (`py/redos`) query, which is already available in JavaScript. +* Added an experimental _Polynomial regular expression used on uncontrolled data_ (`py/polynomial-redos`), which is already available in JavaScript. diff --git a/python/ql/src/Security/CWE-295/RequestWithoutValidation.qhelp b/python/ql/src/Security/CWE-295/RequestWithoutValidation.qhelp index b581c4f4a01..287829f8c7d 100644 --- a/python/ql/src/Security/CWE-295/RequestWithoutValidation.qhelp +++ b/python/ql/src/Security/CWE-295/RequestWithoutValidation.qhelp @@ -7,7 +7,7 @@

    Encryption is key to the security of most, if not all, online communication. Using Transport Layer Security (TLS) can ensure that communication cannot be interrupted by an interloper. -For this reason, is is unwise to disable the verification that TLS provides. +For this reason, it is unwise to disable the verification that TLS provides. Functions in the requests module provide verification by default, and it is only when explicitly turned off using verify=False that no verification occurs.

    diff --git a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp new file mode 100644 index 00000000000..6f7c2b77570 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp @@ -0,0 +1,31 @@ + + + +

    If an LDAP query doesn't carry any kind of authentication, anonymous binds causes an empty or None-set password +to result in a successful authentication.

    +
    + + +

    Use a non-empty password while establishing an LDAP connection.

    +
    + + +

    In the following examples, the code builds a LDAP query whose execution carries no authentication or binds anonymously.

    + + + + +

    In the third and fourth examples, the authentication is established using a password.

    + + + +
    + + +
  • SonarSource: RSPEC-4433.
  • +
  • Python2: LDAP Documentation.
  • +
  • Python3: LDAP Documentation.
  • +
    +
    diff --git a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql new file mode 100644 index 00000000000..5c27d641785 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql @@ -0,0 +1,31 @@ +/** + * @name Improper LDAP Authentication + * @description A user-controlled query carries no authentication + * @kind problem + * @problem.severity warning + * @id py/improper-ldap-auth + * @tags experimental + * security + * external/cwe/cwe-287 + */ + +// Determine precision above +import python +import experimental.semmle.python.Concepts +import semmle.python.dataflow.new.DataFlow + +predicate authenticatesImproperly(LDAPBind ldapBind) { + ( + DataFlow::localFlow(DataFlow::exprNode(any(None noneName)), ldapBind.getPassword()) or + not exists(ldapBind.getPassword()) + ) + or + exists(StrConst emptyString | + emptyString.getText() = "" and + DataFlow::localFlow(DataFlow::exprNode(emptyString), ldapBind.getPassword()) + ) +} + +from LDAPBind ldapBind +where authenticatesImproperly(ldapBind) +select ldapBind, "The following LDAP bind operation is executed without authentication" diff --git a/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_2.py b/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_2.py new file mode 100644 index 00000000000..550305de5e7 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_2.py @@ -0,0 +1,14 @@ +from flask import request, Flask +import ldap +import ldap.filter +import ldap.dn + + +@app.route("/bind_example") +def bind_example(): + dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc'])) + search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search'])) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind('cn=root', "") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) diff --git a/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_3.py b/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_3.py new file mode 100644 index 00000000000..b28aad4155f --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_3.py @@ -0,0 +1,13 @@ +from ldap3 import Server, Connection, ALL +from flask import request, Flask +from ldap3.utils.dn import escape_rdn +from ldap3.utils.conv import escape_filter_chars + +@app.route("/passwordNone") +def passwordNone(): + dn = "dc={}".format(escape_rdn(request.args['dc'])) + search_filter = "(user={})".format(escape_filter_chars(request.args['search'])) + + srv = Server('servername', get_info=ALL) + conn = Connection(srv, user='user_dn', password=None) + status, result, response, _ = conn.search(dn, search_filter) diff --git a/python/ql/src/experimental/Security/CWE-287/examples/auth_good_2.py b/python/ql/src/experimental/Security/CWE-287/examples/auth_good_2.py new file mode 100644 index 00000000000..c0860632171 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-287/examples/auth_good_2.py @@ -0,0 +1,14 @@ +from flask import request, Flask +import ldap +import ldap.filter +import ldap.dn + + +@app.route("/bind_example") +def bind_example(): + dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc'])) + search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search'])) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind('cn=root', "SecurePa$$!") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) diff --git a/python/ql/src/experimental/Security/CWE-287/examples/auth_good_3.py b/python/ql/src/experimental/Security/CWE-287/examples/auth_good_3.py new file mode 100644 index 00000000000..0d93943831b --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-287/examples/auth_good_3.py @@ -0,0 +1,14 @@ +from ldap3 import Server, Connection, ALL +from flask import request, Flask +from ldap3.utils.dn import escape_rdn +from ldap3.utils.conv import escape_filter_chars + +@app.route("/passwordFromEnv") +def passwordFromEnv(): + dn = "dc={}".format(escape_rdn(request.args['dc'])) + search_filter = "(user={})".format(escape_filter_chars(request.args['search'])) + + srv = Server('servername', get_info=ALL) + conn = Connection(srv, user='user_dn', + password="SecurePa$$!") + status, result, response, _ = conn.search(dn, search_filter) diff --git a/python/ql/src/Security/CWE-730/PolynomialBackTracking.ql b/python/ql/src/experimental/Security/CWE-730/PolynomialBackTracking.ql similarity index 100% rename from python/ql/src/Security/CWE-730/PolynomialBackTracking.ql rename to python/ql/src/experimental/Security/CWE-730/PolynomialBackTracking.ql diff --git a/python/ql/src/Security/CWE-730/PolynomialReDoS.qhelp b/python/ql/src/experimental/Security/CWE-730/PolynomialReDoS.qhelp similarity index 100% rename from python/ql/src/Security/CWE-730/PolynomialReDoS.qhelp rename to python/ql/src/experimental/Security/CWE-730/PolynomialReDoS.qhelp diff --git a/python/ql/src/Security/CWE-730/PolynomialReDoS.ql b/python/ql/src/experimental/Security/CWE-730/PolynomialReDoS.ql similarity index 100% rename from python/ql/src/Security/CWE-730/PolynomialReDoS.ql rename to python/ql/src/experimental/Security/CWE-730/PolynomialReDoS.ql diff --git a/python/ql/src/Security/CWE-730/ReDoS.qhelp b/python/ql/src/experimental/Security/CWE-730/ReDoS.qhelp similarity index 100% rename from python/ql/src/Security/CWE-730/ReDoS.qhelp rename to python/ql/src/experimental/Security/CWE-730/ReDoS.qhelp diff --git a/python/ql/src/Security/CWE-730/ReDoS.ql b/python/ql/src/experimental/Security/CWE-730/ReDoS.ql similarity index 100% rename from python/ql/src/Security/CWE-730/ReDoS.ql rename to python/ql/src/experimental/Security/CWE-730/ReDoS.ql diff --git a/python/ql/src/Security/CWE-730/ReDoSIntroduction.inc.qhelp b/python/ql/src/experimental/Security/CWE-730/ReDoSIntroduction.inc.qhelp similarity index 100% rename from python/ql/src/Security/CWE-730/ReDoSIntroduction.inc.qhelp rename to python/ql/src/experimental/Security/CWE-730/ReDoSIntroduction.inc.qhelp diff --git a/python/ql/src/Security/CWE-730/ReDoSReferences.inc.qhelp b/python/ql/src/experimental/Security/CWE-730/ReDoSReferences.inc.qhelp similarity index 100% rename from python/ql/src/Security/CWE-730/ReDoSReferences.inc.qhelp rename to python/ql/src/experimental/Security/CWE-730/ReDoSReferences.inc.qhelp diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 7641ac1becf..21c202851f1 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -146,3 +146,66 @@ class LDAPEscape extends DataFlow::Node { */ DataFlow::Node getAnInput() { result = range.getAnInput() } } + +/** Provides classes for modeling LDAP bind-related APIs. */ +module LDAPBind { + /** + * A data-flow node that collects methods binding a LDAP connection. + * + * Extend this class to model new APIs. If you want to refine existing API models, + * extend `LDAPBind` instead. + */ + abstract class Range extends DataFlow::Node { + /** + * Gets the argument containing the binding expression. + */ + abstract DataFlow::Node getPassword(); + } +} + +/** + * A data-flow node that collects methods binding a LDAP connection. + * + * Extend this class to refine existing API models. If you want to model new APIs, + * extend `LDAPBind::Range` instead. + */ +class LDAPBind extends DataFlow::Node { + LDAPBind::Range range; + + LDAPBind() { this = range } + + DataFlow::Node getPassword() { result = range.getPassword() } +} + +/** Provides classes for modeling SQL sanitization libraries. */ +module SQLEscape { + /** + * A data-flow node that collects functions that escape SQL statements. + * + * Extend this class to model new APIs. If you want to refine existing API models, + * extend `SQLEscape` instead. + */ + abstract class Range extends DataFlow::Node { + /** + * Gets the argument containing the raw SQL statement. + */ + abstract DataFlow::Node getAnInput(); + } +} + +/** + * A data-flow node that collects functions escaping SQL statements. + * + * Extend this class to refine existing API models. If you want to model new APIs, + * extend `SQLEscape::Range` instead. + */ +class SQLEscape extends DataFlow::Node { + SQLEscape::Range range; + + SQLEscape() { this = range } + + /** + * Gets the argument containing the raw SQL statement. + */ + DataFlow::Node getAnInput() { result = range.getAnInput() } +} diff --git a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll index 2843a695b18..83b1accafc1 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll @@ -19,6 +19,20 @@ private module LDAP { * See https://www.python-ldap.org/en/python-ldap-3.3.0/index.html */ private module LDAP2 { + /** Gets a reference to the `ldap` module. */ + API::Node ldap() { result = API::moduleImport("ldap") } + + /** Returns a `ldap` module instance */ + API::Node ldapInitialize() { result = ldap().getMember("initialize") } + + /** Gets a reference to a `ldap` operation. */ + private DataFlow::TypeTrackingNode ldapOperation(DataFlow::TypeTracker t) { + t.start() and + result.(DataFlow::AttrRead).getObject().getALocalSource() = ldapInitialize().getACall() + or + exists(DataFlow::TypeTracker t2 | result = ldapOperation(t2).track(t2, t)) + } + /** * List of `ldap` methods used to execute a query. * @@ -30,32 +44,61 @@ private module LDAP { } } + /** Gets a reference to a `ldap` operation. */ + private DataFlow::Node ldapOperation() { + ldapOperation(DataFlow::TypeTracker::end()).flowsTo(result) + } + + /** Gets a reference to a `ldap` query. */ + private DataFlow::Node ldapQuery() { + result = ldapOperation() and + result.(DataFlow::AttrRead).getAttributeName() instanceof LDAP2QueryMethods + } + /** * A class to find `ldap` methods executing a query. * * See `LDAP2QueryMethods` */ private class LDAP2Query extends DataFlow::CallCfgNode, LDAPQuery::Range { - DataFlow::Node ldapQuery; + LDAP2Query() { this.getFunction() = ldapQuery() } - LDAP2Query() { - exists(DataFlow::AttrRead searchMethod | - this.getFunction() = searchMethod and - API::moduleImport("ldap").getMember("initialize").getACall() = - searchMethod.getObject().getALocalSource() and - searchMethod.getAttributeName() instanceof LDAP2QueryMethods and - ( - ldapQuery = this.getArg(0) - or - ( - ldapQuery = this.getArg(2) or - ldapQuery = this.getArgByName("filterstr") - ) - ) - ) + override DataFlow::Node getQuery() { + result in [this.getArg(0), this.getArg(2), this.getArgByName("filterstr")] } + } - override DataFlow::Node getQuery() { result = ldapQuery } + /** + * List of `ldap` methods used for binding. + * + * See https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html#functions + */ + private class LDAP2BindMethods extends string { + LDAP2BindMethods() { + this in [ + "bind", "bind_s", "simple_bind", "simple_bind_s", "sasl_interactive_bind_s", + "sasl_non_interactive_bind_s", "sasl_external_bind_s", "sasl_gssapi_bind_s" + ] + } + } + + /** Gets a reference to a `ldap` bind. */ + private DataFlow::Node ldapBind() { + result = ldapOperation() and + result.(DataFlow::AttrRead).getAttributeName() instanceof LDAP2BindMethods + } + + /** + * A class to find `ldap` methods binding a connection. + * + * See `LDAP2BindMethods` + */ + private class LDAP2Bind extends DataFlow::CallCfgNode, LDAPBind::Range { + LDAP2Bind() { this.getFunction() = ldapBind() } + + override DataFlow::Node getPassword() { + result in [this.getArg(1), this.getArgByName("cred")] + } } /** @@ -64,9 +107,7 @@ private module LDAP { * See https://github.com/python-ldap/python-ldap/blob/7ce471e238cdd9a4dd8d17baccd1c9e05e6f894a/Lib/ldap/dn.py#L17 */ private class LDAP2EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range { - LDAP2EscapeDNCall() { - this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall() - } + LDAP2EscapeDNCall() { this = ldap().getMember("dn").getMember("escape_dn_chars").getACall() } override DataFlow::Node getAnInput() { result = this.getArg(0) } } @@ -78,8 +119,7 @@ private module LDAP { */ private class LDAP2EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range { LDAP2EscapeFilterCall() { - this = - API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall() + this = ldap().getMember("filter").getMember("escape_filter_chars").getACall() } override DataFlow::Node getAnInput() { result = this.getArg(0) } @@ -92,26 +132,40 @@ private module LDAP { * See https://pypi.org/project/ldap3/ */ private module LDAP3 { + /** Gets a reference to the `ldap3` module. */ + API::Node ldap3() { result = API::moduleImport("ldap3") } + + /** Gets a reference to the `ldap3` `utils` module. */ + API::Node ldap3Utils() { result = ldap3().getMember("utils") } + + /** Returns a `ldap3` module `Server` instance */ + API::Node ldap3Server() { result = ldap3().getMember("Server") } + + /** Returns a `ldap3` module `Connection` instance */ + API::Node ldap3Connection() { result = ldap3().getMember("Connection") } + /** * A class to find `ldap3` methods executing a query. */ private class LDAP3Query extends DataFlow::CallCfgNode, LDAPQuery::Range { - DataFlow::Node ldapQuery; - LDAP3Query() { - exists(DataFlow::AttrRead searchMethod | - this.getFunction() = searchMethod and - API::moduleImport("ldap3").getMember("Connection").getACall() = - searchMethod.getObject().getALocalSource() and - searchMethod.getAttributeName() = "search" and - ( - ldapQuery = this.getArg(0) or - ldapQuery = this.getArg(1) - ) - ) + this.getFunction().(DataFlow::AttrRead).getObject().getALocalSource() = + ldap3Connection().getACall() and + this.getFunction().(DataFlow::AttrRead).getAttributeName() = "search" } - override DataFlow::Node getQuery() { result = ldapQuery } + override DataFlow::Node getQuery() { result in [this.getArg(0), this.getArg(1)] } + } + + /** + * A class to find `ldap3` methods binding a connection. + */ + class LDAP3Bind extends DataFlow::CallCfgNode, LDAPBind::Range { + LDAP3Bind() { this = ldap3Connection().getACall() } + + override DataFlow::Node getPassword() { + result in [this.getArg(2), this.getArgByName("password")] + } } /** @@ -120,14 +174,7 @@ private module LDAP { * See https://github.com/cannatag/ldap3/blob/4d33166f0869b929f59c6e6825a1b9505eb99967/ldap3/utils/dn.py#L390 */ private class LDAP3EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range { - LDAP3EscapeDNCall() { - this = - API::moduleImport("ldap3") - .getMember("utils") - .getMember("dn") - .getMember("escape_rdn") - .getACall() - } + LDAP3EscapeDNCall() { this = ldap3Utils().getMember("dn").getMember("escape_rdn").getACall() } override DataFlow::Node getAnInput() { result = this.getArg(0) } } @@ -139,12 +186,7 @@ private module LDAP { */ private class LDAP3EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range { LDAP3EscapeFilterCall() { - this = - API::moduleImport("ldap3") - .getMember("utils") - .getMember("conv") - .getMember("escape_filter_chars") - .getACall() + this = ldap3Utils().getMember("conv").getMember("escape_filter_chars").getACall() } override DataFlow::Node getAnInput() { result = this.getArg(0) } diff --git a/python/ql/src/experimental/semmle/python/frameworks/SqlAlchemy.qll b/python/ql/src/experimental/semmle/python/frameworks/SqlAlchemy.qll new file mode 100644 index 00000000000..00d550d9844 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/frameworks/SqlAlchemy.qll @@ -0,0 +1,148 @@ +/** + * Provides classes modeling security-relevant aspects of the 'SqlAlchemy' package. + * See https://pypi.org/project/SQLAlchemy/. + */ + +private import python +private import semmle.python.dataflow.new.DataFlow +private import semmle.python.dataflow.new.TaintTracking +private import semmle.python.ApiGraphs +private import semmle.python.Concepts +private import experimental.semmle.python.Concepts + +private module SqlAlchemy { + /** + * Returns an instantization of a SqlAlchemy Session object. + * See https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.Session and + * https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.sessionmaker + */ + private API::Node getSqlAlchemySessionInstance() { + result = API::moduleImport("sqlalchemy.orm").getMember("Session").getReturn() or + result = API::moduleImport("sqlalchemy.orm").getMember("sessionmaker").getReturn().getReturn() + } + + /** + * Returns an instantization of a SqlAlchemy Engine object. + * See https://docs.sqlalchemy.org/en/14/core/engines.html#sqlalchemy.create_engine + */ + private API::Node getSqlAlchemyEngineInstance() { + result = API::moduleImport("sqlalchemy").getMember("create_engine").getReturn() + } + + /** + * Returns an instantization of a SqlAlchemy Query object. + * See https://docs.sqlalchemy.org/en/14/orm/query.html?highlight=query#sqlalchemy.orm.Query + */ + private API::Node getSqlAlchemyQueryInstance() { + result = getSqlAlchemySessionInstance().getMember("query").getReturn() + } + + /** + * A call to `execute` meant to execute an SQL expression + * See the following links: + * - https://docs.sqlalchemy.org/en/14/core/connections.html?highlight=execute#sqlalchemy.engine.Connection.execute + * - https://docs.sqlalchemy.org/en/14/core/connections.html?highlight=execute#sqlalchemy.engine.Engine.execute + * - https://docs.sqlalchemy.org/en/14/orm/session_api.html?highlight=execute#sqlalchemy.orm.Session.execute + */ + private class SqlAlchemyExecuteCall extends DataFlow::CallCfgNode, SqlExecution::Range { + SqlAlchemyExecuteCall() { + // new way + this = getSqlAlchemySessionInstance().getMember("execute").getACall() or + this = + getSqlAlchemyEngineInstance() + .getMember(["connect", "begin"]) + .getReturn() + .getMember("execute") + .getACall() + } + + override DataFlow::Node getSql() { result = this.getArg(0) } + } + + /** + * A call to `scalar` meant to execute an SQL expression + * See https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.Session.scalar and + * https://docs.sqlalchemy.org/en/14/core/connections.html?highlight=scalar#sqlalchemy.engine.Engine.scalar + */ + private class SqlAlchemyScalarCall extends DataFlow::CallCfgNode, SqlExecution::Range { + SqlAlchemyScalarCall() { + this = + [getSqlAlchemySessionInstance(), getSqlAlchemyEngineInstance()] + .getMember("scalar") + .getACall() + } + + override DataFlow::Node getSql() { result = this.getArg(0) } + } + + /** + * A call on a Query object + * See https://docs.sqlalchemy.org/en/14/orm/query.html?highlight=query#sqlalchemy.orm.Query + */ + private class SqlAlchemyQueryCall extends DataFlow::CallCfgNode, SqlExecution::Range { + SqlAlchemyQueryCall() { + this = + getSqlAlchemyQueryInstance() + .getMember(any(SqlAlchemyVulnerableMethodNames methodName)) + .getACall() + } + + override DataFlow::Node getSql() { result = this.getArg(0) } + } + + /** + * This class represents a list of methods vulnerable to sql injection. + * + * See https://github.com/jty-team/codeql/pull/2#issue-611592361 + */ + private class SqlAlchemyVulnerableMethodNames extends string { + SqlAlchemyVulnerableMethodNames() { this in ["filter", "filter_by", "group_by", "order_by"] } + } + + /** + * Additional taint-steps for `sqlalchemy.text()` + * + * See https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.text + * See https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.TextClause + */ + class SqlAlchemyTextAdditionalTaintSteps extends TaintTracking::AdditionalTaintStep { + override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { + exists(DataFlow::CallCfgNode call | + ( + call = API::moduleImport("sqlalchemy").getMember("text").getACall() + or + call = API::moduleImport("sqlalchemy").getMember("sql").getMember("text").getACall() + or + call = + API::moduleImport("sqlalchemy") + .getMember("sql") + .getMember("expression") + .getMember("text") + .getACall() + or + call = + API::moduleImport("sqlalchemy") + .getMember("sql") + .getMember("expression") + .getMember("TextClause") + .getACall() + ) and + nodeFrom in [call.getArg(0), call.getArgByName("text")] and + nodeTo = call + ) + } + } + + /** + * Gets a reference to `sqlescapy.sqlescape`. + * + * See https://pypi.org/project/sqlescapy/ + */ + class SQLEscapySanitizerCall extends DataFlow::CallCfgNode, SQLEscape::Range { + SQLEscapySanitizerCall() { + this = API::moduleImport("sqlescapy").getMember("sqlescape").getACall() + } + + override DataFlow::Node getAnInput() { result = this.getArg(0) } + } +} diff --git a/python/ql/src/semmle/python/dataflow/new/internal/LocalSources.qll b/python/ql/src/semmle/python/dataflow/new/internal/LocalSources.qll index 3acd93de6cc..df1ee7bba16 100644 --- a/python/ql/src/semmle/python/dataflow/new/internal/LocalSources.qll +++ b/python/ql/src/semmle/python/dataflow/new/internal/LocalSources.qll @@ -104,26 +104,20 @@ class LocalSourceNode extends Node { } /** - * DEPRECATED. Use `TypeTrackingNode::track` instead. - * * Gets a node that this node may flow to using one heap and/or interprocedural step. * * See `TypeTracker` for more details about how to use this. */ pragma[inline] - deprecated LocalSourceNode track(TypeTracker t2, TypeTracker t) { t = t2.step(this, result) } + LocalSourceNode track(TypeTracker t2, TypeTracker t) { t = t2.step(this, result) } /** - * DEPRECATED. Use `TypeTrackingNode::backtrack` instead. - * * Gets a node that may flow into this one using one heap and/or interprocedural step. * * See `TypeBackTracker` for more details about how to use this. */ pragma[inline] - deprecated LocalSourceNode backtrack(TypeBackTracker t2, TypeBackTracker t) { - t2 = t.step(result, this) - } + LocalSourceNode backtrack(TypeBackTracker t2, TypeBackTracker t) { t2 = t.step(result, this) } } /** @@ -131,40 +125,46 @@ class LocalSourceNode extends Node { * * All steps made during type tracking should be between instances of this class. */ -class TypeTrackingNode extends Node { - TypeTrackingNode() { - this instanceof LocalSourceNode - or - this instanceof ModuleVariableNode +class TypeTrackingNode = LocalSourceNode; + +/** Temporary holding ground for the `TypeTrackingNode` class. */ +private module FutureWork { + class FutureTypeTrackingNode extends Node { + FutureTypeTrackingNode() { + this instanceof LocalSourceNode + or + this instanceof ModuleVariableNode + } + + /** + * Holds if this node can flow to `nodeTo` in one or more local flow steps. + * + * For `ModuleVariableNode`s, the only "local" step is to the node itself. + * For `LocalSourceNode`s, this is the usual notion of local flow. + */ + pragma[inline] + predicate flowsTo(Node node) { + this instanceof ModuleVariableNode and this = node + or + this.(LocalSourceNode).flowsTo(node) + } + + /** + * Gets a node that this node may flow to using one heap and/or interprocedural step. + * + * See `TypeTracker` for more details about how to use this. + */ + pragma[inline] + TypeTrackingNode track(TypeTracker t2, TypeTracker t) { t = t2.step(this, result) } + + /** + * Gets a node that may flow into this one using one heap and/or interprocedural step. + * + * See `TypeBackTracker` for more details about how to use this. + */ + pragma[inline] + TypeTrackingNode backtrack(TypeBackTracker t2, TypeBackTracker t) { t2 = t.step(result, this) } } - - /** - * Holds if this node can flow to `nodeTo` in one or more local flow steps. - * - * For `ModuleVariableNode`s, the only "local" step is to the node itself. - * For `LocalSourceNode`s, this is the usual notion of local flow. - */ - predicate flowsTo(Node node) { - this instanceof ModuleVariableNode and this = node - or - this.(LocalSourceNode).flowsTo(node) - } - - /** - * Gets a node that this node may flow to using one heap and/or interprocedural step. - * - * See `TypeTracker` for more details about how to use this. - */ - pragma[inline] - TypeTrackingNode track(TypeTracker t2, TypeTracker t) { t = t2.step(this, result) } - - /** - * Gets a node that may flow into this one using one heap and/or interprocedural step. - * - * See `TypeBackTracker` for more details about how to use this. - */ - pragma[inline] - TypeTrackingNode backtrack(TypeBackTracker t2, TypeBackTracker t) { t2 = t.step(result, this) } } cached @@ -179,11 +179,21 @@ private module Cached { source = sink or exists(Node second | - simpleLocalFlowStep(source, second) and - simpleLocalFlowStep*(second, sink) + localSourceFlowStep(source, second) and + localSourceFlowStep*(second, sink) ) } + /** + * Helper predicate for `hasLocalSource`. Removes any steps go to module variable reads, as these + * are already local source nodes in their own right. + */ + cached + private predicate localSourceFlowStep(Node nodeFrom, Node nodeTo) { + simpleLocalFlowStep(nodeFrom, nodeTo) and + not nodeTo = any(ModuleVariableNode v).getARead() + } + /** * Holds if `base` flows to the base of `ref` and `ref` has attribute name `attr`. */ diff --git a/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/ConceptsTest.expected b/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/ConceptsTest.expected new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/ConceptsTest.ql b/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/ConceptsTest.ql new file mode 100644 index 00000000000..b097eb15648 --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/ConceptsTest.ql @@ -0,0 +1,3 @@ +import python +import experimental.meta.ConceptsTest +import experimental.semmle.python.frameworks.SqlAlchemy diff --git a/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/InlineTaintTest.expected b/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/InlineTaintTest.expected new file mode 100644 index 00000000000..79d760d87f4 --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/InlineTaintTest.expected @@ -0,0 +1,3 @@ +argumentToEnsureNotTaintedNotMarkedAsSpurious +untaintedArgumentToEnsureTaintedNotMarkedAsMissing +failures diff --git a/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/InlineTaintTest.ql b/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/InlineTaintTest.ql new file mode 100644 index 00000000000..2ddb342c62f --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/InlineTaintTest.ql @@ -0,0 +1,2 @@ +import experimental.meta.InlineTaintTest +import experimental.semmle.python.frameworks.SqlAlchemy diff --git a/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/SqlExecution.py b/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/SqlExecution.py new file mode 100644 index 00000000000..7731e80e534 --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/SqlExecution.py @@ -0,0 +1,57 @@ +import sqlalchemy +from sqlalchemy import Column, Integer, String, ForeignKey, create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.pool import StaticPool +from sqlalchemy.orm import relationship, backref, sessionmaker, joinedload +from sqlalchemy.sql import text + +engine = create_engine( + 'sqlite:///:memory:', + echo=True, + connect_args={"check_same_thread": False}, + poolclass=StaticPool +) + +Base = declarative_base() + +class User(Base): + __tablename__ = 'users' + + id = Column(Integer, primary_key=True) + name = Column(String) + +Base.metadata.create_all(engine) + +Session = sessionmaker(bind=engine) +session = Session() + +ed_user = User(name='ed') +ed_user2 = User(name='george') + +session.add(ed_user) +session.add(ed_user2) + +session.commit() + +# Injection without requiring the text() taint-step +session.query(User).filter_by(name="some sql") # $ MISSING: getSql="some sql" +session.scalar("some sql") # $ getSql="some sql" +engine.scalar("some sql") # $ getSql="some sql" +session.execute("some sql") # $ getSql="some sql" + +with engine.connect() as connection: + connection.execute("some sql") # $ getSql="some sql" + +with engine.begin() as connection: + connection.execute("some sql") # $ getSql="some sql" + +# Injection requiring the text() taint-step +t = text("some sql") +session.query(User).filter(t) # $ getSql=t +session.query(User).group_by(User.id).having(t) # $ getSql=User.id MISSING: getSql=t +session.query(User).group_by(t).first() # $ getSql=t +session.query(User).order_by(t).first() # $ getSql=t + +query = select(User).where(User.name == t) # $ MISSING: getSql=t +with engine.connect() as conn: + conn.execute(query) # $ getSql=query diff --git a/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/taint_test.py b/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/taint_test.py new file mode 100644 index 00000000000..91f8987132f --- /dev/null +++ b/python/ql/test/experimental/library-tests/frameworks/sqlalchemy/taint_test.py @@ -0,0 +1,12 @@ +import sqlalchemy + +def test_taint(): + ts = TAINTED_STRING + + ensure_tainted( + ts, # $ tainted + sqlalchemy.text(ts), # $ tainted + sqlalchemy.sql.text(ts),# $ tainted + sqlalchemy.sql.expression.text(ts),# $ tainted + sqlalchemy.sql.expression.TextClause(ts),# $ tainted + ) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected new file mode 100644 index 00000000000..766665ee1b6 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected @@ -0,0 +1,10 @@ +| auth_bad_2.py:19:5:19:42 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | +| auth_bad_2.py:33:5:33:44 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | +| auth_bad_2.py:47:5:47:43 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | +| auth_bad_2.py:60:5:60:52 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | +| auth_bad_2.py:73:5:73:39 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | +| auth_bad_2.py:87:5:87:48 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | +| auth_bad_3.py:19:12:19:43 | ControlFlowNode for Connection() | The following LDAP bind operation is executed without authentication | +| auth_bad_3.py:33:12:33:57 | ControlFlowNode for Connection() | The following LDAP bind operation is executed without authentication | +| auth_bad_3.py:46:12:46:55 | ControlFlowNode for Connection() | The following LDAP bind operation is executed without authentication | +| auth_bad_3.py:60:12:60:42 | ControlFlowNode for Connection() | The following LDAP bind operation is executed without authentication | diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.qlref b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.qlref new file mode 100644 index 00000000000..9f5c6e4c43f --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.qlref @@ -0,0 +1 @@ +experimental/Security/CWE-287/ImproperLdapAuth.ql diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py new file mode 100644 index 00000000000..d37cc09cfd0 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py @@ -0,0 +1,92 @@ +from flask import request, Flask +import ldap +import ldap.filter +import ldap.dn + +app = Flask(__name__) + + +@app.route("/simple_bind_example") +def simple_bind_example(): + """ + The bind's password argument is not set + """ + + dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc'])) + search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search'])) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.simple_bind('cn=root') + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + + +@app.route("/simple_bind_s_example") +def simple_bind_s_example(): + """ + The bind's password argument is not set + """ + + dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc'])) + search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search'])) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.simple_bind_s('cn=root') + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + + +@app.route("/bind_s_example") +def bind_s_example(): + """ + The bind's password argument is set to None + """ + + dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc'])) + search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search'])) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind_s('cn=root', None) + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + +@app.route("/bind_s_example") +def bind_s_example_kwargs(): + """ + The bind's password argument is set to None + """ + + dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc'])) + search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search'])) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind_s(who='cn=root', cred=None) + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + +@app.route("/bind_example") +def bind_example(): + """ + The bind's password argument is an empty string + """ + + dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc'])) + search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search'])) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind('cn=root', "") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + + +@app.route("/bind_example") +def bind_example(): + """ + The bind's password argument is an empty string + """ + + dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc'])) + search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search'])) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind(who='cn=root', cred="") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py new file mode 100644 index 00000000000..2500b4cadb6 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py @@ -0,0 +1,65 @@ +from ldap3 import Server, Connection, ALL +from flask import request, Flask +from ldap3.utils.dn import escape_rdn +from ldap3.utils.conv import escape_filter_chars + +app = Flask(__name__) + + +@app.route("/passwordNone") +def passwordNone(): + """ + The bind's password argument is set to None + """ + + dn = "dc={}".format(escape_rdn(request.args['dc'])) + search_filter = "(user={})".format(escape_filter_chars(request.args['search'])) + + srv = Server('servername', get_info=ALL) + conn = Connection(srv, 'user_dn', None) + status, result, response, _ = conn.search(dn, search_filter) + + +@app.route("/passwordNone") +def passwordNoneKwargs(): + """ + The bind's password argument is set to None + """ + + dn = "dc={}".format(escape_rdn(request.args['dc'])) + search_filter = "(user={})".format(escape_filter_chars(request.args['search'])) + + srv = Server('servername', get_info=ALL) + conn = Connection(srv, user='user_dn', password=None) + status, result, response, _ = conn.search(dn, search_filter) + +@app.route("/passwordEmpty") +def passwordEmpty(): + """ + The bind's password argument is an empty string + """ + + dn = "dc={}".format(escape_rdn(request.args['dc'])) + search_filter = "(user={})".format(escape_filter_chars(request.args['search'])) + + srv = Server('servername', get_info=ALL) + conn = Connection(srv, user='user_dn', password="") + status, result, response, _ = conn.search(dn, search_filter) + + +@app.route("/notPassword") +def notPassword(): + """ + The bind's password argument is not set + """ + + dn = "dc={}".format(escape_rdn(request.args['dc'])) + search_filter = "(user={})".format(escape_filter_chars(request.args['search'])) + + srv = Server('servername', get_info=ALL) + conn = Connection(srv, user='user_dn') + status, result, response, _ = conn.search(dn, search_filter) + + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_2.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_2.py new file mode 100644 index 00000000000..1c959efc613 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_2.py @@ -0,0 +1,65 @@ +from flask import request, Flask +import ldap +import ldap.filter +import ldap.dn + +app = Flask(__name__) + + +@app.route("/simple_bind_example") +def simple_bind_example(): + """ + The bind's password argument is a non-empty string + """ + + dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc'])) + search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search'])) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.simple_bind('cn=root', "SecurePa$$!") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + + +@app.route("/simple_bind_s_example") +def simple_bind_s_example(): + """ + The bind's password argument is a non-empty string + """ + + dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc'])) + search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search'])) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.simple_bind_s('cn=root', "SecurePa$$!") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + + +@app.route("/bind_s_example") +def bind_s_example(): + """ + The bind's password argument is a non-empty string + """ + + dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc'])) + search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search'])) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind_s('cn=root', "SecurePa$$!") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + + +@app.route("/bind_example") +def bind_example(): + """ + The bind's password argument is a non-empty string + """ + + dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc'])) + search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search'])) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind('cn=root', "SecurePa$$!") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_3.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_3.py new file mode 100644 index 00000000000..530900877db --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_3.py @@ -0,0 +1,24 @@ +from ldap3 import Server, Connection, ALL +from flask import request, Flask +from ldap3.utils.dn import escape_rdn +from ldap3.utils.conv import escape_filter_chars + +app = Flask(__name__) + + +@app.route("/passwordFromEnv") +def passwordFromEnv(): + """ + The bind's password argument is a non-empty string + """ + + dn = "dc={}".format(escape_rdn(request.args['dc'])) + search_filter = "(user={})".format(escape_filter_chars(request.args['search'])) + + srv = Server('servername', get_info=ALL) + conn = Connection(srv, user='user_dn', + password="SecurePa$$!") + status, result, response, _ = conn.search(dn, search_filter) + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialReDoS.qlref b/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialReDoS.qlref index 8c457eb7467..51d525c7d56 100644 --- a/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialReDoS.qlref +++ b/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialReDoS.qlref @@ -1 +1 @@ -Security/CWE-730/PolynomialReDoS.ql +experimental/Security/CWE-730/PolynomialReDoS.ql diff --git a/python/ql/test/query-tests/Security/CWE-730-ReDoS/ReDoS.qlref b/python/ql/test/query-tests/Security/CWE-730-ReDoS/ReDoS.qlref index 4c19d395edb..80975f7f6db 100644 --- a/python/ql/test/query-tests/Security/CWE-730-ReDoS/ReDoS.qlref +++ b/python/ql/test/query-tests/Security/CWE-730-ReDoS/ReDoS.qlref @@ -1 +1 @@ -Security/CWE-730/ReDoS.ql +experimental/Security/CWE-730/ReDoS.ql