diff --git a/javascript/ql/lib/semmle/javascript/ApiGraphs.qll b/javascript/ql/lib/semmle/javascript/ApiGraphs.qll index 803b4ac4c82d..9e866424c619 100644 --- a/javascript/ql/lib/semmle/javascript/ApiGraphs.qll +++ b/javascript/ql/lib/semmle/javascript/ApiGraphs.qll @@ -7,6 +7,7 @@ import javascript private import semmle.javascript.dataflow.internal.FlowSteps as FlowSteps +private import semmle.javascript.dataflow.internal.PreCallGraphStep private import internal.CachedStages /** @@ -782,6 +783,13 @@ module API { rhs = m.getAnExportedValue(prop) ) or + // In general, turn store steps into member steps for def-nodes + exists(string prop | + PreCallGraphStep::storeStep(rhs, pred, prop) and + lbl = Label::member(prop) and + not DataFlow::PseudoProperties::isPseudoProperty(prop) + ) + or exists(DataFlow::FunctionNode fn | fn = pred and lbl = Label::return() @@ -947,7 +955,6 @@ module API { (base instanceof TNonModuleDef or base instanceof TUse) ) or - // invocations exists(DataFlow::SourceNode src, DataFlow::SourceNode pred | use(base, src) and pred = trackUseNode(src) | @@ -968,6 +975,13 @@ module API { or ref = cls.getAClassReference().getAnInstantiation() ) + or + exists(string prop | + PreCallGraphStep::loadStep(pred.getALocalUse(), ref, prop) and + lbl = Label::member(prop) and + // avoid generating member edges like "$arrayElement$" + not DataFlow::PseudoProperties::isPseudoProperty(prop) + ) ) or exists(DataFlow::Node def, DataFlow::FunctionNode fn | @@ -1535,7 +1549,9 @@ module API { prop = any(CanonicalName c).getName() or prop = any(DataFlow::PropRef p).getPropertyName() or exists(Impl::MkTypeUse(_, prop)) or - exists(any(Module m).getAnExportedValue(prop)) + exists(any(Module m).getAnExportedValue(prop)) or + PreCallGraphStep::loadStep(_, _, prop) or + PreCallGraphStep::storeStep(_, _, prop) } or MkLabelUnknownMember() or MkLabelParameter(int i) { diff --git a/javascript/ql/lib/semmle/javascript/ES2015Modules.qll b/javascript/ql/lib/semmle/javascript/ES2015Modules.qll index 1bdfec7ffe9d..cc84fb87324d 100644 --- a/javascript/ql/lib/semmle/javascript/ES2015Modules.qll +++ b/javascript/ql/lib/semmle/javascript/ES2015Modules.qll @@ -510,6 +510,9 @@ class ExportNamedDeclaration extends ExportDeclaration, @export_named_declaratio or exists(ReExportDeclaration red | red = this | result = red.getReExportedES2015Module().getAnExport().getSourceNode(spec.getLocalName()) + or + spec instanceof ExportNamespaceSpecifier and + result = DataFlow::valueNode(spec) ) ) } @@ -524,6 +527,19 @@ class ExportNamedDeclaration extends ExportDeclaration, @export_named_declaratio ExportSpecifier getASpecifier() { result = this.getSpecifier(_) } } +private import semmle.javascript.dataflow.internal.PreCallGraphStep + +private class ExportNamespaceStep extends PreCallGraphStep { + override predicate storeStep(DataFlow::Node pred, DataFlow::SourceNode succ, string prop) { + exists(ExportNamedDeclaration exprt, ExportNamespaceSpecifier spec | + spec = exprt.getASpecifier() and + pred = + exprt.(ReExportDeclaration).getReExportedES2015Module().getAnExport().getSourceNode(prop) and + succ = DataFlow::valueNode(spec) + ) + } +} + /** * An export declaration with the `type` modifier. */ diff --git a/javascript/ql/lib/semmle/javascript/dataflow/Configuration.qll b/javascript/ql/lib/semmle/javascript/dataflow/Configuration.qll index fd056277b331..50183c656b27 100644 --- a/javascript/ql/lib/semmle/javascript/dataflow/Configuration.qll +++ b/javascript/ql/lib/semmle/javascript/dataflow/Configuration.qll @@ -704,6 +704,10 @@ module SharedFlowStep { * For use with load/store steps in `DataFlow::SharedFlowStep` and TypeTracking. */ module PseudoProperties { + /** Holds if `s` is a pseudo-property. */ + bindingset[s] + predicate isPseudoProperty(string s) { s.matches("$%$") } + bindingset[s] private string pseudoProperty(string s) { result = "$" + s + "$" } diff --git a/javascript/ql/lib/semmle/javascript/dataflow/Sources.qll b/javascript/ql/lib/semmle/javascript/dataflow/Sources.qll index c273053210df..de103ef1c903 100644 --- a/javascript/ql/lib/semmle/javascript/dataflow/Sources.qll +++ b/javascript/ql/lib/semmle/javascript/dataflow/Sources.qll @@ -322,6 +322,7 @@ module SourceNode { astNode instanceof FunctionBindExpr or astNode instanceof DynamicImportExpr or astNode instanceof ImportSpecifier or + astNode instanceof ExportNamespaceSpecifier or astNode instanceof ImportMetaExpr or astNode instanceof TaggedTemplateExpr or astNode instanceof Templating::PipeRefExpr or diff --git a/javascript/ql/test/ApiGraphs/custom-use-steps/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/custom-use-steps/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/custom-use-steps/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/custom-use-steps/VerifyAssertions.ql new file mode 100644 index 000000000000..3c51eecf21f0 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/custom-use-steps/VerifyAssertions.ql @@ -0,0 +1,13 @@ +import ApiGraphs.VerifyAssertions +private import semmle.javascript.dataflow.internal.PreCallGraphStep + +class CustomUseStep extends PreCallGraphStep { + override predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) { + exists(DataFlow::CallNode call | + call.getCalleeName() = "customLoad" and + pred = call.getArgument(0) and + succ = call and + prop = call.getArgument(1).getStringValue() + ) + } +} diff --git a/javascript/ql/test/ApiGraphs/custom-use-steps/index.js b/javascript/ql/test/ApiGraphs/custom-use-steps/index.js new file mode 100644 index 000000000000..6302c9f2e8cf --- /dev/null +++ b/javascript/ql/test/ApiGraphs/custom-use-steps/index.js @@ -0,0 +1,4 @@ +const foo = require("foo"); + +foo.bar; // use=moduleImport("foo").getMember("exports").getMember("bar") +customLoad(foo, "baz") // use=moduleImport("foo").getMember("exports").getMember("baz") diff --git a/javascript/ql/test/ApiGraphs/custom-use-steps/package.json b/javascript/ql/test/ApiGraphs/custom-use-steps/package.json new file mode 100644 index 000000000000..71206e92e6bf --- /dev/null +++ b/javascript/ql/test/ApiGraphs/custom-use-steps/package.json @@ -0,0 +1,3 @@ +{ + "name": "custom-use-steps" +} diff --git a/javascript/ql/test/ApiGraphs/reexport/index.js b/javascript/ql/test/ApiGraphs/reexport/index.js index 2562008211b4..fced05f752ec 100644 --- a/javascript/ql/test/ApiGraphs/reexport/index.js +++ b/javascript/ql/test/ApiGraphs/reexport/index.js @@ -4,5 +4,6 @@ module.exports = { impl, util: require("./lib/utils"), other: require("./lib/stuff"), - util2: require("./lib/utils2") + util2: require("./lib/utils2"), + esmodule: require("./lib/esmodule-reexport"), }; diff --git a/javascript/ql/test/ApiGraphs/reexport/lib/esmodule-reexport.js b/javascript/ql/test/ApiGraphs/reexport/lib/esmodule-reexport.js new file mode 100644 index 000000000000..33f7f79552c2 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/reexport/lib/esmodule-reexport.js @@ -0,0 +1,2 @@ +export * from "./esmodule-reexported1"; +export * as lib2 from "./esmodule-reexported2"; diff --git a/javascript/ql/test/ApiGraphs/reexport/lib/esmodule-reexported1.js b/javascript/ql/test/ApiGraphs/reexport/lib/esmodule-reexported1.js new file mode 100644 index 000000000000..06907a8c73d3 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/reexport/lib/esmodule-reexported1.js @@ -0,0 +1 @@ +export function one() {} /* def=moduleImport("reexport").getMember("exports").getMember("esmodule").getMember("one") */ diff --git a/javascript/ql/test/ApiGraphs/reexport/lib/esmodule-reexported2.js b/javascript/ql/test/ApiGraphs/reexport/lib/esmodule-reexported2.js new file mode 100644 index 000000000000..515369973653 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/reexport/lib/esmodule-reexported2.js @@ -0,0 +1 @@ +export function two() {} /* def=moduleImport("reexport").getMember("exports").getMember("esmodule").getMember("lib2").getMember("two") */ diff --git a/javascript/ql/test/library-tests/Modules/tests.expected b/javascript/ql/test/library-tests/Modules/tests.expected index f0211d1b049b..a594fdcf1104 100644 --- a/javascript/ql/test/library-tests/Modules/tests.expected +++ b/javascript/ql/test/library-tests/Modules/tests.expected @@ -81,6 +81,7 @@ test_Module_exports | export-in-mjs.mjs:1:1:1:34 | | exported_from_mjs | export-in-mjs.mjs:1:32:1:33 | 42 | | f.ts:1:1:6:0 | | foo | f.ts:5:8:5:24 | function foo() {} | | m/c.js:1:1:6:0 | | h | b.js:5:10:5:10 | f | +| reExportNamespace.js:1:1:2:0 | | ns | reExportNamespace.js:1:8:1:14 | * as ns | | tst.html:4:23:8:0 | | y | tst.html:7:20:7:21 | 42 | test_NamedImportSpecifier | d.js:1:10:1:21 | default as g | @@ -149,4 +150,5 @@ test_getSourceNode | export-in-mjs.mjs:1:1:1:34 | export ... s = 42; | exported_from_mjs | export-in-mjs.mjs:1:32:1:33 | 42 | | f.ts:5:1:5:24 | export ... oo() {} | foo | f.ts:5:8:5:24 | function foo() {} | | m/c.js:5:1:5:30 | export ... '../b'; | h | b.js:5:10:5:10 | f | +| reExportNamespace.js:1:1:1:26 | export ... "./a"; | ns | reExportNamespace.js:1:8:1:14 | * as ns | | tst.html:7:3:7:22 | export const y = 42; | y | tst.html:7:20:7:21 | 42 |