Merge branch 'main' into angular-sources-sinks

This commit is contained in:
Paul Hodgkinson
2025-01-24 15:46:48 +00:00
committed by GitHub
119 changed files with 13233 additions and 4129 deletions

View File

@@ -26,6 +26,11 @@ class ThreatModelSource extends DataFlow::Node instanceof ThreatModelSource::Ran
/** Gets a string that describes the type of this threat-model source. */
string getSourceType() { result = super.getSourceType() }
/**
* Holds if this is a source of data that is specific to the web browser environment.
*/
predicate isClientSideSource() { super.isClientSideSource() }
}
/** Provides a class for modeling new sources for specific threat-models. */
@@ -48,6 +53,11 @@ module ThreatModelSource {
/** Gets a string that describes the type of this threat-model source. */
abstract string getSourceType();
/**
* Holds if this is a source of data that is specific to the web browser environment.
*/
predicate isClientSideSource() { this.getThreatModel() = "view-component-input" }
}
}

View File

@@ -0,0 +1,47 @@
/**
* Provides a classes and predicates for contributing to the `view-component-input` threat model.
*/
private import javascript
/**
* An input to a view component, such as React props.
*/
abstract class ViewComponentInput extends DataFlow::Node {
/** Gets a string that describes the type of this threat-model source. */
abstract string getSourceType();
}
private class ViewComponentInputAsThreatModelSource extends ThreatModelSource::Range instanceof ViewComponentInput
{
ViewComponentInputAsThreatModelSource() { not isSafeType(this.asExpr().getType()) }
final override string getThreatModel() { result = "view-component-input" }
final override string getSourceType() { result = ViewComponentInput.super.getSourceType() }
}
private predicate isSafeType(Type t) {
t instanceof NumberLikeType
or
t instanceof BooleanLikeType
or
t instanceof UndefinedType
or
t instanceof NullType
or
t instanceof VoidType
or
hasSafeTypes(t, t.(UnionType).getNumElementType())
or
isSafeType(t.(IntersectionType).getAnElementType())
}
/** Hold if the first `n` components of `t` are safe types. */
private predicate hasSafeTypes(UnionType t, int n) {
isSafeType(t.getElementType(0)) and
n = 1
or
isSafeType(t.getElementType(n - 1)) and
hasSafeTypes(t, n - 1)
}

View File

@@ -8,6 +8,7 @@ private import semmle.javascript.security.dataflow.CodeInjectionCustomizations
private import semmle.javascript.security.dataflow.ClientSideUrlRedirectCustomizations
private import semmle.javascript.DynamicPropertyAccess
private import semmle.javascript.dataflow.internal.PreCallGraphStep
private import semmle.javascript.ViewComponentInput
/**
* Provides classes for working with Angular (also known as Angular 2.x) applications.
@@ -610,4 +611,17 @@ module Angular2 {
)
}
}
private class InputFieldAsViewComponentInput extends ViewComponentInput {
InputFieldAsViewComponentInput() {
this =
API::moduleImport("@angular/core")
.getMember("Input")
.getReturn()
.getADecoratedMember()
.asSource()
}
override string getSourceType() { result = "Angular component input field" }
}
}

View File

@@ -5,6 +5,7 @@
import javascript
private import semmle.javascript.dataflow.internal.FlowSteps as FlowSteps
private import semmle.javascript.dataflow.internal.PreCallGraphStep
private import semmle.javascript.ViewComponentInput
/**
* Gets a reference to the 'React' object.
@@ -868,3 +869,9 @@ private class PropsFlowStep extends PreCallGraphStep {
)
}
}
private class ReactPropAsViewComponentInput extends ViewComponentInput {
ReactPropAsViewComponentInput() { this = any(ReactComponent c).getADirectPropsAccess() }
override string getSourceType() { result = "React props" }
}

View File

@@ -3,6 +3,7 @@
*/
import javascript
import semmle.javascript.ViewComponentInput
module Vue {
/** The global variable `Vue`, as an API graph entry point. */
@@ -85,17 +86,16 @@ module Vue {
* A class with a `@Component` decorator, making it usable as an "options" object in Vue.
*/
class ClassComponent extends DataFlow::ClassNode {
private ClassDefinition cls;
DataFlow::Node decorator;
ClassComponent() {
exists(ClassDefinition cls |
this = cls.flow() and
cls.getADecorator().getExpression() = decorator.asExpr() and
(
componentDecorator().flowsTo(decorator)
or
componentDecorator().getACall() = decorator
)
this = cls.flow() and
cls.getADecorator().getExpression() = decorator.asExpr() and
(
componentDecorator().flowsTo(decorator)
or
componentDecorator().getACall() = decorator
)
}
@@ -105,6 +105,9 @@ module Vue {
* These options correspond to the options one would pass to `new Vue({...})` or similar.
*/
API::Node getDecoratorOptions() { result = decorator.(API::CallNode).getParameter(0) }
/** Gets the AST node for the class definition. */
ClassDefinition getClassDefinition() { result = cls }
}
private string memberKindVerb(DataFlow::MemberKind kind) {
@@ -460,6 +463,12 @@ module Vue {
SingleFileComponent() { this = MkSingleFileComponent(file) }
/** Gets a call to `defineProps` in this component. */
DataFlow::CallNode getDefinePropsCall() {
result = DataFlow::globalVarRef("defineProps").getACall() and
result.getFile() = file
}
override Template::Element getTemplateElement() {
exists(HTML::Element e | result.(Template::HtmlElement).getElement() = e |
e.getFile() = file and
@@ -697,4 +706,68 @@ module Vue {
override ClientSideRemoteFlowKind getKind() { result = kind }
}
/**
* Holds if the given type annotation indicates a value that is not typically considered taintable.
*/
private predicate isSafeType(TypeAnnotation type) {
type.isBooleany() or
type.isNumbery() or
type.isRawFunction() or
type instanceof FunctionTypeExpr
}
/**
* Holds if the given field has a type that indicates that is can not contain a taintable value.
*/
private predicate isSafeField(FieldDeclaration field) { isSafeType(field.getTypeAnnotation()) }
private DataFlow::Node getPropSpec(Component component) {
result = component.getOption("props")
or
result = component.(SingleFileComponent).getDefinePropsCall().getArgument(0)
}
/**
* Holds if `component` has an input prop with the given name, that is of a taintable type.
*/
private predicate hasTaintableProp(Component component, string name) {
exists(DataFlow::SourceNode spec | spec = getPropSpec(component).getALocalSource() |
spec.(DataFlow::ArrayCreationNode).getAnElement().getStringValue() = name
or
exists(DataFlow::PropWrite write |
write = spec.getAPropertyWrite(name) and
not DataFlow::globalVarRef(["Number", "Boolean"]).flowsTo(write.getRhs())
)
)
or
exists(FieldDeclaration field |
field = component.getAsClassComponent().getClassDefinition().getField(name) and
DataFlow::moduleMember("vue-property-decorator", "Prop")
.getACall()
.flowsToExpr(field.getADecorator().getExpression()) and
not isSafeField(field)
)
or
// defineProps() can be called with only type arguments and then the Vue compiler will
// infer the prop types.
exists(CallExpr call, FieldDeclaration field |
call = component.(SingleFileComponent).getDefinePropsCall().asExpr() and
field = call.getTypeArgument(0).(InterfaceTypeExpr).getMember(name) and
not isSafeField(field)
)
}
private class PropAsViewComponentInput extends ViewComponentInput {
PropAsViewComponentInput() {
exists(Component component, string name | hasTaintableProp(component, name) |
this = component.getAnInstanceRef().getAPropertyRead(name)
or
// defineProps() returns the props
this = component.(SingleFileComponent).getDefinePropsCall().getAPropertyRead(name)
)
}
override string getSourceType() { result = "Vue prop" }
}
}

View File

@@ -27,6 +27,12 @@ module BrokenCryptoAlgorithmConfig implements DataFlow::ConfigSig {
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
result = sink.(Sink).getLocation()
or
result = sink.(Sink).getInitialization().getLocation()
}
}
/**

View File

@@ -33,6 +33,12 @@ module ClientSideRequestForgeryConfig implements DataFlow::ConfigSig {
}
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
result = sink.(Sink).getLocation()
or
result = sink.(Sink).getARequest().getLocation()
}
}
/**

View File

@@ -34,7 +34,7 @@ module CommandInjection {
* An active threat-model source, considered as a flow source.
*/
private class ActiveThreatModelSourceAsSource extends Source instanceof ActiveThreatModelSource {
ActiveThreatModelSourceAsSource() { not this instanceof ClientSideRemoteFlowSource }
ActiveThreatModelSourceAsSource() { not this.isClientSideSource() }
override string getSourceType() { result = "a user-provided value" }
}

View File

@@ -32,6 +32,13 @@ module CommandInjectionConfig implements DataFlow::ConfigSig {
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
exists(DataFlow::Node node |
isSinkWithHighlight(sink, node) and
result = node.getLocation()
)
}
}
/**

View File

@@ -36,7 +36,7 @@ module CorsMisconfigurationForCredentials {
* An active threat-model source, considered as a flow source.
*/
private class ActiveThreatModelSourceAsSource extends Source instanceof ActiveThreatModelSource {
ActiveThreatModelSourceAsSource() { not this instanceof ClientSideRemoteFlowSource }
ActiveThreatModelSourceAsSource() { not this.isClientSideSource() }
}
/**

View File

@@ -25,6 +25,12 @@ module CorsMisconfigurationConfig implements DataFlow::ConfigSig {
}
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
result = sink.(Sink).getLocation()
or
result = sink.(Sink).getCredentialsHeader().getLocation()
}
}
/**

View File

@@ -35,6 +35,15 @@ module DeepObjectResourceExhaustionConfig implements DataFlow::StateConfigSig {
}
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
result = sink.(Sink).getLocation()
or
exists(DataFlow::Node link |
sink.(Sink).hasReason(link, _) and
result = link.getLocation()
)
}
}
/**

View File

@@ -28,6 +28,13 @@ module IndirectCommandInjectionConfig implements DataFlow::ConfigSig {
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
exists(DataFlow::Node node |
isSinkWithHighlight(sink, node) and
result = node.getLocation()
)
}
}
/**

View File

@@ -25,6 +25,12 @@ module InsecureDownloadConfig implements DataFlow::StateConfigSig {
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
result = sink.(Sink).getLocation()
or
result = sink.(Sink).getDownloadCall().getLocation()
}
}
/**

View File

@@ -54,7 +54,7 @@ deprecated class LogInjectionConfiguration extends TaintTracking::Configuration
* A source of remote user controlled input.
*/
class RemoteSource extends Source instanceof RemoteFlowSource {
RemoteSource() { not this instanceof ClientSideRemoteFlowSource }
RemoteSource() { not this.isClientSideSource() }
}
/**

View File

@@ -49,6 +49,15 @@ module PrototypePollutionConfig implements DataFlow::StateConfigSig {
}
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
result = sink.(Sink).getLocation()
or
exists(Locatable loc |
sink.(Sink).dependencyInfo(_, loc) and
result = loc.getLocation()
)
}
}
/**

View File

@@ -34,7 +34,7 @@ module RegExpInjection {
* An active threat-model source, considered as a flow source.
*/
private class ActiveThreatModelSourceAsSource extends Source instanceof ActiveThreatModelSource {
ActiveThreatModelSourceAsSource() { not this instanceof ClientSideRemoteFlowSource }
ActiveThreatModelSourceAsSource() { not this.isClientSideSource() }
}
private import IndirectCommandInjectionCustomizations

View File

@@ -24,12 +24,18 @@ private module Cached {
/**
* A source of remote input in a web browser environment.
*
* Note that this does not include `view-component-input` sources even if that threat model has been enabled by the user.
* Consider using the predicate `ThreatModelSource#isClientSideSource()` to check for a broader class of client-side sources.
*/
cached
abstract class ClientSideRemoteFlowSource extends RemoteFlowSource {
/** Gets a string indicating what part of the browser environment this was derived from. */
cached
abstract ClientSideRemoteFlowKind getKind();
cached
final override predicate isClientSideSource() { any() }
}
}

View File

@@ -52,7 +52,7 @@ module RequestForgery {
not this.(ClientSideRemoteFlowSource).getKind().isPathOrUrl()
}
override predicate isServerSide() { not this instanceof ClientSideRemoteFlowSource }
override predicate isServerSide() { not super.isClientSideSource() }
}
/**

View File

@@ -28,6 +28,12 @@ module RequestForgeryConfig implements DataFlow::ConfigSig {
}
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
result = sink.(Sink).getLocation()
or
result = sink.(Sink).getARequest().getLocation()
}
}
/**

View File

@@ -63,7 +63,7 @@ module ResourceExhaustion {
private class ActiveThreatModelSourceAsSource extends Source instanceof ActiveThreatModelSource {
ActiveThreatModelSourceAsSource() {
// exclude source that only happen client-side
not this instanceof ClientSideRemoteFlowSource and
not this.isClientSideSource() and
not this = DataFlow::parameterNode(any(PostMessageEventHandler pmeh).getEventParameter())
}
}

View File

@@ -29,6 +29,13 @@ module ShellCommandInjectionFromEnvironmentConfig implements DataFlow::ConfigSig
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
exists(DataFlow::Node node |
isSinkWithHighlight(sink, node) and
result = node.getLocation()
)
}
}
/**

View File

@@ -719,7 +719,7 @@ module TaintedPath {
* An active threat-model source, considered as a flow source.
*/
private class ActiveThreatModelSourceAsSource extends Source instanceof ActiveThreatModelSource {
ActiveThreatModelSourceAsSource() { not this instanceof ClientSideRemoteFlowSource }
ActiveThreatModelSourceAsSource() { not this.isClientSideSource() }
}
/**

View File

@@ -34,6 +34,12 @@ module UnsafeCodeConstruction {
DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext }
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
result = sink.(Sink).getLocation()
or
result = sink.(Sink).getCodeSink().getLocation()
}
}
/**

View File

@@ -62,6 +62,12 @@ module UnsafeHtmlConstructionConfig implements DataFlow::StateConfigSig {
DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext }
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
result = sink.(Sink).getLocation()
or
result = sink.(Sink).getSink().getLocation()
}
}
/**

View File

@@ -38,6 +38,12 @@ module UnsafeJQueryPluginConfig implements DataFlow::ConfigSig {
}
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSourceLocation(DataFlow::Node source) {
result = source.(Source).getLocation()
or
result = source.(Source).getPlugin().getLocation()
}
}
/**

View File

@@ -27,6 +27,14 @@ module UnsafeShellCommandConstructionConfig implements DataFlow::ConfigSig {
DataFlow::FlowFeature getAFeature() { result instanceof DataFlow::FeatureHasSourceCallContext }
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
result = sink.(Sink).getLocation()
or
result = sink.(Sink).getAlertLocation().getLocation()
or
result = sink.(Sink).getCommandExecution().getLocation()
}
}
/**

View File

@@ -27,6 +27,14 @@ module PolynomialReDoSConfig implements DataFlow::ConfigSig {
int fieldFlowBranchLimit() { result = 1 } // library inputs are too expensive on some projects
predicate observeDiffInformedIncrementalMode() { any() }
Location getASelectedSinkLocation(DataFlow::Node sink) {
result = sink.(Sink).getLocation()
or
result = sink.(Sink).getHighlight().getLocation()
or
result = sink.(Sink).getRegExp().getLocation()
}
}
/** Taint-tracking for reasoning about polynomial regular expression denial-of-service attacks. */