Skip to content

Commit a45da05

Browse files
authored
Merge pull request #18623 from asgerf/js/nest-di
JS: Add support for dependency injection in Nest
2 parents f35fea3 + d23c198 commit a45da05

File tree

10 files changed

+140
-5
lines changed

10 files changed

+140
-5
lines changed

javascript/ql/lib/semmle/javascript/dataflow/Nodes.qll

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ private import javascript
88
private import semmle.javascript.dependencies.Dependencies
99
private import internal.CallGraphs
1010
private import semmle.javascript.internal.CachedStages
11+
private import semmle.javascript.dataflow.internal.PreCallGraphStep
1112

1213
/**
1314
* A data flow node corresponding to an expression.
@@ -995,6 +996,9 @@ class ClassNode extends DataFlow::SourceNode instanceof ClassNode::Range {
995996
result.getAstNode().getFile() = this.getAstNode().getFile()
996997
)
997998
or
999+
t.start() and
1000+
PreCallGraphStep::classObjectSource(this, result)
1001+
or
9981002
result = this.getAClassReferenceRec(t)
9991003
}
10001004

@@ -1044,6 +1048,9 @@ class ClassNode extends DataFlow::SourceNode instanceof ClassNode::Range {
10441048
// Note that this also blocks flows into a property of the receiver,
10451049
// but the `localFieldStep` rule will often compensate for this.
10461050
not result = any(DataFlow::ClassNode cls).getAReceiverNode()
1051+
or
1052+
t.start() and
1053+
PreCallGraphStep::classInstanceSource(this, result)
10471054
}
10481055

10491056
pragma[noinline]

javascript/ql/lib/semmle/javascript/dataflow/internal/PreCallGraphStep.qll

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ class PreCallGraphStep extends Unit {
4444
) {
4545
none()
4646
}
47+
48+
/**
49+
* Holds if `node` can hold an instance of `cls`.
50+
*/
51+
predicate classInstanceSource(DataFlow::ClassNode cls, DataFlow::Node node) { none() }
52+
53+
/**
54+
* Holds if `node` can hold an reference to the `cls` class itself.
55+
*/
56+
predicate classObjectSource(DataFlow::ClassNode cls, DataFlow::Node node) { none() }
4757
}
4858

4959
cached
@@ -90,6 +100,22 @@ module PreCallGraphStep {
90100
) {
91101
any(PreCallGraphStep s).loadStoreStep(pred, succ, loadProp, storeProp)
92102
}
103+
104+
/**
105+
* Holds if `node` can hold an instance of `cls`.
106+
*/
107+
cached
108+
predicate classInstanceSource(DataFlow::ClassNode cls, DataFlow::Node node) {
109+
any(PreCallGraphStep s).classInstanceSource(cls, node)
110+
}
111+
112+
/**
113+
* Holds if `node` can hold an reference to the `cls` class itself.
114+
*/
115+
cached
116+
predicate classObjectSource(DataFlow::ClassNode cls, DataFlow::Node node) {
117+
any(PreCallGraphStep s).classObjectSource(cls, node)
118+
}
93119
}
94120

95121
/**

javascript/ql/lib/semmle/javascript/frameworks/Nest.qll

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import javascript
66
private import semmle.javascript.security.dataflow.ServerSideUrlRedirectCustomizations
7+
private import semmle.javascript.dataflow.internal.PreCallGraphStep
78

89
/**
910
* Provides classes and predicates for reasoning about [Nest](https://nestjs.com/).
@@ -462,4 +463,54 @@ module NestJS {
462463
result.(DataFlow::FunctionNode).getAParameter() = this
463464
}
464465
}
466+
467+
/**
468+
* A value passed in the `providers` array in:
469+
* ```js
470+
* @Module({ providers: [ ... ] })
471+
* class App { ... }
472+
* ```
473+
*/
474+
private DataFlow::Node providerTuple() {
475+
result =
476+
DataFlow::moduleImport("@nestjs/common")
477+
.getAPropertyRead("Module")
478+
.getACall()
479+
.getOptionArgument(0, "providers")
480+
.getALocalSource()
481+
.(DataFlow::ArrayCreationNode)
482+
.getAnElement()
483+
}
484+
485+
private predicate providerPair(DataFlow::Node interface, DataFlow::Node concreteClass) {
486+
exists(DataFlow::SourceNode tuple |
487+
tuple = providerTuple().getALocalSource() and
488+
interface = tuple.getAPropertyWrite("provide").getRhs() and
489+
concreteClass = tuple.getAPropertyWrite("useClass").getRhs()
490+
)
491+
}
492+
493+
/** Gets the class being referenced at `node` without relying on the call graph. */
494+
private DataFlow::ClassNode getClassFromNode(DataFlow::Node node) {
495+
result.getAstNode() = node.analyze().getAValue().(AbstractClass).getClass()
496+
}
497+
498+
private predicate providerClassPair(
499+
DataFlow::ClassNode interface, DataFlow::ClassNode concreteClass
500+
) {
501+
exists(DataFlow::Node interfaceNode, DataFlow::Node concreteClassNode |
502+
providerPair(interfaceNode, concreteClassNode) and
503+
interface = getClassFromNode(interfaceNode) and
504+
concreteClass = getClassFromNode(concreteClassNode)
505+
)
506+
}
507+
508+
private class DependencyInjectionStep extends PreCallGraphStep {
509+
override predicate classInstanceSource(DataFlow::ClassNode cls, DataFlow::Node node) {
510+
exists(DataFlow::ClassNode interfaceClass |
511+
node.asExpr().(Parameter).getType().(ClassType).getClass() = interfaceClass.getAstNode() and
512+
providerClassPair(interfaceClass, cls)
513+
)
514+
}
515+
}
465516
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: majorAnalysis
3+
---
4+
* Improved support for NestJS applications that make use of dependency injection with custom providers.
5+
Calls to methods on an injected service should now be resolved properly.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
import { Controller } from './validation';
3+
import { Foo } from './foo.interface';
4+
import { FooImpl } from './foo.impl';
5+
6+
@Module({
7+
controllers: [Controller],
8+
providers: [{
9+
provide: Foo, useClass: FooImpl
10+
}],
11+
})
12+
export class AppModule { }
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Foo } from "./foo.interface";
2+
3+
export class FooImpl extends Foo {
4+
fooMethod(x: string) {
5+
sink(x); // $ hasValueFlow=x
6+
}
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export abstract class Foo {
2+
abstract fooMethod(x: string): void;
3+
}

javascript/ql/test/library-tests/frameworks/Nest/global/validation.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
import { Get, Query } from '@nestjs/common';
22
import { IsIn } from 'class-validator';
3+
import { Foo } from './foo.interface';
34

45
export class Controller {
6+
constructor(
7+
private readonly foo: Foo
8+
) { }
9+
510
@Get()
611
route1(@Query('x') validatedObj: Struct, @Query('y') unvalidated: string) {
712
if (Math.random()) return unvalidated; // NOT OK
813
return validatedObj.key; // OK
914
}
15+
16+
@Get()
17+
route2(@Query('x') x: string) {
18+
this.foo.fooMethod(x);
19+
}
1020
}
1121

1222
class Struct {

javascript/ql/test/library-tests/frameworks/Nest/test.expected

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
testFailures
12
routeHandler
2-
| global/validation.ts:6:3:9:3 | route1( ... OK\\n } |
3+
| global/validation.ts:11:3:14:3 | route1( ... OK\\n } |
4+
| global/validation.ts:17:3:19:3 | route2( ... x);\\n } |
35
| local/customDecorator.ts:18:3:20:3 | sneaky( ... OK\\n } |
46
| local/customDecorator.ts:23:3:25:3 | safe(@S ... OK\\n } |
57
| local/customPipe.ts:20:5:22:5 | sanitiz ... K\\n } |
@@ -36,8 +38,9 @@ requestInputAccess
3638
| body | local/routes.ts:40:16:40:19 | body |
3739
| body | local/routes.ts:66:26:66:29 | file |
3840
| body | local/routes.ts:71:31:71:35 | files |
39-
| parameter | global/validation.ts:6:22:6:33 | validatedObj |
40-
| parameter | global/validation.ts:6:56:6:66 | unvalidated |
41+
| parameter | global/validation.ts:11:22:11:33 | validatedObj |
42+
| parameter | global/validation.ts:11:56:11:66 | unvalidated |
43+
| parameter | global/validation.ts:17:22:17:22 | x |
4144
| parameter | local/customDecorator.ts:6:12:6:41 | request ... ryParam |
4245
| parameter | local/customPipe.ts:5:15:5:19 | value |
4346
| parameter | local/customPipe.ts:13:15:13:19 | value |
@@ -58,8 +61,8 @@ requestInputAccess
5861
| parameter | local/validation.ts:42:22:42:33 | validatedObj |
5962
| parameter | local/validation.ts:42:56:42:66 | unvalidated |
6063
responseSendArgument
61-
| global/validation.ts:7:31:7:41 | unvalidated |
62-
| global/validation.ts:8:12:8:27 | validatedObj.key |
64+
| global/validation.ts:12:31:12:41 | unvalidated |
65+
| global/validation.ts:13:12:13:27 | validatedObj.key |
6366
| local/customDecorator.ts:19:12:19:16 | value |
6467
| local/customDecorator.ts:24:12:24:16 | value |
6568
| local/customPipe.ts:21:16:21:29 | '' + sanitized |

javascript/ql/test/library-tests/frameworks/Nest/test.ql

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import javascript
22
private import semmle.javascript.security.dataflow.ServerSideUrlRedirectCustomizations
3+
private import utils.test.InlineFlowTest
34

45
query Http::RouteHandler routeHandler() { any() }
56

@@ -17,3 +18,13 @@ query RemoteFlowSource requestInputAccess(string kind) {
1718
query Http::ResponseSendArgument responseSendArgument() { any() }
1819

1920
query ServerSideUrlRedirect::Sink redirectSink() { any() }
21+
22+
module TestConfig implements DataFlow::ConfigSig {
23+
predicate isSource(DataFlow::Node node) { node instanceof RemoteFlowSource }
24+
25+
predicate isSink(DataFlow::Node node) {
26+
exists(DataFlow::CallNode call | call.getCalleeName() = "sink" and node = call.getArgument(0))
27+
}
28+
}
29+
30+
import ValueFlowTest<TestConfig>

0 commit comments

Comments
 (0)