Merge branch 'main' into ts-54

This commit is contained in:
erik-krogh
2024-02-25 21:20:43 +01:00
1315 changed files with 80439 additions and 48696 deletions

View File

@@ -1,3 +1,13 @@
## 0.8.9
### Minor Analysis Improvements
* The name "certification" is no longer seen as possibly being a certificate, and will therefore no longer be flagged in queries like "clear-text-logging" which look for sensitive data.
## 0.8.8
No user-facing changes.
## 0.8.7
### Minor Analysis Improvements

View File

@@ -3,6 +3,7 @@
*/
import semmle.files.FileSystem
private import codeql.util.FileSystem
/**
* Returns the `File` matching the given source file name as encoded by the VS
@@ -10,13 +11,5 @@ import semmle.files.FileSystem
*/
cached
File getFileBySourceArchiveName(string name) {
// The name provided for a file in the source archive by the VS Code extension
// has some differences from the absolute path in the database:
// 1. colons are replaced by underscores
// 2. there's a leading slash, even for Windows paths: "C:/foo/bar" ->
// "/C_/foo/bar"
// 3. double slashes in UNC prefixes are replaced with a single slash
// We can handle 2 and 3 together by unconditionally adding a leading slash
// before replacing double slashes.
name = ("/" + result.getAbsolutePath().replaceAll(":", "_")).replaceAll("//", "/")
result = IdeContextual<File>::getFileBySourceArchiveName(name)
}

View File

@@ -0,0 +1,3 @@
## 0.8.8
No user-facing changes.

View File

@@ -0,0 +1,5 @@
## 0.8.9
### Minor Analysis Improvements
* The name "certification" is no longer seen as possibly being a certificate, and will therefore no longer be flagged in queries like "clear-text-logging" which look for sensitive data.

View File

@@ -1,2 +1,2 @@
---
lastReleaseVersion: 0.8.7
lastReleaseVersion: 0.8.9

View File

@@ -1,5 +1,5 @@
name: codeql/javascript-all
version: 0.8.8-dev
version: 0.8.10-dev
groups: javascript
dbscheme: semmlecode.javascript.dbscheme
extractor: javascript

View File

@@ -594,6 +594,9 @@ module API {
exportedName = "" and
result = getAModuleImportRaw(moduleName)
}
/** Gets a sink node that represents instances of `cls`. */
Node getClassInstance(DataFlow::ClassNode cls) { result = Impl::MkClassInstance(cls) }
}
/**
@@ -1621,6 +1624,7 @@ private predicate exports(string m, DataFlow::Node rhs) {
exists(Module mod | mod = importableModule(m) |
rhs = mod.(AmdModule).getDefine().getModuleExpr().flow()
or
not mod.(ES2015Module).hasBothNamedAndDefaultExports() and
exports(m, "default", rhs)
or
exists(ExportAssignDeclaration assgn | assgn.getTopLevel() = mod |
@@ -1634,6 +1638,7 @@ private predicate exports(string m, DataFlow::Node rhs) {
/** Holds if module `m` exports `rhs` under the name `prop`. */
private predicate exports(string m, string prop, DataFlow::Node rhs) {
exists(ExportDeclaration exp | exp.getEnclosingModule() = importableModule(m) |
not exp.isTypeOnly() and
rhs = exp.getSourceNode(prop)
or
exists(Variable v |

View File

@@ -516,16 +516,37 @@ class MemberDeclaration extends @property, Documentable {
*/
predicate hasPublicKeyword() { has_public_keyword(this) }
/**
* Holds if this member is considered private.
*
* This may occur in two cases:
* - it is a TypeScript member annotated with the `private` keyword, or
* - the member has a private name, such as `#foo`, referring to a private field in the class
*/
predicate isPrivate() { this.hasPrivateKeyword() or this.hasPrivateFieldName() }
/**
* Holds if this is a TypeScript member annotated with the `private` keyword.
*/
predicate isPrivate() { has_private_keyword(this) }
predicate hasPrivateKeyword() { has_private_keyword(this) }
/**
* Holds if this is a TypeScript member annotated with the `protected` keyword.
*/
predicate isProtected() { has_protected_keyword(this) }
/**
* Holds if the member has a private name, such as `#foo`, referring to a private field in the class.
*
* For example:
* ```js
* class Foo {
* #method() {}
* }
* ```
*/
predicate hasPrivateFieldName() { this.getNameExpr().(Label).getName().charAt(0) = "#" }
/**
* Gets the expression specifying the name of this member,
* or nothing if this is a call signature.

View File

@@ -39,6 +39,20 @@ class ES2015Module extends Module {
// modules are implicitly strict
any()
}
/**
* Holds if this module contains both named and `default` exports.
*
* This is used to determine whether a default-import of the module should be reinterpreted
* as a namespace-import, to accommodate the non-standard behavior implemented by some compilers.
*
* When a module has both named and `default` exports, the non-standard interpretation can lead to
* ambiguities, so we only allow the standard interpretation in that case.
*/
predicate hasBothNamedAndDefaultExports() {
hasNamedExports(this) and
hasDefaultExport(this)
}
}
/**
@@ -64,17 +78,6 @@ private predicate hasDefaultExport(ES2015Module mod) {
mod.getAnExport().(ExportNamedDeclaration).getASpecifier().getExportedName() = "default"
}
/**
* Holds if `mod` contains both named and `default` exports.
*
* This is used to determine whether a default-import of the module should be reinterpreted
* as a namespace-import, to accommodate the non-standard behavior implemented by some compilers.
*/
private predicate hasBothNamedAndDefaultExports(ES2015Module mod) {
hasNamedExports(mod) and
hasDefaultExport(mod)
}
/**
* An import declaration.
*
@@ -131,7 +134,7 @@ class ImportDeclaration extends Stmt, Import, @import_declaration {
// For compatibility with the non-standard implementation of default imports,
// treat default imports as namespace imports in cases where it can't cause ambiguity
// between named exports and the properties of a default-exported object.
not hasBothNamedAndDefaultExports(this.getImportedModule()) and
not this.getImportedModule().(ES2015Module).hasBothNamedAndDefaultExports() and
is.getImportedName() = "default"
)
or

View File

@@ -29,7 +29,8 @@ class PackageJson extends JsonObject {
parentDir.getAChildContainer+() = currentDir and
pkgNameDiff = currentDir.getAbsolutePath().suffix(parentDir.getAbsolutePath().length()) and
not exists(pkgNameDiff.indexOf("/node_modules/")) and
result = parentPkgName + pkgNameDiff
result = parentPkgName + pkgNameDiff and
not parentPkg.isPrivate()
)
}

View File

@@ -1693,7 +1693,11 @@ module DataFlow {
exists(Expr predExpr, Expr succExpr |
pred = valueNode(predExpr) and succ = valueNode(succExpr)
|
predExpr = succExpr.(LogicalBinaryExpr).getAnOperand()
predExpr = succExpr.(LogicalOrExpr).getAnOperand()
or
predExpr = succExpr.(NullishCoalescingExpr).getAnOperand()
or
predExpr = succExpr.(LogicalAndExpr).getRightOperand()
or
predExpr = succExpr.(ConditionalExpr).getABranch()
or

View File

@@ -238,6 +238,26 @@ private class AnalyzedBinaryExpr extends DataFlow::AnalyzedValueNode {
}
}
pragma[nomagic]
private predicate falsyValue(AbstractValue value) { value.getBooleanValue() = false }
/**
* Flow analysis for `&&` operators.
*/
private class AnalyzedLogicalAndExpr extends DataFlow::AnalyzedValueNode {
override LogicalAndExpr astNode;
pragma[nomagic]
private AnalyzedValueNode leftOperand() { result = astNode.getLeftOperand().analyze() }
override AbstractValue getALocalValue() {
result = super.getALocalValue()
or
result = this.leftOperand().getALocalValue() and
falsyValue(result)
}
}
/**
* Gets the `n`th operand of the given `+` or `+=` expression.
*/

View File

@@ -0,0 +1,497 @@
/**
* Provides predicates for generating names for classes and functions that are part
* of the public API of a library.
*
* When possible, we try to use the qualified name by which a class/function can be accessed
* from client code.
*
* However, there are cases where classes and functions can be exposed to client
* code without being accessible as a qualified name. For example;
* ```js
* // 'Foo' is internal, but clients can call its methods, e.g. `getFoo().m()`
* class Foo {
* m() {}
* }
* export function getFoo() {
* return new Foo();
* }
*
* // Clients can call m() via getObj().m()
* export function getObj() {
* return {
* m() {}
* }
* }
* ```
*
* In these cases, we try to make up human-readable names for the endpoints.
* We make an effort to make these unambiguous in practice, though this is not always guaranteed.
*/
private import javascript
/** Concatenates two access paths. */
bindingset[x, y]
private string join(string x, string y) {
if x = "" or y = "" then result = x + y else result = x + "." + y
}
private predicate isPackageExport(API::Node node) { node = API::moduleExport(_) }
/**
* A version of `getInstance()` only from sink nodes to the special `ClassInstance` node.
*
* This ensures we see instance methods, but not side effects on `this` or on instantiations of the class.
*/
private predicate instanceEdge(API::Node pred, API::Node succ) {
exists(DataFlow::ClassNode cls |
pred.getAValueReachingSink() = cls and
succ = API::Internal::getClassInstance(cls)
)
}
/** Holds if `pred -> succ` is an edge we can use for naming. */
private predicate relevantEdge(API::Node pred, API::Node succ) {
succ = pred.getMember(_) and
not isPrivateLike(succ)
or
instanceEdge(pred, succ)
}
private signature predicate isRootNodeSig(API::Node node);
private signature predicate edgeSig(API::Node pred, API::Node succ);
/** Builds `shortestDistances` using the API graph root node as the only origin node, to ensure unique results. */
private module ApiGraphDistance<isRootNodeSig/1 isRootNode, edgeSig/2 edges> {
private predicate edgesWithEntry(API::Node pred, API::Node succ) {
edges(pred, succ)
or
pred = API::root() and
isRootNode(succ)
}
int distanceTo(API::Node node) = shortestDistances(API::root/0, edgesWithEntry/2)(_, node, result)
}
/** Gets the shortest distance from a package export to `nd` in the API graph. */
private predicate distanceFromPackageExport =
ApiGraphDistance<isPackageExport/1, relevantEdge/2>::distanceTo/1;
/**
* Holds if `(package, name)` is the fallback name for `cls`, to be used as a last resort
* in order to name its instance methods.
*
* This happens when the class is not accessible via an access path, but instances of the
* class can still escape via more complex access patterns, for example:
*
* class InternalClass {}
* function foo() {
* return new InternalClass();
* }
*/
private predicate classHasFallbackName(
DataFlow::ClassNode cls, string package, string name, int badness
) {
hasEscapingInstance(cls) and
not exists(distanceFromPackageExport(any(API::Node node | node.getAValueReachingSink() = cls))) and
exists(string baseName |
InternalModuleNaming::fallbackModuleName(cls.getTopLevel(), package, baseName, badness - 100) and
name = join(baseName, cls.getName())
)
}
/** Holds if `node` describes instances of a class that has a fallback name. */
private predicate isClassInstanceWithFallbackName(API::Node node) {
exists(DataFlow::ClassNode cls |
classHasFallbackName(cls, _, _, _) and
node = API::Internal::getClassInstance(cls)
)
}
/** Gets the shortest distance from a node with a fallback name, to `nd` in the API graph. */
private predicate distanceFromFallbackName =
ApiGraphDistance<isClassInstanceWithFallbackName/1, relevantEdge/2>::distanceTo/1;
/** Gets the shortest distance from a name-root (package export or fallback name) to `nd` */
private int distanceFromRoot(API::Node nd) {
result = distanceFromPackageExport(nd)
or
not exists(distanceFromPackageExport(nd)) and
result = 100 + distanceFromFallbackName(nd)
}
/** Holds if `node` can be given a name. */
private predicate isRelevant(API::Node node) { exists(distanceFromRoot(node)) }
/**
* Holds if `node` is a default export that can be reinterpreted as a namespace export,
* because the enclosing module has no named exports.
*/
private predicate defaultExportCanBeInterpretedAsNamespaceExport(API::Node node) {
exists(ES2015Module mod |
node.asSink() = mod.getAnExportedValue("default") and
not mod.hasBothNamedAndDefaultExports()
)
}
private predicate isPrivateAssignment(DataFlow::Node node) {
exists(MemberDeclaration decl |
node = decl.getInit().flow() and
decl.isPrivate()
)
or
exists(DataFlow::PropWrite write |
write.isPrivateField() and
node = write.getRhs()
)
}
private predicate isPrivateLike(API::Node node) { isPrivateAssignment(node.asSink()) }
bindingset[name]
private int getNameBadness(string name) {
if name = ["constructor", "default"] then result = 10 else result = 0
}
private API::Node getASuccessor(API::Node node, string name, int badness) {
isRelevant(node) and
isRelevant(result) and
(
exists(string member |
result = node.getMember(member) and
if member = "default" and defaultExportCanBeInterpretedAsNamespaceExport(node)
then (
badness = 5 and name = ""
) else (
name = member and
badness = getNameBadness(name)
)
)
or
instanceEdge(node, result) and
name = "prototype" and
badness = 0
)
}
private API::Node getAPredecessor(API::Node node, string name, int badness) {
node = getASuccessor(result, name, badness)
}
/**
* Gets the predecessor of `node` to use when constructing a qualified name for it,
* and binds `name` and `badness` corresponding to the label on that edge.
*/
private API::Node getPreferredPredecessor(API::Node node, string name, int badness) {
// For root nodes, we prefer not having a predecessor, as we use the package name.
not isPackageExport(node) and
// Rank predecessors by name-badness, export-distance, and name.
// Since min() can only return a single value, we need a separate min() call per column.
badness =
min(API::Node pred, int b |
pred = getAPredecessor(node, _, b) and
// ensure the preferred predecessor is strictly closer to a root export, even if it means accepting more badness
distanceFromRoot(pred) < distanceFromRoot(node)
|
b
) and
result =
min(API::Node pred, string name1 |
pred = getAPredecessor(node, name1, badness) and
// ensure the preferred predecessor is strictly closer to a root export, even if it means accepting more badness
distanceFromRoot(pred) < distanceFromRoot(node)
|
pred order by distanceFromRoot(pred), name1
) and
name = min(string n | result = getAPredecessor(node, n, badness) | n)
}
/**
* Holds if `(package, name)` is a potential name to associate with `sink`.
*
* `badness` is bound to the associated badness of the name.
*/
private predicate sinkHasNameCandidate(API::Node sink, string package, string name, int badness) {
sink = API::moduleExport(package) and
name = "" and
badness = 0
or
exists(DataFlow::ClassNode cls, string className |
sink = API::Internal::getClassInstance(cls) and
classHasFallbackName(cls, package, className, badness) and
name = join(className, "prototype")
)
or
exists(API::Node baseNode, string baseName, int baseBadness, string step, int stepBadness |
sinkHasNameCandidate(baseNode, package, baseName, baseBadness) and
baseNode = getPreferredPredecessor(sink, step, stepBadness) and
badness = baseBadness + stepBadness and
name = join(baseName, step)
)
}
/**
* Holds if `(package, name)` is the primary name to associate with `sink`.
*
* `badness` is bound to the associated badness of the name.
*/
private predicate sinkHasPrimaryName(API::Node sink, string package, string name, int badness) {
badness = min(int b | sinkHasNameCandidate(sink, _, _, b) | b) and
package = min(string p | sinkHasNameCandidate(sink, p, _, badness) | p) and
name = min(string n | sinkHasNameCandidate(sink, package, n, badness) | n order by n.length(), n)
}
/**
* Holds if `(package, name)` is the primary name to associate with `node`.
*/
predicate sinkHasPrimaryName(API::Node sink, string package, string name) {
sinkHasPrimaryName(sink, package, name, _)
}
/** Gets a source node that can flow to `sink` without using a return step. */
private DataFlow::SourceNode nodeReachingSink(API::Node sink, DataFlow::TypeBackTracker t) {
t.start() and
result = sink.asSink().getALocalSource()
or
exists(DataFlow::TypeBackTracker t2 |
result = nodeReachingSink(sink, t2).backtrack(t2, t) and
t.hasReturn() = false
)
}
/** Gets a source node that can flow to `sink` without using a return step. */
DataFlow::SourceNode nodeReachingSink(API::Node sink) {
result = nodeReachingSink(sink, DataFlow::TypeBackTracker::end())
}
/** Gets a sink node reachable from `node`. */
private API::Node getASinkNode(DataFlow::SourceNode node) { node = nodeReachingSink(result) }
/**
* Holds if `node` is assigned to a global access path. Note that such nodes generally do not have API nodes.
*/
private predicate nameFromGlobal(DataFlow::Node node, string package, string name, int badness) {
package = "global" and
node = AccessPath::getAnAssignmentTo(name) and
(if node.getTopLevel().isExterns() then badness = -10 else badness = 10)
}
/** Holds if an instance of `cls` can be exposed to client code. */
private predicate hasEscapingInstance(DataFlow::ClassNode cls) {
cls.getAnInstanceReference().flowsTo(any(API::Node n).asSink())
}
private predicate sourceNodeHasNameCandidate(
DataFlow::SourceNode node, string package, string name, int badness
) {
sinkHasPrimaryName(getASinkNode(node), package, name, badness)
or
nameFromGlobal(node, package, name, badness)
}
private predicate sourceNodeHasPrimaryName(
DataFlow::SourceNode node, string package, string name, int badness
) {
badness = min(int b | sourceNodeHasNameCandidate(node, _, _, b) | b) and
package =
min(string p | sourceNodeHasNameCandidate(node, p, _, badness) | p order by p.length(), p) and
name =
min(string n | sourceNodeHasNameCandidate(node, package, n, badness) | n order by n.length(), n)
}
/** Gets a data flow node referring to a function value. */
private DataFlow::SourceNode functionValue(DataFlow::TypeTracker t) {
t.start() and
(
result instanceof DataFlow::FunctionNode
or
result instanceof DataFlow::ClassNode
or
result instanceof DataFlow::PartialInvokeNode
or
result = DataFlow::globalVarRef(["Function", "eval"]).getAnInvocation()
or
// Assume double-invocation of Function also returns a function
result = DataFlow::globalVarRef("Function").getAnInvocation().getAnInvocation()
)
or
exists(DataFlow::TypeTracker t2 | result = functionValue(t2).track(t2, t))
}
/** Gets a data flow node referring to a function value. */
private DataFlow::SourceNode functionValue() {
result = functionValue(DataFlow::TypeTracker::end())
}
/**
* Holds if `node` is a function or a call that returns a function.
*/
private predicate isFunctionSource(DataFlow::SourceNode node) {
(
exists(getASinkNode(node))
or
nameFromGlobal(node, _, _, _)
) and
(
node instanceof DataFlow::FunctionNode
or
node instanceof DataFlow::ClassNode
or
node = functionValue() and
node instanceof DataFlow::InvokeNode and
// `getASinkNode` steps through imports (but not other calls) so exclude calls that are imports (i.e. require calls)
// as we want to get as close to the source as possible.
not node instanceof DataFlow::ModuleImportNode
)
}
/**
* Holds if `(package, name)` is the primary name for the given `function`.
*
* The `function` node may be an actual function expression, or a call site from which a function is returned.
*/
predicate functionHasPrimaryName(DataFlow::SourceNode function, string package, string name) {
sourceNodeHasPrimaryName(function, package, name, _) and
isFunctionSource(function)
}
private predicate sinkHasSourceName(API::Node sink, string package, string name, int badness) {
exists(DataFlow::SourceNode source |
sink = getASinkNode(source) and
sourceNodeHasPrimaryName(source, package, name, badness)
)
}
private predicate sinkHasPrimarySourceName(API::Node sink, string package, string name) {
strictcount(string p, string n | sinkHasSourceName(sink, p, n, _)) = 1 and
sinkHasSourceName(sink, package, name, _)
}
private predicate aliasCandidate(
string package, string name, string targetPackage, string targetName, API::Node aliasDef
) {
sinkHasPrimaryName(aliasDef, package, name) and
sinkHasPrimarySourceName(aliasDef, targetPackage, targetName) and
not sinkHasSourceName(_, package, name, _) // (package, name) cannot be an alias if a source has it as its primary name
}
private predicate nonAlias(string package, string name) {
// `(package, name)` appears to be an alias for multiple things. Treat it as a primary name instead.
strictcount(string targetPackage, string targetName |
aliasCandidate(package, name, targetPackage, targetName, _)
) > 1
or
// Not all sinks with this name agree on the alias target
exists(API::Node sink, string targetPackage, string targetName |
aliasCandidate(package, name, targetPackage, targetName, _) and
sinkHasPrimaryName(sink, package, name) and
not sinkHasPrimarySourceName(sink, targetPackage, targetName)
)
}
/**
* Holds if `(package, name)` is an alias for `(targetPackage, targetName)`,
* defined at `aliasDef`.
*
* Only the last component of an access path is reported as an alias, the prefix always
* uses the primary name for that access path. The aliases for the prefix are reported
* as separate tuples.
*
* For example, we might report that `a.b.C` is an alias for `a.b.c`, and that `a.B` is an alias for `a.b`.
* By combining the two aliasing facts, we may conclude that `a.B.C` is an alias for `a.b.c`, but this fact is not
* reported separately.
*/
predicate aliasDefinition(
string package, string name, string targetPackage, string targetName, API::Node aliasDef
) {
aliasCandidate(package, name, targetPackage, targetName, aliasDef) and
not nonAlias(package, name)
}
/**
* Converts a `(package, name)` pair to a string of form `(package).name`.
*/
bindingset[package, name]
string renderName(string package, string name) { result = join("(" + package + ")", name) }
/**
* Contains predicates for naming individual modules (i.e. files) inside of a package.
*
* These names are not necessarily part of a package's public API, and so we only used them
* as a fallback when a publicly-accessible access path cannot be found.
*/
private module InternalModuleNaming {
/** Gets the path to `folder` relative to its enclosing non-private `package.json` file. */
private string getPackageRelativePathFromFolder(Folder folder) {
exists(PackageJson json |
json.getFile() = folder.getFile("package.json") and
not json.isPrivate() and
result = json.getPackageName()
)
or
not exists(folder.getFile("package.json")) and
result =
getPackageRelativePathFromFolder(folder.getParentContainer()) + "/" + folder.getBaseName()
}
private string getPackageRelativePath(Module mod) {
exists(PackageJson json, string relativePath |
not json.isPrivate() and
json.getExportedModule(relativePath) = mod and
if relativePath = "."
then result = json.getPackageName()
else result = json.getPackageName() + "/" + relativePath.regexpReplaceAll("^\\./", "")
)
or
not mod = any(PackageJson json | not json.isPrivate()).getExportedModule(_) and
not mod.isAmbient() and
exists(string folderPath |
folderPath = getPackageRelativePathFromFolder(mod.getFile().getParentContainer()) and
if mod.getName() = "index"
then result = folderPath
else result = folderPath + "/" + mod.getName()
)
}
/** Holds if `(package, name)` should be used to refer to code inside `mod`. */
predicate fallbackModuleName(Module mod, string package, string name, int badness) {
badness = 50 and
package = getPackageRelativePath(mod) and
name = ""
}
}
/**
* Contains query predicates for emitting debugging information about endpoint naming.
*/
module Debug {
/** Holds if `node` has multiple preferred predecessors. */
query predicate ambiguousPreferredPredecessor(API::Node node) {
strictcount(API::Node pred, string name, int badness |
pred = getPreferredPredecessor(node, name, badness)
) > 1
}
/** Holds if the given `node` has multiple primary names. */
query string ambiguousSinkName(API::Node node) {
strictcount(string package, string name | sinkHasPrimaryName(node, package, name)) > 1 and
result =
concat(string package, string name |
sinkHasPrimaryName(node, package, name)
|
renderName(package, name), ", "
)
}
/** Holds if the given `node` has multiple primary names. */
query string ambiguousFunctionName(DataFlow::FunctionNode node) {
strictcount(string package, string name | functionHasPrimaryName(node, package, name)) > 1 and
result =
concat(string package, string name |
functionHasPrimaryName(node, package, name)
|
renderName(package, name), ", "
)
}
}

View File

@@ -75,7 +75,7 @@ module HeuristicNames {
* Gets a regular expression that identifies strings that may indicate the presence of
* a certificate.
*/
string maybeCertificate() { result = "(?is).*(cert)(?!.*(format|name)).*" }
string maybeCertificate() { result = "(?is).*(cert)(?!.*(format|name|ification)).*" }
/**
* Gets a regular expression that identifies strings that may indicate the presence