Skip to content

JS: Fix handling of constant array index reads, and fix the fallout#17412

Merged
asgerf merged 25 commits into
github:js/shared-dataflow-branchfrom
asgerf:jss/array-index-constant
Sep 18, 2024
Merged

JS: Fix handling of constant array index reads, and fix the fallout#17412
asgerf merged 25 commits into
github:js/shared-dataflow-branchfrom
asgerf:jss/array-index-constant

Conversation

@asgerf

@asgerf asgerf commented Sep 9, 2024

Copy link
Copy Markdown
Contributor

Previously, when reading an array element at a known index, we did not read values stored at an unknown index. We now properly read from unknown array indices.

However, this interacted poorly with how we handled split('#') and split('?') calls in TaintedUrlSuffix. In this case we want to block flow into the first element of the resulting array, while also transitioning from the tainted-url-suffix state to the taint state. Instead of the various hacks we've employed in the past, this is now modelled with the help of flow summaries.

Optional steps: Flow summaries for flow state and query-specific steps

Flow summaries cannot be query-specific and don't know anything about flow states (because those are query-specific), but I've adopted a solution where a flow summary can contribute so-called optional steps and barriers identified by a name, which queries can then opt into using their name.

So we use this summary for split('#') and split('?'):

      input = "Argument[this].OptionalBarrier[split-url-suffix]" and
      output = "ReturnValue.ArrayElement"
      or
      input = "Argument[this].OptionalStep[split-url-suffix-post]" and
      output = "ReturnValue.ArrayElement[1]"

And we then use those steps in our queries (here simplifying the indirection through the TaintedUrlSuffix.qll library)

predicate isBarrier(DataFlow::Node node, FlowState state) {
  optionalBarrier(node, "split-url-suffix") and state = "tainted-url-suffix"
}

predicate isAdditionalFlowStep(DataFlow::Node node1, FlowState state1, DataFlow::Node node2, FlowState state2) {
  optionalStep(node1, "split-url-suffix-post", node2) and
  state1 = "tainted-url-suffix" and
  state2 = "taint"
}

The barrier is used to disable the default behaviour of the split summary (which taints the whole array), and the step then enables flow into the array element at index 1.

At the moment this is hidden in DataFlowPrivate but we could consider exposing the feature more widely in the future.

Evaluation

Evaluation looks good, it fixes 8 FPs and has neutral perf. Since that's based on a slightly older version of the PR, a new run is also underway.

For ContentSet it is ambiguous whether asArrayIndex() should get a singleton content set, or the KnownArrayElement content set. The user will now have to choose between asSingleton().asArrayIndex() or ContentSet::arrayElementKnown.
@asgerf asgerf added the no-change-note-required This PR does not need a change note label Sep 9, 2024
@github-actions github-actions Bot added the JS label Sep 9, 2024
OptionalStep[foo] and OptionalBarrier[foo] contribute steps/barriers that are not active by default, but can be opted into by specific queries or for specific flow states.

(Will be used in the following commits)
This summary uses the notion of optional steps/barriers so it becomes configurable whether there is flow into the zero'th array element.

Also makes sure we handle the second-argument version of split().
Array.prototype.shift only returns the first array element.

The mutation of Argument[this] is not yet modelled, and is better handled when we have use-use flow.
TaintedUrlSuffix is currently only used in TaintTracking configs meaning it is already propagated
by taint steps. The inclusion of these taint steps here however meant that implicit reads could appear prior to any of these steps.

This was is problematic for PropRead steps as an expression like x[0] could spuriously read from array element 1 via the path:

x [element 1]
x [empty access path] (after implicit read)
x[0] (taint step through PropRead)
A URL of form https://example.com?evil#bar will contain '?evil' after splitting out the '#' suffix, and vice versa.
Preserving tainted-url-suffix into array element 0 seemed like a good idea, but didn't work out so well.
@asgerf asgerf force-pushed the jss/array-index-constant branch from 5f90820 to 1df69ec Compare September 12, 2024 11:43
@asgerf asgerf changed the title JS: Fix handling of constant array index reads JS: Fix handling of constant array index reads, and fix the fallout Sep 12, 2024
@asgerf asgerf marked this pull request as ready for review September 12, 2024 12:15
@asgerf asgerf requested a review from a team as a code owner September 12, 2024 12:15
@asgerf asgerf requested a review from erik-krogh September 13, 2024 09:59

@erik-krogh erik-krogh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice work again 👍

input = "Argument[this].ArrayElement[0]" and
output = "ReturnValue"
or
// ArrayElement[0] is not automatically converted to a taint step, so add it manually

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this comment is placed at the right place?
Or you meant Argument[this] instead of ArrayElement[0]?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It refers to the ArrayElement[0] in the summary above here.

ArrayElement is automatically converted to a taint step, but ArrayElement[0] is not, which is why we need this extra summary.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, now I get it.
Maybe a reword like:
// The above doesn't add a taint-step from the array itself to the return value, so we add that manually.

Comment thread javascript/ql/lib/semmle/javascript/internal/flow_summaries/Strings.qll Outdated
@asgerf asgerf merged commit 5e4c090 into github:js/shared-dataflow-branch Sep 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

JS no-change-note-required This PR does not need a change note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants