Skip to content

Swift: Unsafe JS Eval Query#11001

Merged
d10c merged 22 commits intogithub:mainfrom
d10c:swift/js-injection
Nov 24, 2022
Merged

Swift: Unsafe JS Eval Query#11001
d10c merged 22 commits intogithub:mainfrom
d10c:swift/js-injection

Conversation

@d10c
Copy link
Copy Markdown
Contributor

@d10c d10c commented Oct 26, 2022

No description provided.

@github-actions github-actions Bot added the Swift label Oct 26, 2022
Comment thread .vscode/settings.json Outdated
Comment thread swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.ql
Comment thread swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.ql
Comment thread swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.ql Outdated
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
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.

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?

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.

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.

Comment thread swift/ql/test/query-tests/Security/CWE-094/UnsafeJsEval.swift Outdated
@d10c d10c force-pushed the swift/js-injection branch from c3cd871 to 3d24e0a Compare November 3, 2022 10:19
d10c added 3 commits November 3, 2022 16:14
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.
Comment thread swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.ql Fixed
d10c added 2 commits November 8, 2022 11:24
Still TODO: a more comprehensive taint flow model for Data in the libs.
Copy link
Copy Markdown
Contributor

@geoffw0 geoffw0 left a comment

Choose a reason for hiding this comment

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

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
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 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.

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.

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.

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.

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.

Comment thread swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.ql
Comment thread swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.ql Outdated
Comment thread swift/ql/test/query-tests/Security/CWE-094/UnsafeJsEval.swift Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Nov 9, 2022

QHelp previews:

swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp

JavaScript Injection

Evaluating 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.

Recommendation

When 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 WKWebView.callAsyncJavaScript with the arguments dictionary to pass remote data objects.

Example

In the following (bad) example, a call to WKWebView.evaluateJavaScript evaluates JavaScript source code that is tainted with remote data, potentially introducing a code injection vulnerability.

let webview: WKWebView
let remoteData = try String(contentsOf: URL(string: "http://example.com/evil.json")!)

...

_ = try await webview.evaluateJavaScript("console.log(" + remoteData + ")") // BAD

In the following (good) example, we sanitize the remote data by passing it using the arguments dictionary of WKWebView.callAsyncJavaScript. This ensures that untrusted data cannot be evaluated as JavaScript source code.

let webview: WKWebView
let remoteData = try String(contentsOf: URL(string: "http://example.com/evil.json")!)

...

_ = try await webview.callAsyncJavaScript(
    "console.log(data)",
    arguments: ["data": remoteData], // GOOD
    contentWorld: .page
)

References

@d10c d10c marked this pull request as ready for review November 9, 2022 12:21
@d10c d10c requested review from a team as code owners November 9, 2022 12:21
@d10c d10c added the ready-for-doc-review This PR requires and is ready for review from the GitHub docs team. label Nov 9, 2022
Comment thread swift/ql/src/queries/Security/CWE-094/UnsafeJsEvalGood.swift Outdated
Copy link
Copy Markdown
Contributor

@geoffw0 geoffw0 left a comment

Choose a reason for hiding this comment

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

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
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 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?

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.

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.

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.

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.

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.

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?

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.

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.

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.

Are you saying the additional edges are expected / harmless?

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.

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.

Copy link
Copy Markdown
Contributor

@geoffw0 geoffw0 Nov 16, 2022

Choose a reason for hiding this comment

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

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.

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.

Feel free to create an issue for it. I don't think this should block this PR.

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 agree.

Copy link
Copy Markdown
Contributor

@sabrowning1 sabrowning1 left a comment

Choose a reason for hiding this comment

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

@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!

Comment thread swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp Outdated
Comment thread swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp Outdated
Comment thread swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp Outdated
Comment thread swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp Outdated
Comment thread swift/ql/src/queries/Security/CWE-094/UnsafeJsEval.qhelp Outdated
d10c and others added 3 commits November 15, 2022 21:14
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>
d10c and others added 3 commits November 15, 2022 21:14
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
sabrowning1 previously approved these changes Nov 16, 2022
Copy link
Copy Markdown
Contributor

@sabrowning1 sabrowning1 left a comment

Choose a reason for hiding this comment

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

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
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.

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.

@geoffw0 geoffw0 requested a review from a team as a code owner November 23, 2022 14:58
Copy link
Copy Markdown
Contributor Author

@d10c d10c left a comment

Choose a reason for hiding this comment

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

Last commit LGTM 👍🏻

@d10c d10c merged commit 8f065e9 into github:main Nov 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation ready-for-doc-review This PR requires and is ready for review from the GitHub docs team. Swift

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants