Skip to content

Commit c324b2a

Browse files
committed
JS: Refactor
1 parent eb7d024 commit c324b2a

File tree

2 files changed

+116
-84
lines changed

2 files changed

+116
-84
lines changed

javascript/ql/lib/semmle/javascript/endpoints/EndpointNaming.qll

Lines changed: 113 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,91 @@ private string join(string x, string y) {
3838

3939
private predicate isPackageExport(API::Node node) { node = API::moduleExport(_) }
4040

41+
/**
42+
* A version of `getInstance()` only from sink nodes to the special `ClassInstance` node.
43+
*
44+
* This ensures we see instance methods, but not side effects on `this` or on instantiations of the class.
45+
*/
46+
private predicate instanceEdge(API::Node pred, API::Node succ) {
47+
exists(DataFlow::ClassNode cls |
48+
pred.getAValueReachingSink() = cls and
49+
succ = API::Internal::getClassInstance(cls)
50+
)
51+
}
52+
53+
/** Holds if `pred -> succ` is an edge we can use for naming. */
4154
private predicate relevantEdge(API::Node pred, API::Node succ) {
4255
succ = pred.getMember(_) and
4356
not isPrivateLike(succ)
4457
or
45-
succ = pred.getInstance()
58+
instanceEdge(pred, succ)
59+
}
60+
61+
private signature predicate isRootNodeSig(API::Node node);
62+
63+
private signature predicate edgeSig(API::Node pred, API::Node succ);
64+
65+
/** Builds `shortestDistances` using the API graph root node as the only origin node, to ensure unique results. */
66+
private module ApiGraphDistance<isRootNodeSig/1 isRootNode, edgeSig/2 edges> {
67+
private predicate edgesWithEntry(API::Node pred, API::Node succ) {
68+
edges(pred, succ)
69+
or
70+
pred = API::root() and
71+
isRootNode(succ)
72+
}
73+
74+
int distanceTo(API::Node node) = shortestDistances(API::root/0, edgesWithEntry/2)(_, node, result)
75+
}
76+
77+
/** Gets the shortest distance from a package export to `nd` in the API graph. */
78+
private predicate distanceFromPackageExport =
79+
ApiGraphDistance<isPackageExport/1, relevantEdge/2>::distanceTo/1;
80+
81+
/**
82+
* Holds if `(package, name)` is the fallback name for `cls`, to be used as a last resort
83+
* in order to name its instance methods.
84+
*
85+
* This happens when the class is not accessible via an access path, but instances of the
86+
* class can still escape via more complex access patterns, for example:
87+
*
88+
* class InternalClass {}
89+
* function foo() {
90+
* return new InternalClass();
91+
* }
92+
*/
93+
private predicate classHasFallbackName(
94+
DataFlow::ClassNode cls, string package, string name, int badness
95+
) {
96+
hasEscapingInstance(cls) and
97+
not exists(distanceFromPackageExport(any(API::Node node | node.getAValueReachingSink() = cls))) and
98+
exists(string baseName |
99+
InternalModuleNaming::fallbackModuleName(cls.getTopLevel(), package, baseName, badness - 100) and
100+
name = join(baseName, cls.getName())
101+
)
102+
}
103+
104+
/** Holds if `node` describes instances of a class that has a fallback name. */
105+
private predicate isClassInstanceWithFallbackName(API::Node node) {
106+
exists(DataFlow::ClassNode cls |
107+
classHasFallbackName(cls, _, _, _) and
108+
node = API::Internal::getClassInstance(cls)
109+
)
46110
}
47111

48-
/** Gets the shortest distance from a packaeg export to `nd` in the API graph. */
49-
private int distanceFromPackageExport(API::Node nd) =
50-
shortestDistances(isPackageExport/1, relevantEdge/2)(_, nd, result)
112+
/** Gets the shortest distance from a node with a fallback name, to `nd` in the API graph. */
113+
private predicate distanceFromFallbackName =
114+
ApiGraphDistance<isClassInstanceWithFallbackName/1, relevantEdge/2>::distanceTo/1;
115+
116+
/** Gets the shortest distance from a name-root (package export or fallback name) to `nd` */
117+
private int distanceFromRoot(API::Node nd) {
118+
result = distanceFromPackageExport(nd)
119+
or
120+
not exists(distanceFromPackageExport(nd)) and
121+
result = 100 + distanceFromFallbackName(nd)
122+
}
51123

52-
private predicate isExported(API::Node node) { exists(distanceFromPackageExport(node)) }
124+
/** Holds if `nd` can be given a name. */
125+
private predicate isRelevant(API::Node node) { exists(distanceFromRoot(node)) }
53126

54127
/**
55128
* Holds if `node` is a default export that can be reinterpreted as a namespace export,
@@ -76,26 +149,27 @@ private predicate isPrivateAssignment(DataFlow::Node node) {
76149

77150
private predicate isPrivateLike(API::Node node) { isPrivateAssignment(node.asSink()) }
78151

152+
bindingset[name]
153+
private int getNameBadness(string name) {
154+
if name = ["constructor", "default"] then result = 10 else result = 0
155+
}
156+
79157
private API::Node getASuccessor(API::Node node, string name, int badness) {
80-
isExported(node) and
81-
isExported(result) and
158+
isRelevant(node) and
159+
isRelevant(result) and
82160
(
83161
exists(string member |
84162
result = node.getMember(member) and
85-
if member = "default"
86-
then
87-
if defaultExportCanBeInterpretedAsNamespaceExport(node)
88-
then (
89-
badness = 5 and name = ""
90-
) else (
91-
badness = 10 and name = "default"
92-
)
93-
else (
94-
name = member and badness = 0
163+
if member = "default" and defaultExportCanBeInterpretedAsNamespaceExport(node)
164+
then (
165+
badness = 5 and name = ""
166+
) else (
167+
name = member and
168+
badness = getNameBadness(name)
95169
)
96170
)
97171
or
98-
result = node.getInstance() and
172+
instanceEdge(node, result) and
99173
name = "prototype" and
100174
badness = 0
101175
)
@@ -118,15 +192,17 @@ private API::Node getPreferredPredecessor(API::Node node, string name, int badne
118192
min(API::Node pred, int b |
119193
pred = getAPredecessor(node, _, b) and
120194
// ensure the preferred predecessor is strictly closer to a root export, even if it means accepting more badness
121-
distanceFromPackageExport(pred) < distanceFromPackageExport(node)
195+
distanceFromRoot(pred) < distanceFromRoot(node)
122196
|
123197
b
124198
) and
125199
result =
126200
min(API::Node pred, string name1 |
127-
pred = getAPredecessor(node, name1, badness)
201+
pred = getAPredecessor(node, name1, badness) and
202+
// ensure the preferred predecessor is strictly closer to a root export, even if it means accepting more badness
203+
distanceFromRoot(pred) < distanceFromRoot(node)
128204
|
129-
pred order by distanceFromPackageExport(pred), name1
205+
pred order by distanceFromRoot(pred), name1
130206
) and
131207
name = min(string n | result = getAPredecessor(node, n, badness) | n)
132208
}
@@ -141,6 +217,12 @@ private predicate sinkHasNameCandidate(API::Node sink, string package, string na
141217
name = "" and
142218
badness = 0
143219
or
220+
exists(DataFlow::ClassNode cls, string className |
221+
sink = API::Internal::getClassInstance(cls) and
222+
classHasFallbackName(cls, package, className, badness) and
223+
name = join(className, "prototype")
224+
)
225+
or
144226
exists(API::Node baseNode, string baseName, int baseBadness, string step, int stepBadness |
145227
sinkHasNameCandidate(baseNode, package, baseName, baseBadness) and
146228
baseNode = getPreferredPredecessor(sink, step, stepBadness) and
@@ -192,51 +274,7 @@ private API::Node getASinkNode(DataFlow::SourceNode node) { node = nodeReachingS
192274
private predicate nameFromGlobal(DataFlow::Node node, string package, string name, int badness) {
193275
package = "global" and
194276
node = AccessPath::getAnAssignmentTo(name) and
195-
badness = -10
196-
}
197-
198-
bindingset[qualifiedName]
199-
private int getBadnessOfClassName(string qualifiedName) {
200-
if qualifiedName.matches("%.constructor")
201-
then result = 10
202-
else
203-
if qualifiedName = ""
204-
then result = 5
205-
else result = 0
206-
}
207-
208-
/** Holds if `(package, name)` is a potential name for `cls`, with the given `badness`. */
209-
private predicate classObjectHasNameCandidate(
210-
DataFlow::ClassNode cls, string package, string name, int badness
211-
) {
212-
// There can be multiple API nodes associated with `cls`.
213-
// For example:
214-
///
215-
// class C {}
216-
// module.exports.A = C; // first sink
217-
// module.exports.B = C; // second sink
218-
//
219-
exists(int baseBadness |
220-
sinkHasPrimaryName(getASinkNode(cls), package, name, baseBadness) and
221-
badness = baseBadness + getBadnessOfClassName(name)
222-
)
223-
or
224-
nameFromGlobal(cls, package, name, badness)
225-
or
226-
// If the class is not accessible via an access path, but instances of the
227-
// class can still escape via more complex access patterns, resort to a synthesized name.
228-
// For example:
229-
//
230-
// class InternalClass {}
231-
// function foo() {
232-
// return new InternalClass();
233-
// }
234-
//
235-
hasEscapingInstance(cls) and
236-
exists(string baseName |
237-
InternalModuleNaming::fallbackModuleName(cls.getTopLevel(), package, baseName, badness - 100) and
238-
name = join(baseName, cls.getName())
239-
)
277+
(if node.getTopLevel().isExterns() then badness = -10 else badness = 10)
240278
}
241279

242280
/** Holds if an instance of `cls` can be exposed to client code. */
@@ -250,8 +288,6 @@ private predicate sourceNodeHasNameCandidate(
250288
sinkHasPrimaryName(getASinkNode(node), package, name, badness)
251289
or
252290
nameFromGlobal(node, package, name, badness)
253-
or
254-
classObjectHasNameCandidate(node, package, name, badness)
255291
}
256292

257293
private predicate sourceNodeHasPrimaryName(
@@ -273,6 +309,11 @@ private DataFlow::SourceNode functionValue(DataFlow::TypeTracker t) {
273309
result instanceof DataFlow::ClassNode
274310
or
275311
result instanceof DataFlow::PartialInvokeNode
312+
or
313+
result = DataFlow::globalVarRef(["Function", "eval"]).getAnInvocation()
314+
or
315+
// Assume double-invocation of Function also returns a function
316+
result = DataFlow::globalVarRef("Function").getAnInvocation().getAnInvocation()
276317
)
277318
or
278319
exists(DataFlow::TypeTracker t2 | result = functionValue(t2).track(t2, t))
@@ -299,7 +340,6 @@ private predicate isFunctionSource(DataFlow::SourceNode node) {
299340
or
300341
node = functionValue() and
301342
node instanceof DataFlow::InvokeNode and
302-
exists(node.getABoundFunctionValue(_)) and
303343
// `getASinkNode` steps through imports (but not other calls) so exclude calls that are imports (i.e. require calls)
304344
// as we want to get as close to the source as possible.
305345
not node instanceof DataFlow::ModuleImportNode
@@ -323,25 +363,17 @@ private predicate sinkHasSourceName(API::Node sink, string package, string name,
323363
)
324364
}
325365

326-
private predicate sinkHasPrimarySourceName(API::Node sink, string package, string name, int badness) {
327-
badness = min(int b | sinkHasSourceName(sink, _, _, b) | b) and
328-
package = min(string p | sinkHasSourceName(sink, p, _, badness) | p order by p.length(), p) and
329-
name = min(string n | sinkHasSourceName(sink, package, n, badness) | n order by n.length(), n)
330-
}
331-
332366
private predicate sinkHasPrimarySourceName(API::Node sink, string package, string name) {
333-
sinkHasPrimarySourceName(sink, package, name, _)
367+
strictcount(string p, string n | sinkHasSourceName(sink, p, n, _)) = 1 and
368+
sinkHasSourceName(sink, package, name, _)
334369
}
335370

336371
private predicate aliasCandidate(
337372
string package, string name, string targetPackage, string targetName, API::Node aliasDef
338373
) {
339374
sinkHasPrimaryName(aliasDef, package, name) and
340375
sinkHasPrimarySourceName(aliasDef, targetPackage, targetName) and
341-
not (
342-
package = targetPackage and
343-
name = targetName
344-
)
376+
not sinkHasSourceName(_, package, name, _) // (package, name) cannot be an alias if a source has it as its primary name
345377
}
346378

347379
private predicate nonAlias(string package, string name) {
@@ -354,7 +386,7 @@ private predicate nonAlias(string package, string name) {
354386
exists(API::Node sink, string targetPackage, string targetName |
355387
aliasCandidate(package, name, targetPackage, targetName, _) and
356388
sinkHasPrimaryName(sink, package, name) and
357-
not sinkHasPrimarySourceName(sink, targetPackage, targetName, _)
389+
not sinkHasPrimarySourceName(sink, targetPackage, targetName)
358390
)
359391
}
360392

@@ -424,8 +456,6 @@ private module InternalModuleNaming {
424456

425457
/** Holds if `(package, name)` should be used to refer to code inside `mod`. */
426458
predicate fallbackModuleName(Module mod, string package, string name, int badness) {
427-
sinkHasPrimaryName(getASinkNode(mod.getDefaultOrBulkExport()), package, name, badness)
428-
or
429459
badness = 50 and
430460
package = getPackageRelativePath(mod) and
431461
name = ""

javascript/ql/test/library-tests/EndpointNaming/pack1/main.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ class PrivateClass {}
44

55
export const ExportedConst = class ExportedConstClass {} // $ name=(pack1).ExportedConst
66

7-
class ClassWithEscapingInstance {}
7+
class ClassWithEscapingInstance {
8+
m() {} // $ name=(pack1).ClassWithEscapingInstance.prototype.m
9+
}
810

911
export function getEscapingInstance() {
1012
return new ClassWithEscapingInstance();

0 commit comments

Comments
 (0)