Swift: Unsafe JS Eval Query#11001
Conversation
| let remoteString = getRemoteData() | ||
|
|
||
| try! await sink(localString) // GOOD: the HTML data is local | ||
| try! await sink(getRemoteData()) // BAD: HTML contains remote input, may access local secrets |
There was a problem hiding this comment.
How will we tell which sink types each case is working for? Might we be better off with thorough tests for just one sink type, then an additional simple test of each other sink type to verify coverage?
There was a problem hiding this comment.
True, it would be simpler if I inlined (most of) the tests. But we can eye-ball the correctness of the test by running the query in VSCode and checking that each of the sinks has the same number of paths leading to it.
The `-arch=x86_64` from `swift/rules.bzl` turns out to be unnecessary, even on Arm-based Macs.
c3cd871 to
3d24e0a
Compare
Instead of hand-rolled predicates.
This enables DataFlow to skip over it and not get stuck.
TODO: Convert those flow steps to taint flow models in the library.
Still TODO: a more comprehensive taint flow model for Data in the libs.
geoffw0
left a comment
There was a problem hiding this comment.
Looks great. We have a lot of follow-up work to do on models!
Also thanks for updating the QL to use the new predicates (MethodDecl).hasQualifiedName and FreeFunctionDecl.
|
|
||
| override predicate isSanitizer(DataFlow::Node node) { | ||
| none() // TODO: A conversion to a primitive type or an enum | ||
| // TODO: convert to new taint flow models |
There was a problem hiding this comment.
I think we should aim to create follow-up tasks for most of these things rather than addressing them all now. Many are planned already. And I don't want this PR just growing and growing.
There was a problem hiding this comment.
Makes sense. Though it's a tradeoff between a growing PR and shipping more tech debt. An alternative is that I move these additional taint steps into the library, and create issues to flesh the library out to handle other cases that I didn't handle here.
There was a problem hiding this comment.
That sounds fine as long as its straightforward.
I have a preference for smaller, faster PRs where possible as they're easier for everyone to review and manage.
|
QHelp previews: swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelpJavaScript InjectionEvaluating JavaScript that contains a substring from a remote origin may lead to remote code execution. Code written by an attacker can execute unauthorized actions, including exfiltration of local data through a third party web service. RecommendationWhen loading JavaScript into a web view, evaluate only known, locally-defined source code. If part of the input comes from a remote source, do not inject it into the JavaScript code to be evaluated. Instead, send it to the web view as data using an API such as ExampleIn the following (bad) example, a call to In the following (good) example, we sanitize the remote data by passing it using the References
|
geoffw0
left a comment
There was a problem hiding this comment.
Otherwise I think we're just waiting for docs review. The DCA run looks fine I think.
| let remoteString = getRemoteData() | ||
|
|
||
| try! await sink(localString) // GOOD: the HTML data is local | ||
| try! await sink(getRemoteData()) // BAD [NOT DETECTED - TODO: extract Callables of @MainActor method calls]: HTML contains remote input, may access local secrets |
There was a problem hiding this comment.
I wonder if the results of this test might be easier to interpret if we replace the calls to getRemoteData with an in-place taint source such as:
try String(contentsOf: URL(string: "http://example.com/")!)
Then the sources + sinks on the result lines should show us all of the combinations are working?
There was a problem hiding this comment.
Weirdly enough, they are harder to interpret (see the latest commit). Now there is a separate result for this new in-place source (as expected). But unexpectedly, for the JSEvaluateScript sinks, there are two separate paths for this result: one passing through the JSString constructor function, one omitting this taint step.
There was a problem hiding this comment.
for the JSEvaluateScript sinks, there are two separate paths for this result: one passing through the JSString constructor function, one omitting this taint step.
I think I see what you mean, there's an unnecessary edge:
| UnsafeJsEval.swift:287:31:287:97 | call to JSStringCreateWithCharacters(_:_:) : | UnsafeJsEval.swift:291:17:291:17 | jsstr |
and also:
| UnsafeJsEval.swift:286:51:286:51 | stringBytes : | UnsafeJsEval.swift:291:17:291:17 | jsstr |
But they don't appear to be new in this commit.
The new results under #select look good to me. You will need to replace the other calls to getRemoteData with inline sinks to see the complete grid of results there.
There was a problem hiding this comment.
Notably both unnecessary edges are the transitive closure of more than one isAdditionalTaintStep step, ending at the sink. I wonder if this is a bug in taint flow itself?
There was a problem hiding this comment.
The dataflow library will "compress" both dataflow and taint-flow steps before reporting them as edges in the edges relation. So we can't really conclude that there's a bug based on the edges data in the .expected file.
If you want to disable this compression, you can replace none() with any() in the charpred for CastNode here. Then each local step will be visible in the edges relation.
There was a problem hiding this comment.
Are you saying the additional edges are expected / harmless?
There was a problem hiding this comment.
Not quite. I'm saying that the transitive closure of more than one isAdditionalTaintStep is expected (this happens all the time in dataflow/taintflow). But this shouldn't produce additional spurious edges. The presence of additional edges might indicate an unnecessary taintflow/dataflow rule somewhere.
There was a problem hiding this comment.
I checked over the rules in isAdditionalTaintStep for this query again and they all seem reasonable. I'm not sure what else could be generating edges through this - JSString* isn't modelled yet so it should be nothing.
There was a problem hiding this comment.
Feel free to create an issue for it. I don't think this should block this PR.
sabrowning1
left a comment
There was a problem hiding this comment.
@d10c hello from Docs! 👋🏼 Thanks for your work on the documentation; I made some small suggestions to bring things in line with our style guide. Let me know if you have any questions!
Co-authored-by: Sam Browning <106113886+sabrowning1@users.noreply.github.com>
Co-authored-by: Sam Browning <106113886+sabrowning1@users.noreply.github.com>
Co-authored-by: Sam Browning <106113886+sabrowning1@users.noreply.github.com>
Co-authored-by: Sam Browning <106113886+sabrowning1@users.noreply.github.com>
Co-authored-by: Sam Browning <106113886+sabrowning1@users.noreply.github.com>
Strangely, there are two separate paths to each of the JSEvaluateScript sinks: one passing through the JSString constructor, one omitting this step.
sabrowning1
left a comment
There was a problem hiding this comment.
Looks good for Docs, thanks for making those changes @d10c! 🎉
| let remoteString = getRemoteData() | ||
|
|
||
| sink(localString) // GOOD: the HTML data is local | ||
| sink(try! String(contentsOf: URL(string: "http://example.com/")!)) // BAD: HTML contains remote input, may access local secrets |
There was a problem hiding this comment.
Do you plan to change all of the cases in testSync / testAsync work this way? I still think it will be easier to read the results like that - though if you feel otherwise I don't want us to get stuck on this.
No description provided.