Skip to content

Commit acb30e7

Browse files
committed
JS: More precise handling of default import fallback
1 parent d07e69e commit acb30e7

File tree

5 files changed

+68
-12
lines changed

5 files changed

+68
-12
lines changed

javascript/ql/src/semmle/javascript/ES2015Modules.qll

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,40 @@ class ES2015Module extends Module {
4040
}
4141
}
4242

43+
/**
44+
* Holds if `mod` contains one or more named export declarations other than `default`.
45+
*/
46+
private predicate hasNamedExports(ES2015Module mod) {
47+
mod.getAnExport().(ExportNamedDeclaration).getASpecifier().getExportedName() != "default"
48+
or
49+
exists(mod.getAnExport().(ExportNamedDeclaration).getAnExportedDecl())
50+
or
51+
// Bulk re-exports only export named bindings (not "default")
52+
mod.getAnExport() instanceof BulkReExportDeclaration
53+
}
54+
55+
/**
56+
* Holds if this module contains a `default` export.
57+
*/
58+
private predicate hasDefaultExport(ES2015Module mod) {
59+
// export default foo;
60+
mod.getAnExport() instanceof ExportDefaultDeclaration
61+
or
62+
// export { foo as default };
63+
mod.getAnExport().(ExportNamedDeclaration).getASpecifier().getExportedName() = "default"
64+
}
65+
66+
/**
67+
* Holds if `mod` contains both named and `default` exports.
68+
*
69+
* This is used to determine whether a default-import of the module should be reinterpreted
70+
* as a namespace-import, to accomodate the non-standard behavior implemented by some compilers.
71+
*/
72+
private predicate hasBothNamedAndDefaultExports(ES2015Module mod) {
73+
hasNamedExports(mod) and
74+
hasDefaultExport(mod)
75+
}
76+
4377
/**
4478
* An import declaration.
4579
*
@@ -70,6 +104,10 @@ class ImportDeclaration extends Stmt, Import, @import_declaration {
70104
is instanceof ImportNamespaceSpecifier and
71105
count(getASpecifier()) = 1
72106
or
107+
// For compatibility with the non-standard implementation of default imports,
108+
// treat default imports as namespace imports in cases where it can't cause ambiguity
109+
// between named exports and the properties of a default-exported object.
110+
not hasBothNamedAndDefaultExports(getImportedModule()) and
73111
is.getImportedName() = "default"
74112
)
75113
or

javascript/ql/src/semmle/javascript/NodeJS.qll

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,16 @@ private predicate isRequire(DataFlow::Node nd) {
205205
or
206206
isRequire(nd.getAPredecessor())
207207
or
208-
// `import { createRequire } from 'module';` support.
209-
// specialized to ES2015 modules to avoid recursion in the `DataFlow::moduleImport()` predicate.
210-
exists(ImportDeclaration imp | imp.getImportedPath().getValue() = "module" |
211-
nd =
212-
imp
213-
.getImportedModuleNode()
214-
.(DataFlow::SourceNode)
215-
.getAPropertyRead("createRequire")
216-
.getACall()
208+
// `import { createRequire } from 'module';`.
209+
// specialized to ES2015 modules to avoid recursion in the `DataFlow::moduleImport()` predicate and to avoid
210+
// negative recursion between `Import.getImportedModuleNode()` and `Import.getImportedModule()`.
211+
exists(ImportDeclaration imp, DataFlow::SourceNode baseObj |
212+
imp.getImportedPath().getValue() = "module"
213+
|
214+
baseObj =
215+
[DataFlow::destructuredModuleImportNode(imp),
216+
DataFlow::valueNode(imp.getASpecifier().(ImportNamespaceSpecifier))] and
217+
nd = baseObj.getAPropertyRead("createRequire").getACall()
217218
)
218219
}
219220

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
let source = 'tainted';
2+
3+
export const x = source;
4+
5+
export default {
6+
x: 'safe'
7+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import defaultValue from './mixedExports';
2+
import { x } from './mixedExports';
3+
import * as ns from './mixedExports';
4+
5+
let sink1 = defaultValue.x; // OK
6+
let sink2 = x; // NOT OK
7+
let sink3 = ns.x; // NOT OK

javascript/ql/test/library-tests/InterProceduralFlow/tests.expected

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
dataFlow
22
| a.js:1:15:1:23 | "tainted" | b.js:4:13:4:40 | whoKnow ... Tainted |
3-
| a.js:1:15:1:23 | "tainted" | b.js:6:13:6:13 | x |
43
| a.js:2:15:2:28 | "also tainted" | b.js:5:13:5:29 | notTaintedTrustMe |
54
| async.js:2:16:2:23 | "source" | async.js:8:15:8:27 | await async() |
65
| async.js:2:16:2:23 | "source" | async.js:13:15:13:20 | sync() |
@@ -26,6 +25,8 @@ dataFlow
2625
| global.js:2:15:2:24 | "tainted2" | global.js:10:13:10:22 | g(source2) |
2726
| global.js:5:22:5:35 | "also tainted" | global.js:9:13:9:22 | g(source1) |
2827
| global.js:5:22:5:35 | "also tainted" | global.js:10:13:10:22 | g(source2) |
28+
| mixedExports.js:1:14:1:22 | 'tainted' | mixedExportsClient.js:6:13:6:13 | x |
29+
| mixedExports.js:1:14:1:22 | 'tainted' | mixedExportsClient.js:7:13:7:16 | ns.x |
2930
| nodeJsLib.js:2:15:2:23 | "tainted" | esClient.js:7:13:7:18 | nj.foo |
3031
| nodeJsLib.js:2:15:2:23 | "tainted" | esClient.js:10:13:10:17 | njFoo |
3132
| nodeJsLib.js:2:15:2:23 | "tainted" | nodeJsClient.js:4:13:4:18 | nj.foo |
@@ -77,7 +78,6 @@ flowLabels
7778
| tst5.mjs:15:8:15:19 | source(flow) | tst5.mjs:16:13:16:16 | flow |
7879
taintTracking
7980
| a.js:1:15:1:23 | "tainted" | b.js:4:13:4:40 | whoKnow ... Tainted |
80-
| a.js:1:15:1:23 | "tainted" | b.js:6:13:6:13 | x |
8181
| a.js:2:15:2:28 | "also tainted" | b.js:5:13:5:29 | notTaintedTrustMe |
8282
| async.js:2:16:2:23 | "source" | async.js:7:14:7:20 | async() |
8383
| async.js:2:16:2:23 | "source" | async.js:8:15:8:27 | await async() |
@@ -106,6 +106,8 @@ taintTracking
106106
| global.js:2:15:2:24 | "tainted2" | global.js:10:13:10:22 | g(source2) |
107107
| global.js:5:22:5:35 | "also tainted" | global.js:9:13:9:22 | g(source1) |
108108
| global.js:5:22:5:35 | "also tainted" | global.js:10:13:10:22 | g(source2) |
109+
| mixedExports.js:1:14:1:22 | 'tainted' | mixedExportsClient.js:6:13:6:13 | x |
110+
| mixedExports.js:1:14:1:22 | 'tainted' | mixedExportsClient.js:7:13:7:16 | ns.x |
109111
| nodeJsLib.js:1:15:1:23 | "tainted" | esClient.js:7:13:7:18 | nj.foo |
110112
| nodeJsLib.js:1:15:1:23 | "tainted" | esClient.js:10:13:10:17 | njFoo |
111113
| nodeJsLib.js:1:15:1:23 | "tainted" | nodeJsClient.js:4:13:4:18 | nj.foo |
@@ -182,7 +184,6 @@ taintTracking
182184
| underscore.js:19:17:19:22 | "src5" | underscore.js:20:15:20:44 | _.map([ ... ource5) |
183185
germanFlow
184186
| a.js:1:15:1:23 | "tainted" | b.js:4:13:4:40 | whoKnow ... Tainted |
185-
| a.js:1:15:1:23 | "tainted" | b.js:6:13:6:13 | x |
186187
| a.js:2:15:2:28 | "also tainted" | b.js:5:13:5:29 | notTaintedTrustMe |
187188
| async.js:2:16:2:23 | "source" | async.js:8:15:8:27 | await async() |
188189
| async.js:2:16:2:23 | "source" | async.js:13:15:13:20 | sync() |
@@ -209,6 +210,8 @@ germanFlow
209210
| global.js:2:15:2:24 | "tainted2" | global.js:10:13:10:22 | g(source2) |
210211
| global.js:5:22:5:35 | "also tainted" | global.js:9:13:9:22 | g(source1) |
211212
| global.js:5:22:5:35 | "also tainted" | global.js:10:13:10:22 | g(source2) |
213+
| mixedExports.js:1:14:1:22 | 'tainted' | mixedExportsClient.js:6:13:6:13 | x |
214+
| mixedExports.js:1:14:1:22 | 'tainted' | mixedExportsClient.js:7:13:7:16 | ns.x |
212215
| nodeJsLib.js:2:15:2:23 | "tainted" | esClient.js:7:13:7:18 | nj.foo |
213216
| nodeJsLib.js:2:15:2:23 | "tainted" | esClient.js:10:13:10:17 | njFoo |
214217
| nodeJsLib.js:2:15:2:23 | "tainted" | nodeJsClient.js:4:13:4:18 | nj.foo |

0 commit comments

Comments
 (0)