diff --git a/javascript/ql/lib/semmle/javascript/dataflow/internal/FlowSummaryPrivate.qll b/javascript/ql/lib/semmle/javascript/dataflow/internal/FlowSummaryPrivate.qll index 0f25c694f61..31f5f16bbfb 100644 --- a/javascript/ql/lib/semmle/javascript/dataflow/internal/FlowSummaryPrivate.qll +++ b/javascript/ql/lib/semmle/javascript/dataflow/internal/FlowSummaryPrivate.qll @@ -264,3 +264,7 @@ module Stage { cached predicate backref() { optionalStep(_, _, _) } } + +predicate unsupportedCallable = Private::unsupportedCallable/1; + +predicate unsupportedCallable = Private::unsupportedCallable/4; diff --git a/javascript/ql/lib/semmle/javascript/frameworks/data/ModelsAsData.qll b/javascript/ql/lib/semmle/javascript/frameworks/data/ModelsAsData.qll index 9f143a9713a..0e19e84b666 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/data/ModelsAsData.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/data/ModelsAsData.qll @@ -19,6 +19,7 @@ private import javascript private import internal.ApiGraphModels as Shared private import internal.ApiGraphModelsSpecific as Specific +private import semmle.javascript.dataflow.internal.FlowSummaryPrivate private import semmle.javascript.endpoints.EndpointNaming as EndpointNaming import Shared::ModelInput as ModelInput import Shared::ModelOutput as ModelOutput @@ -45,12 +46,50 @@ private class ThreatModelSourceFromDataExtension extends ThreatModelSource::Rang } } +private class SummarizedCallableFromModel extends DataFlow::SummarizedCallable { + string type; + string path; + + SummarizedCallableFromModel() { + ModelOutput::relevantSummaryModel(type, path, _, _, _, _) and + this = type + ";" + path + } + + override DataFlow::InvokeNode getACall() { ModelOutput::resolvedSummaryBase(type, path, result) } + + override predicate propagatesFlow( + string input, string output, boolean preservesValue, string model + ) { + exists(string kind | ModelOutput::relevantSummaryModel(type, path, input, output, kind, model) | + kind = "value" and + preservesValue = true + or + kind = "taint" and + preservesValue = false + ) + } + + predicate hasTypeAndPath(string type_, string path_) { type = type_ and path = path_ } + + predicate isUnsupportedByFlowSummaries() { unsupportedCallable(this) } +} + +private predicate shouldInduceStepsFromSummary(string type, string path) { + exists(SummarizedCallableFromModel callable | + callable.isUnsupportedByFlowSummaries() and + callable.hasTypeAndPath(type, path) + ) +} + /** * Holds if `path` is an input or output spec for a summary with the given `base` node. */ pragma[nomagic] private predicate relevantInputOutputPath(API::InvokeNode base, AccessPath inputOrOutput) { exists(string type, string input, string output, string path | + // If the summary for 'callable' could not be handled as a flow summary, we need to evaluate + // its inputs and outputs to a set of nodes, so we can generate steps instead. + shouldInduceStepsFromSummary(type, path) and ModelOutput::resolvedSummaryBase(type, path, base) and ModelOutput::relevantSummaryModel(type, path, input, output, _, _) and inputOrOutput = [input, output] @@ -81,6 +120,7 @@ private API::Node getNodeFromInputOutputPath(API::InvokeNode baseNode, AccessPat private predicate summaryStep(API::Node pred, API::Node succ, string kind) { exists(string type, string path, API::InvokeNode base, AccessPath input, AccessPath output | + shouldInduceStepsFromSummary(type, path) and ModelOutput::relevantSummaryModel(type, path, input, output, kind, _) and ModelOutput::resolvedSummaryBase(type, path, base) and pred = getNodeFromInputOutputPath(base, input) and diff --git a/javascript/ql/test/library-tests/TripleDot/underscore.string.js b/javascript/ql/test/library-tests/TripleDot/underscore.string.js index 07f186343ce..dd904cd78c3 100644 --- a/javascript/ql/test/library-tests/TripleDot/underscore.string.js +++ b/javascript/ql/test/library-tests/TripleDot/underscore.string.js @@ -39,7 +39,7 @@ function strToStr() { } function strToArray() { - sink(s.chop(source("s1"), 3)); // $ MISSING: hasTaintFlow=s1 + sink(s.chop(source("s1"), 3)); // $ hasTaintFlow=s1 sink(s.chars(source("s2"))[0]); // $ hasTaintFlow=s2 sink(s.words(source("s3"))[0]); // $ hasTaintFlow=s3 sink(s.lines(source("s7"))[0]); // $ hasTaintFlow=s7 @@ -97,7 +97,7 @@ function multiSource() { function chaining() { sink(s(source("s1")) - .slugify().capitalize().decapitalize().clean().cleanDiacritics() + .slugify().capitalize().decapitalize().clean().cleanDiacritics() .swapCase().escapeHTML().unescapeHTML().wrap().dedent() .reverse().pred().succ().titleize().camelize().classify() .underscored().dasherize().humanize().trim().ltrim().rtrim() @@ -119,8 +119,8 @@ function chaining() { .q(source("s17")).ljust(10, source("s18")) .rjust(10, source("s19"))); // $ hasTaintFlow=s16 hasTaintFlow=s17 hasTaintFlow=s18 hasTaintFlow=s19 - sink(s(source("s20")).tap(function(value) { - return value + source("s21"); + sink(s(source("s20")).tap(function(value) { + return value + source("s21"); }).value()); // $ hasTaintFlow=s20 hasTaintFlow=s21 }