mirror of
https://github.com/github/codeql.git
synced 2025-12-31 08:06:36 +01:00
502 lines
16 KiB
Plaintext
502 lines
16 KiB
Plaintext
/**
|
|
* Provides models of "portals", that is, interface points between different
|
|
* components of a code base. A typical example of a portal is a parameter
|
|
* of a function exported by an npm package (which is the only kind of component
|
|
* we support at the moment).
|
|
*
|
|
* Portals have entry and exit nodes. For example, the (unique) exit node of
|
|
* the parameter of an exported function is the `ParameterNode` corresponding
|
|
* to that parameter, while its entries are all nodes corresponding to arguments
|
|
* passed into the parameter via a call.
|
|
*
|
|
* The API of this library is not stable yet and may change.
|
|
*/
|
|
|
|
import javascript
|
|
|
|
private newtype TPortal =
|
|
MkGlobalObjectPortal() or
|
|
MkNpmPackagePortal(string pkgName) {
|
|
NpmPackagePortal::imports(_, pkgName) or
|
|
NpmPackagePortal::imports(_, pkgName, _) or
|
|
NpmPackagePortal::exports(pkgName, _) or
|
|
MemberPortal::exports(pkgName, _, _)
|
|
} or
|
|
MkMemberPortal(Portal base, string prop) {
|
|
(
|
|
MemberPortal::reads(base, prop, _, _) or
|
|
MemberPortal::writes(base, prop, _, _)
|
|
) and
|
|
// only consider alpha-numeric properties, excluding special properties
|
|
// and properties whose names look like they are meant to be internal
|
|
prop.regexpMatch("(?!prototype$|__)[a-zA-Z_]\\w*")
|
|
} or
|
|
MkInstancePortal(Portal base) {
|
|
InstancePortal::instanceUse(base, _, _) or
|
|
InstancePortal::instanceDef(base, _, _) or
|
|
InstancePortal::instanceMemberDef(base, _, _, _)
|
|
} or
|
|
MkParameterPortal(Portal base, int i) {
|
|
ParameterPortal::parameter(base, i, _, _) or
|
|
ParameterPortal::argument(base, i, _, _)
|
|
} or
|
|
MkReturnPortal(Portal base) {
|
|
ReturnPortal::calls(_, base, _) or
|
|
ReturnPortal::returns(base, _, _)
|
|
}
|
|
|
|
/**
|
|
* A portal, that is, an interface point between different npm packages.
|
|
*/
|
|
cached
|
|
class Portal extends TPortal {
|
|
/**
|
|
* Gets an exit node for this portal, that is, a node from which data
|
|
* that comes through the portal emerges. The flag `isRemote`
|
|
* indicates whether data read from this node may come from a different
|
|
* package.
|
|
*/
|
|
cached
|
|
abstract DataFlow::SourceNode getAnExitNode(boolean isRemote);
|
|
|
|
/**
|
|
* Gets an entry node for this portal, that is, a node through which data
|
|
* enters the portal. The flag `escapes` indicates whether data written to
|
|
* the node may escape to a different package.
|
|
*/
|
|
cached
|
|
abstract DataFlow::Node getAnEntryNode(boolean escapes);
|
|
|
|
/**
|
|
* Gets the member portal with the given `name` of this portal, if any.
|
|
*/
|
|
cached
|
|
MemberPortal getMember(string name) {
|
|
result.getName() = name and
|
|
result.getBasePortal() = this
|
|
}
|
|
|
|
/**
|
|
* Gets the instance portal of this portal, if any.
|
|
*/
|
|
cached
|
|
InstancePortal getInstance() { result.getBasePortal() = this }
|
|
|
|
/**
|
|
* Gets the portal of parameter `idx` of this portal, if any.
|
|
*/
|
|
cached
|
|
ParameterPortal getParameter(int idx) {
|
|
result.getIndex() = idx and
|
|
result.getBasePortal() = this
|
|
}
|
|
|
|
/**
|
|
* Gets the return value portal of this portal, if any.
|
|
*/
|
|
cached
|
|
ReturnPortal getReturn() { result.getBasePortal() = this }
|
|
|
|
/**
|
|
* Gets the `i`th base portal of this portal.
|
|
*
|
|
* The `0`th base portal is the portal itself, the `n+1`st base portal is the `n`th base portal
|
|
* of the portal `p` of which this is a member, instance, parameter, or return portal.
|
|
*/
|
|
cached
|
|
Portal getBasePortal(int i) {
|
|
i = 0 and
|
|
result = this
|
|
or
|
|
result = this.(CompoundPortal).getBasePortal().getBasePortal(i - 1)
|
|
}
|
|
|
|
/**
|
|
* Gets a textual representation of this portal.
|
|
*
|
|
* Different portals must have different `toString`s, so the result of
|
|
* this predicate can be used to uniquely identify a portal.
|
|
*/
|
|
cached
|
|
abstract string toString();
|
|
|
|
/**
|
|
* INTERNAL: Do not use outside this library.
|
|
*
|
|
* The constructor depth of this portal, used to limit the number of
|
|
* portals.
|
|
*/
|
|
cached
|
|
abstract int depth();
|
|
}
|
|
|
|
/**
|
|
* A portal representing the global object.
|
|
*/
|
|
private class GlobalObjectPortal extends Portal, MkGlobalObjectPortal {
|
|
override DataFlow::SourceNode getAnExitNode(boolean isRemote) {
|
|
result = DataFlow::globalObjectRef() and
|
|
isRemote = true
|
|
}
|
|
|
|
override DataFlow::Node getAnEntryNode(boolean escapes) { none() }
|
|
|
|
override string toString() { result = "(global)" }
|
|
|
|
override int depth() { result = 1 }
|
|
}
|
|
|
|
/**
|
|
* A portal representing the exports value of the main module of an npm
|
|
* package (that is, a value of `module.exports` for CommonJS modules, or
|
|
* the module namespace object for ES2015 modules).
|
|
*
|
|
* Assignments to `module.exports` are entries to this portal, while
|
|
* imports are exits.
|
|
*/
|
|
private class NpmPackagePortal extends Portal, MkNpmPackagePortal {
|
|
string pkgName;
|
|
|
|
NpmPackagePortal() { this = MkNpmPackagePortal(pkgName) }
|
|
|
|
/** Gets the name of the npm package. */
|
|
string getName() { result = pkgName }
|
|
|
|
override DataFlow::SourceNode getAnExitNode(boolean isRemote) {
|
|
NpmPackagePortal::imports(result, pkgName) and
|
|
isRemote = false
|
|
}
|
|
|
|
override DataFlow::Node getAnEntryNode(boolean escapes) {
|
|
NpmPackagePortal::exports(pkgName, result) and
|
|
escapes = true
|
|
}
|
|
|
|
override string toString() { result = "(root https://www.npmjs.com/package/" + pkgName + ")" }
|
|
|
|
override int depth() { result = 1 }
|
|
}
|
|
|
|
private module NpmPackagePortal {
|
|
/** Gets an import of `imported` inside package `importer`. */
|
|
pragma[noinline]
|
|
private DataFlow::SourceNode getAModuleImport(NpmPackage importer, string imported) {
|
|
result = DataFlow::moduleImport(imported) and
|
|
result.getTopLevel() = importer.getAModule()
|
|
}
|
|
|
|
/** Gets an import of `member` from `imported` inside package `importer`. */
|
|
pragma[noinline]
|
|
private DataFlow::SourceNode getAModuleMemberImport(
|
|
NpmPackage importer, string imported, string member
|
|
) {
|
|
result = DataFlow::moduleMember(imported, member) and
|
|
result.getTopLevel() = importer.getAModule()
|
|
}
|
|
|
|
/** Holds if `imp` is an import of package `pkgName`. */
|
|
predicate imports(DataFlow::SourceNode imp, string pkgName) {
|
|
imp = getAModuleImport(_, pkgName) and
|
|
pkgName.regexpMatch("[^./].*")
|
|
}
|
|
|
|
/** Holds if `imp` imports `member` from package `pkgName`. */
|
|
predicate imports(DataFlow::SourceNode imp, string pkgName, string member) {
|
|
imp = getAModuleMemberImport(_, pkgName, member) and
|
|
pkgName.regexpMatch("[^./].*")
|
|
}
|
|
|
|
/** Gets the main module of package `pkgName`. */
|
|
Module packageMain(string pkgName) {
|
|
exists(PackageJson pkg |
|
|
// don't construct portals for private packages
|
|
not pkg.isPrivate() and
|
|
// don't construct portals for vendored-in packages
|
|
exists(Folder pkgDir | pkgDir = pkg.getFile().getParentContainer() |
|
|
pkgDir.getRelativePath() = ""
|
|
or
|
|
not pkgDir.getParentContainer().getBaseName() = "node_modules"
|
|
) and
|
|
pkg.getPackageName() = pkgName and
|
|
result = pkg.getMainModule()
|
|
)
|
|
}
|
|
|
|
/** Holds if the main module of package `pkgName` exports `exp`. */
|
|
predicate exports(string pkgName, DataFlow::Node exp) {
|
|
exists(Module m | m = packageMain(pkgName) |
|
|
exists(AnalyzedPropertyWrite apw |
|
|
apw.writes(m.(AnalyzedModule).getModuleObject(), "exports", exp)
|
|
)
|
|
or
|
|
exists(DataFlow::PropWrite export | exp = export |
|
|
export.getRhs() = m.(ES2015Module).getAnExportedValue("default")
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the maximum depth a portal may have.
|
|
*
|
|
* This is a somewhat crude way of preventing us from constructing infinitely many portals.
|
|
*/
|
|
private int maxdepth() { result = 10 }
|
|
|
|
/**
|
|
* A portal that is constructed over some base portal.
|
|
*/
|
|
abstract private class CompoundPortal extends Portal {
|
|
Portal base;
|
|
|
|
bindingset[this]
|
|
CompoundPortal() {
|
|
// bound size of portal to prevent infinite recursion
|
|
base.depth() < maxdepth()
|
|
}
|
|
|
|
/** Gets the base portal over which this portal is constructed. */
|
|
Portal getBasePortal() { result = base }
|
|
|
|
override int depth() { result = base.depth() + 1 }
|
|
}
|
|
|
|
/**
|
|
* A portal corresponding to a named property of objects flowing through another portal.
|
|
*
|
|
* Entries to this portal are the right-hand sides of writes to the property, while
|
|
* property reads are exits.
|
|
*/
|
|
private class MemberPortal extends CompoundPortal, MkMemberPortal {
|
|
string prop;
|
|
|
|
MemberPortal() { this = MkMemberPortal(base, prop) }
|
|
|
|
/** Gets the name of this member. */
|
|
string getName() { result = prop }
|
|
|
|
override DataFlow::SourceNode getAnExitNode(boolean isRemote) {
|
|
MemberPortal::reads(base, prop, result, isRemote)
|
|
}
|
|
|
|
override DataFlow::Node getAnEntryNode(boolean escapes) {
|
|
MemberPortal::writes(base, prop, result, escapes)
|
|
}
|
|
|
|
override string toString() { result = "(member " + prop + " " + base + ")" }
|
|
}
|
|
|
|
private module MemberPortal {
|
|
/** Gets a node representing a value flowing through `base`, that is, either an entry node or an exit node. */
|
|
private DataFlow::SourceNode portalBaseRef(Portal base, boolean escapes) {
|
|
result = base.getAnExitNode(escapes)
|
|
or
|
|
result = base.getAnEntryNode(escapes).getALocalSource()
|
|
}
|
|
|
|
/** Holds if `read` is a read of property `prop` of a value flowing through `base`. */
|
|
predicate reads(Portal base, string prop, DataFlow::SourceNode read, boolean isRemote) {
|
|
read = portalBaseRef(base, isRemote).getAPropertyRead(prop)
|
|
or
|
|
// imports are a kind of property read
|
|
exists(string pkg |
|
|
NpmPackagePortal::imports(read, pkg, prop) and
|
|
base = MkNpmPackagePortal(pkg) and
|
|
isRemote = false
|
|
)
|
|
or
|
|
// global variable reads are a kind of property read
|
|
base instanceof GlobalObjectPortal and
|
|
read = DataFlow::globalVarRef(prop) and
|
|
isRemote = true
|
|
}
|
|
|
|
/** Holds if the main module of `pkgName` exports `rhs` under the name `prop`. */
|
|
predicate exports(string pkgName, string prop, DataFlow::Node rhs) {
|
|
exists(AnalyzedModule m, AnalyzedPropertyWrite apw |
|
|
m = NpmPackagePortal::packageMain(pkgName) and
|
|
apw.writes(m.getAnExportsValue(), prop, rhs)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Holds if there is a write to property `prop` of a value flowing through `base`, and `rhs` is the
|
|
* right-hand side of that write.
|
|
*/
|
|
predicate writes(Portal base, string prop, DataFlow::Node rhs, boolean escapes) {
|
|
portalBaseRef(base, escapes).hasPropertyWrite(prop, rhs)
|
|
or
|
|
InstancePortal::instanceMemberDef(base.(InstancePortal).getBasePortal(), prop, rhs, escapes)
|
|
or
|
|
// exports are a kind of property write.
|
|
exists(string pkgName |
|
|
exports(pkgName, prop, rhs) and
|
|
base = MkNpmPackagePortal(pkgName) and
|
|
escapes = true
|
|
)
|
|
or
|
|
// global variable writes are a kind of property write
|
|
base instanceof GlobalObjectPortal and
|
|
exists(AssignExpr assgn |
|
|
assgn.getLhs() = DataFlow::globalVarRef(prop).asExpr() and
|
|
rhs = assgn.getRhs().flow()
|
|
) and
|
|
escapes = true
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A portal corresponding to an instantiation of functions or classes flowing through
|
|
* another portal.
|
|
*
|
|
* Entries to this portal are the return values of functions that flow through the base
|
|
* portal (to model the fact that `new f()` evaluates to the return value of `f` it is
|
|
* non-primitive), while exits are `new` expressions and other expressions referring to
|
|
* instances of functions/classes flowing through the base portal.
|
|
*/
|
|
private class InstancePortal extends CompoundPortal, MkInstancePortal {
|
|
InstancePortal() { this = MkInstancePortal(base) }
|
|
|
|
override DataFlow::SourceNode getAnExitNode(boolean isRemote) {
|
|
InstancePortal::instanceUse(base, result, isRemote)
|
|
}
|
|
|
|
override DataFlow::Node getAnEntryNode(boolean escapes) {
|
|
InstancePortal::instanceDef(base, result, escapes)
|
|
}
|
|
|
|
override string toString() { result = "(instance " + base + ")" }
|
|
}
|
|
|
|
private module InstancePortal {
|
|
/**
|
|
* Holds if `ctor` is a class, or a function that looks like a constructor function,
|
|
* that is, it contains a `this` expression.
|
|
*/
|
|
private predicate instantiable(DataFlow::Node ctor) {
|
|
ctor.getAstNode() instanceof ClassDefinition
|
|
or
|
|
exists(ThisExpr thiz | ctor = thiz.getBinder().flow())
|
|
}
|
|
|
|
/** Holds if `i` represents instances of `ctor`, which flows into `base`. */
|
|
private predicate isInstance(
|
|
Portal base, DataFlow::SourceNode ctor, AbstractInstance i, boolean escapes
|
|
) {
|
|
ctor = DataFlow::valueNode(i.getConstructor().getDefinition()) and
|
|
ctor.flowsTo(base.getAnEntryNode(escapes)) and
|
|
instantiable(ctor)
|
|
}
|
|
|
|
/** Holds if `nd` is an expression evaluating to an instance of `base`. */
|
|
predicate instanceUse(Portal base, DataFlow::SourceNode nd, boolean isRemote) {
|
|
nd = base.getAnExitNode(isRemote).getAnInstantiation()
|
|
or
|
|
isInstance(base, _, nd.analyze().getAValue(), isRemote)
|
|
}
|
|
|
|
/**
|
|
* Holds if there is a definition of a property `name` on an instance of `base`, and `rhs` is the
|
|
* right-hand side of that definition.
|
|
*/
|
|
predicate instanceMemberDef(Portal base, string name, DataFlow::Node rhs, boolean escapes) {
|
|
exists(DataFlow::SourceNode ctor | isInstance(base, ctor, _, escapes) |
|
|
// ES2015 instance method
|
|
exists(MemberDefinition mem |
|
|
mem = ctor.getAstNode().(ClassDefinition).getAMember() and
|
|
not mem.isStatic() and
|
|
not mem instanceof ConstructorDefinition
|
|
|
|
|
name = mem.getName() and
|
|
rhs = DataFlow::valueNode(mem.getInit())
|
|
)
|
|
or
|
|
// ES5 instance method
|
|
exists(DataFlow::PropWrite pw |
|
|
pw = ctor.getAPropertyRead("prototype").getAPropertyWrite(name) and
|
|
rhs = pw.getRhs()
|
|
)
|
|
)
|
|
}
|
|
|
|
/** Holds if `nd` is a return node of a function flowing into `base`. */
|
|
predicate instanceDef(Portal base, DataFlow::Node nd, boolean escapes) {
|
|
exists(DataFlow::FunctionNode fn |
|
|
isInstance(base, fn, _, escapes) and
|
|
nd = fn.getAReturn() and
|
|
instantiable(fn)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A portal corresponding to a positional parameter of another portal.
|
|
*
|
|
* Arguments to functions flowing through the base portal are entries, while the corresponding
|
|
* parameter nodes are exits.
|
|
*/
|
|
class ParameterPortal extends CompoundPortal, MkParameterPortal {
|
|
int i;
|
|
|
|
ParameterPortal() { this = MkParameterPortal(base, i) }
|
|
|
|
/** Gets the index of the parameterb represented by this portal. */
|
|
int getIndex() { result = i }
|
|
|
|
override DataFlow::SourceNode getAnExitNode(boolean isRemote) {
|
|
ParameterPortal::parameter(base, i, result, isRemote)
|
|
}
|
|
|
|
override DataFlow::Node getAnEntryNode(boolean escapes) {
|
|
ParameterPortal::argument(base, i, result, escapes)
|
|
}
|
|
|
|
override string toString() { result = "(parameter " + i + " " + base + ")" }
|
|
}
|
|
|
|
private module ParameterPortal {
|
|
/** Holds if `param` is the `i`th parameter of a function flowing through `base`. */
|
|
predicate parameter(Portal base, int i, DataFlow::SourceNode param, boolean isRemote) {
|
|
param = base.getAnEntryNode(isRemote).getALocalSource().(DataFlow::FunctionNode).getParameter(i)
|
|
}
|
|
|
|
/** Holds if `arg` is the `i`th argument passed to an invocation of a function flowing through `base`. */
|
|
predicate argument(Portal base, int i, DataFlow::Node arg, boolean escapes) {
|
|
exists(DataFlow::InvokeNode invk |
|
|
invk = base.getAnExitNode(escapes).getAnInvocation() and
|
|
arg = invk.getArgument(i)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A portal corresponding to the return value of another portal.
|
|
*
|
|
* Returned expressions are entries, calls are exits.
|
|
*/
|
|
class ReturnPortal extends CompoundPortal, MkReturnPortal {
|
|
ReturnPortal() { this = MkReturnPortal(base) }
|
|
|
|
override DataFlow::SourceNode getAnExitNode(boolean isRemote) {
|
|
ReturnPortal::calls(result, base, isRemote)
|
|
}
|
|
|
|
override DataFlow::Node getAnEntryNode(boolean escapes) {
|
|
ReturnPortal::returns(base, result, escapes)
|
|
}
|
|
|
|
override string toString() { result = "(return " + base + ")" }
|
|
}
|
|
|
|
private module ReturnPortal {
|
|
/** Holds if `invk` is a call to a function flowing through `callee`. */
|
|
predicate calls(DataFlow::InvokeNode invk, Portal callee, boolean isRemote) {
|
|
invk = callee.getAnExitNode(isRemote).getAnInvocation()
|
|
}
|
|
|
|
/** Holds if `ret` is a return node of a function flowing through `base`. */
|
|
predicate returns(Portal base, DataFlow::Node ret, boolean escapes) {
|
|
ret = base.getAnEntryNode(escapes).getALocalSource().(DataFlow::FunctionNode).getAReturn()
|
|
}
|
|
}
|