Files
codeql/javascript/ql/src/Declarations/UnstableCyclicImport.ql
2020-10-15 20:57:29 +02:00

156 lines
5.7 KiB
Plaintext

/**
* @name Unstable cyclic import
* @description If the top-level of a module accesses a variable from a cyclic import, its value depends on
* which module is globally imported first.
* @kind problem
* @problem.severity warning
* @id js/unstable-cyclic-import
* @tags maintainability
* @precision low
*/
import javascript
/**
* Holds if the contents of the given container are executed as part of the top-level code,
* and it is unreachable after top-level execution.
*
* Some of this code might be conditionally executed, but the fact that it can only execute
* as part of the top-level means cyclic imports can't be known to be resolved at this stage.
*/
predicate isImmediatelyExecutedContainer(StmtContainer container) {
container instanceof TopLevel
or
// Namespaces are immediately executed (they cannot be declared inside a function).
container instanceof NamespaceDeclaration
or
// IIFEs at the top-level are immediately executed
exists(ImmediatelyInvokedFunctionExpr function | container = function |
not function.isAsync() and
not function.isGenerator() and
isImmediatelyExecutedContainer(container.getEnclosingContainer())
)
}
/**
* Holds if the given import is only used to import type names, hence has no runtime effect.
*/
predicate isAmbientImport(ImportDeclaration decl) {
decl.getFile().getFileType().isTypeScript() and
exists(decl.getASpecifier()) and
not exists(decl.getASpecifier().getLocal().getVariable().getAnAccess())
}
/**
* Gets an import in `source` that imports `destination` at runtime.
*/
Import getARuntimeImport(Module source, Module destination) {
result = source.getAnImport() and
result.getImportedModule() = destination and
not isAmbientImport(result)
}
predicate isImportedAtRuntime(Module source, Module destination) {
exists(getARuntimeImport(source, destination))
}
/**
* A variable access that is executed as part of the top-level and is not part of an export.
*
* Exported variables reference the variable itself, as opposed it its current value, so it is not
* necessarily an error to export it before it has been initialized. Some runtimes and transpilers
* do not support such name bindings yet, but they are safe according to the spec, so we do not report them.
*/
class CandidateVarAccess extends VarAccess {
CandidateVarAccess() {
isImmediatelyExecutedContainer(getContainer()) and
not exists(ExportSpecifier spec | spec.getLocal() = this)
}
}
/**
* Gets the first candidate access to a variable imported by the given import declaration.
*
* We use this to avoid duplicate alerts about the same underlying cyclic import.
*/
VarAccess getFirstCandidateAccess(ImportDeclaration decl) {
result =
min(decl.getASpecifier().getLocal().getVariable().getAnAccess().(CandidateVarAccess) as p
order by
p.getLocation().getStartLine(), p.getLocation().getStartColumn()
)
}
/**
* This is the main alert definition.
*
* The predicate is used to restrict the path construction predicate to just those paths that
* are relevant for the alerts we found.
*/
predicate cycleAlert(Module mod, ImportDeclaration import_, Module importedModule, VarAccess access) {
import_ = mod.getAnImport() and
access = getFirstCandidateAccess(import_) and
importedModule = import_.getImportedModule() and
importedModule != mod and // don't report self-imports
// Suppress warning if this is the unique importer of that module.
// That's a sufficient and somewhat maintainable safety guarantee.
exists(Module otherEntry | isImportedAtRuntime(otherEntry, importedModule) and otherEntry != mod)
}
/** Holds if the length of the shortest sequence of runtime imports from `source` to `destination` is `steps`. */
predicate numberOfStepsToModule(Module source, Module destination, int steps) =
shortestDistances(anyModule/1, isImportedAtRuntime/2)(source, destination, steps)
predicate anyModule(Module m) { any() }
/**
* Gets the name of the module containing the given import.
*/
string repr(Import import_) { result = import_.getEnclosingModule().getName() }
/**
* Builds a string visualizing the shortest import path from `source` to `destination`, excluding
* the destination.
*
* For example, the path from `A` to `D` might be:
* ```
* A => B => C
* ```
* Notice that `D` is not mentioned in the output.
*
* The caller will then complete the cycle by putting `D` at the beginning and end:
* ```
* D => A => B => C => D
* ```
*/
string pathToModule(Module source, Module destination, int steps) {
// Restrict paths to those that are relevant for building a path from the imported module of an alert back to the importer.
exists(Module m | cycleAlert(destination, _, m, _) and numberOfStepsToModule(m, source, _)) and
numberOfStepsToModule(source, destination, steps) and
(
steps = 1 and
result = repr(getARuntimeImport(source, destination))
or
steps > 1 and
exists(Module next |
// Only extend the path to one of the potential successors, as we only need one example.
next =
min(Module mod |
isImportedAtRuntime(source, mod) and
numberOfStepsToModule(mod, destination, steps - 1)
|
mod order by mod.getName()
) and
result =
repr(getARuntimeImport(source, next)) + " => " + pathToModule(next, destination, steps - 1)
)
)
}
from Module mod, ImportDeclaration import_, Module importedModule, VarAccess access
where cycleAlert(mod, import_, importedModule, access)
select access,
access.getName() + " is uninitialized if $@ is loaded first in the cyclic import:" + " " +
repr(import_) + " => " + min(pathToModule(importedModule, mod, _)) + " => " + repr(import_) +
".", import_.getImportedPath(), importedModule.getName()