Skip to content

Commit 1b7977b

Browse files
authored
Merge pull request #18466 from asgerf/js/view-component-inputs
JS: Add view-component-input threat model
2 parents 60f9160 + 051fa66 commit 1b7977b

34 files changed

+407
-66
lines changed

docs/codeql/reusables/threat-model-description.rst

+1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ The less commonly used categories are:
1515
- ``database-access-result`` which represents a database access. Currently only used by JavaScript.
1616
- ``file-write`` which represents opening a file in write mode. Currently only used in C#.
1717
- ``reverse-dns`` which represents reverse DNS lookups. Currently only used in Java.
18+
- ``view-component-input`` which represents inputs to a React, Vue, or Angular component (also known as "props"). Currently only used by JavaScript/TypeScript.
1819

1920
When running a CodeQL analysis, the ``remote`` threat model is included by default. You can optionally include other threat models as appropriate when using the CodeQL CLI and in GitHub code scanning. For more information, see `Analyzing your code with CodeQL queries <https://docs.github.com/code-security/codeql-cli/getting-started-with-the-codeql-cli/analyzing-your-code-with-codeql-queries#including-model-packs-to-add-potential-sources-of-tainted-data>`__ and `Customizing your advanced setup for code scanning <https://docs.github.com/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning#extending-codeql-coverage-with-threat-models>`__.

javascript/ql/lib/semmle/javascript/Concepts.qll

+10
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ class ThreatModelSource extends DataFlow::Node instanceof ThreatModelSource::Ran
2626

2727
/** Gets a string that describes the type of this threat-model source. */
2828
string getSourceType() { result = super.getSourceType() }
29+
30+
/**
31+
* Holds if this is a source of data that is specific to the web browser environment.
32+
*/
33+
predicate isClientSideSource() { super.isClientSideSource() }
2934
}
3035

3136
/** Provides a class for modeling new sources for specific threat-models. */
@@ -48,6 +53,11 @@ module ThreatModelSource {
4853

4954
/** Gets a string that describes the type of this threat-model source. */
5055
abstract string getSourceType();
56+
57+
/**
58+
* Holds if this is a source of data that is specific to the web browser environment.
59+
*/
60+
predicate isClientSideSource() { this.getThreatModel() = "view-component-input" }
5161
}
5262
}
5363

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Provides a classes and predicates for contributing to the `view-component-input` threat model.
3+
*/
4+
5+
private import javascript
6+
7+
/**
8+
* An input to a view component, such as React props.
9+
*/
10+
abstract class ViewComponentInput extends DataFlow::Node {
11+
/** Gets a string that describes the type of this threat-model source. */
12+
abstract string getSourceType();
13+
}
14+
15+
private class ViewComponentInputAsThreatModelSource extends ThreatModelSource::Range instanceof ViewComponentInput
16+
{
17+
ViewComponentInputAsThreatModelSource() { not isSafeType(this.asExpr().getType()) }
18+
19+
final override string getThreatModel() { result = "view-component-input" }
20+
21+
final override string getSourceType() { result = ViewComponentInput.super.getSourceType() }
22+
}
23+
24+
private predicate isSafeType(Type t) {
25+
t instanceof NumberLikeType
26+
or
27+
t instanceof BooleanLikeType
28+
or
29+
t instanceof UndefinedType
30+
or
31+
t instanceof NullType
32+
or
33+
t instanceof VoidType
34+
or
35+
hasSafeTypes(t, t.(UnionType).getNumElementType())
36+
or
37+
isSafeType(t.(IntersectionType).getAnElementType())
38+
}
39+
40+
/** Hold if the first `n` components of `t` are safe types. */
41+
private predicate hasSafeTypes(UnionType t, int n) {
42+
isSafeType(t.getElementType(0)) and
43+
n = 1
44+
or
45+
isSafeType(t.getElementType(n - 1)) and
46+
hasSafeTypes(t, n - 1)
47+
}

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

+14
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ private import semmle.javascript.security.dataflow.CodeInjectionCustomizations
88
private import semmle.javascript.security.dataflow.ClientSideUrlRedirectCustomizations
99
private import semmle.javascript.DynamicPropertyAccess
1010
private import semmle.javascript.dataflow.internal.PreCallGraphStep
11+
private import semmle.javascript.ViewComponentInput
1112

1213
/**
1314
* Provides classes for working with Angular (also known as Angular 2.x) applications.
@@ -575,4 +576,17 @@ module Angular2 {
575576
)
576577
}
577578
}
579+
580+
private class InputFieldAsViewComponentInput extends ViewComponentInput {
581+
InputFieldAsViewComponentInput() {
582+
this =
583+
API::moduleImport("@angular/core")
584+
.getMember("Input")
585+
.getReturn()
586+
.getADecoratedMember()
587+
.asSource()
588+
}
589+
590+
override string getSourceType() { result = "Angular component input field" }
591+
}
578592
}

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

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import javascript
66
private import semmle.javascript.dataflow.internal.FlowSteps as FlowSteps
77
private import semmle.javascript.dataflow.internal.PreCallGraphStep
8+
private import semmle.javascript.ViewComponentInput
89

910
/**
1011
* Gets a reference to the 'React' object.
@@ -868,3 +869,9 @@ private class PropsFlowStep extends PreCallGraphStep {
868869
)
869870
}
870871
}
872+
873+
private class ReactPropAsViewComponentInput extends ViewComponentInput {
874+
ReactPropAsViewComponentInput() { this = any(ReactComponent c).getADirectPropsAccess() }
875+
876+
override string getSourceType() { result = "React props" }
877+
}

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

+81-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import javascript
6+
import semmle.javascript.ViewComponentInput
67

78
module Vue {
89
/** The global variable `Vue`, as an API graph entry point. */
@@ -85,17 +86,16 @@ module Vue {
8586
* A class with a `@Component` decorator, making it usable as an "options" object in Vue.
8687
*/
8788
class ClassComponent extends DataFlow::ClassNode {
89+
private ClassDefinition cls;
8890
DataFlow::Node decorator;
8991

9092
ClassComponent() {
91-
exists(ClassDefinition cls |
92-
this = cls.flow() and
93-
cls.getADecorator().getExpression() = decorator.asExpr() and
94-
(
95-
componentDecorator().flowsTo(decorator)
96-
or
97-
componentDecorator().getACall() = decorator
98-
)
93+
this = cls.flow() and
94+
cls.getADecorator().getExpression() = decorator.asExpr() and
95+
(
96+
componentDecorator().flowsTo(decorator)
97+
or
98+
componentDecorator().getACall() = decorator
9999
)
100100
}
101101

@@ -105,6 +105,9 @@ module Vue {
105105
* These options correspond to the options one would pass to `new Vue({...})` or similar.
106106
*/
107107
API::Node getDecoratorOptions() { result = decorator.(API::CallNode).getParameter(0) }
108+
109+
/** Gets the AST node for the class definition. */
110+
ClassDefinition getClassDefinition() { result = cls }
108111
}
109112

110113
private string memberKindVerb(DataFlow::MemberKind kind) {
@@ -460,6 +463,12 @@ module Vue {
460463

461464
SingleFileComponent() { this = MkSingleFileComponent(file) }
462465

466+
/** Gets a call to `defineProps` in this component. */
467+
DataFlow::CallNode getDefinePropsCall() {
468+
result = DataFlow::globalVarRef("defineProps").getACall() and
469+
result.getFile() = file
470+
}
471+
463472
override Template::Element getTemplateElement() {
464473
exists(HTML::Element e | result.(Template::HtmlElement).getElement() = e |
465474
e.getFile() = file and
@@ -697,4 +706,68 @@ module Vue {
697706

698707
override ClientSideRemoteFlowKind getKind() { result = kind }
699708
}
709+
710+
/**
711+
* Holds if the given type annotation indicates a value that is not typically considered taintable.
712+
*/
713+
private predicate isSafeType(TypeAnnotation type) {
714+
type.isBooleany() or
715+
type.isNumbery() or
716+
type.isRawFunction() or
717+
type instanceof FunctionTypeExpr
718+
}
719+
720+
/**
721+
* Holds if the given field has a type that indicates that is can not contain a taintable value.
722+
*/
723+
private predicate isSafeField(FieldDeclaration field) { isSafeType(field.getTypeAnnotation()) }
724+
725+
private DataFlow::Node getPropSpec(Component component) {
726+
result = component.getOption("props")
727+
or
728+
result = component.(SingleFileComponent).getDefinePropsCall().getArgument(0)
729+
}
730+
731+
/**
732+
* Holds if `component` has an input prop with the given name, that is of a taintable type.
733+
*/
734+
private predicate hasTaintableProp(Component component, string name) {
735+
exists(DataFlow::SourceNode spec | spec = getPropSpec(component).getALocalSource() |
736+
spec.(DataFlow::ArrayCreationNode).getAnElement().getStringValue() = name
737+
or
738+
exists(DataFlow::PropWrite write |
739+
write = spec.getAPropertyWrite(name) and
740+
not DataFlow::globalVarRef(["Number", "Boolean"]).flowsTo(write.getRhs())
741+
)
742+
)
743+
or
744+
exists(FieldDeclaration field |
745+
field = component.getAsClassComponent().getClassDefinition().getField(name) and
746+
DataFlow::moduleMember("vue-property-decorator", "Prop")
747+
.getACall()
748+
.flowsToExpr(field.getADecorator().getExpression()) and
749+
not isSafeField(field)
750+
)
751+
or
752+
// defineProps() can be called with only type arguments and then the Vue compiler will
753+
// infer the prop types.
754+
exists(CallExpr call, FieldDeclaration field |
755+
call = component.(SingleFileComponent).getDefinePropsCall().asExpr() and
756+
field = call.getTypeArgument(0).(InterfaceTypeExpr).getMember(name) and
757+
not isSafeField(field)
758+
)
759+
}
760+
761+
private class PropAsViewComponentInput extends ViewComponentInput {
762+
PropAsViewComponentInput() {
763+
exists(Component component, string name | hasTaintableProp(component, name) |
764+
this = component.getAnInstanceRef().getAPropertyRead(name)
765+
or
766+
// defineProps() returns the props
767+
this = component.(SingleFileComponent).getDefinePropsCall().getAPropertyRead(name)
768+
)
769+
}
770+
771+
override string getSourceType() { result = "Vue prop" }
772+
}
700773
}

javascript/ql/lib/semmle/javascript/security/dataflow/CommandInjectionCustomizations.qll

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ module CommandInjection {
3434
* An active threat-model source, considered as a flow source.
3535
*/
3636
private class ActiveThreatModelSourceAsSource extends Source instanceof ActiveThreatModelSource {
37-
ActiveThreatModelSourceAsSource() { not this instanceof ClientSideRemoteFlowSource }
37+
ActiveThreatModelSourceAsSource() { not this.isClientSideSource() }
3838

3939
override string getSourceType() { result = "a user-provided value" }
4040
}

javascript/ql/lib/semmle/javascript/security/dataflow/CorsMisconfigurationForCredentialsCustomizations.qll

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ module CorsMisconfigurationForCredentials {
3636
* An active threat-model source, considered as a flow source.
3737
*/
3838
private class ActiveThreatModelSourceAsSource extends Source instanceof ActiveThreatModelSource {
39-
ActiveThreatModelSourceAsSource() { not this instanceof ClientSideRemoteFlowSource }
39+
ActiveThreatModelSourceAsSource() { not this.isClientSideSource() }
4040
}
4141

4242
/**

javascript/ql/lib/semmle/javascript/security/dataflow/LogInjectionQuery.qll

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ deprecated class LogInjectionConfiguration extends TaintTracking::Configuration
5454
* A source of remote user controlled input.
5555
*/
5656
class RemoteSource extends Source instanceof RemoteFlowSource {
57-
RemoteSource() { not this instanceof ClientSideRemoteFlowSource }
57+
RemoteSource() { not this.isClientSideSource() }
5858
}
5959

6060
/**

javascript/ql/lib/semmle/javascript/security/dataflow/RegExpInjectionCustomizations.qll

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ module RegExpInjection {
3434
* An active threat-model source, considered as a flow source.
3535
*/
3636
private class ActiveThreatModelSourceAsSource extends Source instanceof ActiveThreatModelSource {
37-
ActiveThreatModelSourceAsSource() { not this instanceof ClientSideRemoteFlowSource }
37+
ActiveThreatModelSourceAsSource() { not this.isClientSideSource() }
3838
}
3939

4040
private import IndirectCommandInjectionCustomizations

javascript/ql/lib/semmle/javascript/security/dataflow/RemoteFlowSources.qll

+6
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,18 @@ private module Cached {
2424

2525
/**
2626
* A source of remote input in a web browser environment.
27+
*
28+
* Note that this does not include `view-component-input` sources even if that threat model has been enabled by the user.
29+
* Consider using the predicate `ThreatModelSource#isClientSideSource()` to check for a broader class of client-side sources.
2730
*/
2831
cached
2932
abstract class ClientSideRemoteFlowSource extends RemoteFlowSource {
3033
/** Gets a string indicating what part of the browser environment this was derived from. */
3134
cached
3235
abstract ClientSideRemoteFlowKind getKind();
36+
37+
cached
38+
final override predicate isClientSideSource() { any() }
3339
}
3440
}
3541

javascript/ql/lib/semmle/javascript/security/dataflow/RequestForgeryCustomizations.qll

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ module RequestForgery {
5252
not this.(ClientSideRemoteFlowSource).getKind().isPathOrUrl()
5353
}
5454

55-
override predicate isServerSide() { not this instanceof ClientSideRemoteFlowSource }
55+
override predicate isServerSide() { not super.isClientSideSource() }
5656
}
5757

5858
/**

javascript/ql/lib/semmle/javascript/security/dataflow/ResourceExhaustionCustomizations.qll

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ module ResourceExhaustion {
6363
private class ActiveThreatModelSourceAsSource extends Source instanceof ActiveThreatModelSource {
6464
ActiveThreatModelSourceAsSource() {
6565
// exclude source that only happen client-side
66-
not this instanceof ClientSideRemoteFlowSource and
66+
not this.isClientSideSource() and
6767
not this = DataFlow::parameterNode(any(PostMessageEventHandler pmeh).getEventParameter())
6868
}
6969
}

javascript/ql/lib/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll

+1-1
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,7 @@ module TaintedPath {
719719
* An active threat-model source, considered as a flow source.
720720
*/
721721
private class ActiveThreatModelSourceAsSource extends Source instanceof ActiveThreatModelSource {
722-
ActiveThreatModelSourceAsSource() { not this instanceof ClientSideRemoteFlowSource }
722+
ActiveThreatModelSourceAsSource() { not this.isClientSideSource() }
723723
}
724724

725725
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
category: majorAnalysis
3+
---
4+
* Added a new threat model kind called `view-component-input`, which can enabled with [advanced setup](https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning#extending-codeql-coverage-with-threat-models).
5+
When enabled, all React props, Vue props, and input fields in an Angular component are seen as taint sources, even if none of the corresponding instantiation sites appear to pass in a tainted value.
6+
Some users may prefer this as a "defense in depth" option but note that it may result in false positives.
7+
Regardless of whether the threat model is enabled, CodeQL will propagate taint from the instantiation sites of such components into the components themselves.

javascript/ql/src/meta/alerts/TaintSources.ql

+3-10
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,6 @@
1111
import javascript
1212
import meta.internal.TaintMetrics
1313

14-
string getName(DataFlow::Node node) {
15-
result = node.(RemoteFlowSource).getSourceType()
16-
or
17-
not node instanceof RemoteFlowSource and
18-
result = "Taint source"
19-
}
20-
21-
from DataFlow::Node node
22-
where node = relevantTaintSource()
23-
select node, getName(node)
14+
from ThreatModelSource node
15+
where node = relevantTaintSource() and node.getThreatModel() = "remote"
16+
select node, getTaintSourceName(node)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @name Threat model sources
3+
* @description Sources of possibly untrusted input that can be configured via threat models.
4+
* @kind problem
5+
* @problem.severity recommendation
6+
* @id js/meta/alerts/threat-model-sources
7+
* @tags meta
8+
* @precision very-low
9+
*/
10+
11+
import javascript
12+
import meta.internal.TaintMetrics
13+
14+
from ThreatModelSource node, string threatModel
15+
where
16+
node = relevantTaintSource() and
17+
threatModel = node.getThreatModel() and
18+
threatModel != "remote" // "remote" is reported by TaintSources.ql
19+
select node, getTaintSourceName(node) + " (\"" + threatModel + "\" threat model)"

javascript/ql/src/meta/internal/TaintMetrics.qll

+9-2
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ DataFlow::Node relevantTaintSink(string kind) {
7575
DataFlow::Node relevantTaintSink() { result = relevantTaintSink(_) }
7676

7777
/**
78-
* Gets a relevant remote flow source.
78+
* Gets a relevant threat model source.
7979
*/
80-
RemoteFlowSource relevantTaintSource() { not result.getFile() instanceof IgnoredFile }
80+
ThreatModelSource relevantTaintSource() { not result.getFile() instanceof IgnoredFile }
8181

8282
/**
8383
* Gets the output of a call that shows intent to sanitize a value
@@ -100,3 +100,10 @@ DataFlow::Node relevantSanitizerInput() {
100100
result = any(HtmlSanitizerCall call).getInput() and
101101
not result.getFile() instanceof IgnoredFile
102102
}
103+
104+
string getTaintSourceName(DataFlow::Node node) {
105+
result = node.(ThreatModelSource).getSourceType()
106+
or
107+
not node instanceof ThreatModelSource and
108+
result = "Taint source"
109+
}

0 commit comments

Comments
 (0)