Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS: Dynamic import as code injection sink #14293

Merged
merged 15 commits into from
Jun 20, 2024

Conversation

am0o0
Copy link
Contributor

@am0o0 am0o0 commented Sep 22, 2023

Dynamic import in nodejs support URLs starts with data: which is dangerous.
There is another nodejs API that accepts the data: URL which is:

const {Worker} = require('node:worker_threads');
new Worker(new URL('data:text/javascript,console.log("hello!");'))

but it needs to be a URL Type as input, not any string value that starts with data:, I'm not sure what is the best way to implement it.

@am0o0
Copy link
Contributor Author

am0o0 commented Sep 28, 2023

Hi, I wanted to add two sinks, one of the sinks should have a new URL instance in one of the middle nodes, so I've used flow steps but it seems that I can't reach some sinks like this.

@pwntester
Copy link
Contributor

pwntester commented Oct 2, 2023

@amammad Not sure flow labels are needed for this use case. Can you try removing them and checking if the taint flows through the string concatenations and URL construction (keeping the existing flow step)?

@am0o0
Copy link
Contributor Author

am0o0 commented Oct 2, 2023

@pwntester I'm using labels because dynamic import doesn't need a URL construction but worker needs.
do you want to simply create two queries for each one? Also, how can I indicate on the taint configuration that we must have a URL construction as a middle dataflow node?

@pwntester
Copy link
Contributor

pwntester commented Oct 2, 2023

Does the Worker constructor accept either a String or an URL and only the later is vulnerable? If thats the case, you need the Flow labels and affixing a label on the URL constructor that you can later check at the sink. Note that you can ignore that check for the imports sink but enforce it for the Worker one.

@am0o0
Copy link
Contributor Author

am0o0 commented Oct 2, 2023

@pwntester
If the input is string then it should be a JS file path which then is not a code injection anymore, it can be arbitrary JS file execution.
If the input is a URL object then it can lead to code injection.

@pwntester
Copy link
Contributor

@amammad Ok, in that case you need to use flow labels as you were doing. Sorry for the confusion. You can start use the regular isSource(DataFlow::Node src) which should affix the default implicit label (just an empty string) and then at the URL constructor taint step affix a new label representing that the taint tracking flowed through this specific taint step. At the sink, you can use the isSink(DataFlow::Node sink, FlowLabel sinkLbl) and check the label only for the Worker sink. I think something like that should work but have not tried it:

  override predicate isSource(DataFlow::Node source) {
    source instanceof RemoteFlowSource
  }
  override predicate isAdditionalFlowStep(
    DataFlow::Node pred, DataFlow::Node succ, FlowLabel predlbl, FlowLabel succlbl
  ) {
    exists(DataFlow::NewNode newUrl | succ = newUrl |
      newUrl = DataFlow::globalVarRef(“URL”).getAnInstantiation() and
      pred = newUrl.getArgument(0)
    ) and
    predlbl = StandardFlowLabel and
    succlbl instanceof URLConstructorLabel
  }
  override predicate isSink(DataFlow::Node sink) {
    sink instanceof DynamicImport
  }
  override predicate isSink(DataFlow::Node sink, FlowLabel label) {
    sink instanceof WorkerThreads and label instanceof URLConstructorLabel
  }

@am0o0
Copy link
Contributor Author

am0o0 commented Oct 8, 2023

@pwntester Thank you a lot! I can see the correct results now. you can see the tests and sanitizer also is working for both sinks.

@am0o0 am0o0 marked this pull request as ready for review May 27, 2024 18:37
@am0o0 am0o0 requested a review from a team as a code owner May 27, 2024 18:37
Copy link
Contributor

@erik-krogh erik-krogh left a comment

Choose a reason for hiding this comment

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

This looks OK for an experimental query, just a note about the query-id.

The solution that you guys figured out which flow-labels is nice 👍

Also, could you do a merge with main.
The CI hasn't run, and I think that might be because your branch is lacking a lot of progress from main.

* @problem.severity error
* @security-severity 9.3
* @precision high
* @id js/code-injection
Copy link
Contributor

Choose a reason for hiding this comment

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

This shouldn't have the same query-id as the query in the standard suite.

Copy link
Contributor

Choose a reason for hiding this comment

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

^ this is still an issue.

And now the CI is complaining about it.

Copy link
Contributor

github-actions bot commented Jun 6, 2024

QHelp previews:

javascript/ql/src/Security/CWE-094/CodeInjection.qhelp

Code injection

Directly evaluating user input (for example, an HTTP request parameter) as code without properly sanitizing the input first allows an attacker arbitrary code execution. This can occur when user input is treated as JavaScript, or passed to a framework which interprets it as an expression to be evaluated. Examples include AngularJS expressions or JQuery selectors.

Recommendation

Avoid including user input in any expression which may be dynamically evaluated. If user input must be included, use context-specific escaping before including it. It is important that the correct escaping is used for the type of evaluation that will occur.

Example

The following example shows part of the page URL being evaluated as JavaScript code. This allows an attacker to provide JavaScript within the URL. If an attacker can persuade a user to click on a link to such a URL, the attacker can evaluate arbitrary JavaScript in the browser of the user to, for example, steal cookies containing session information.

eval(document.location.href.substring(document.location.href.indexOf("default=")+8))

The following example shows a Pug template being constructed from user input, allowing attackers to run arbitrary code via a payload such as #{global.process.exit(1)}.

const express = require('express')
var pug = require('pug');
const app = express()

app.post('/', (req, res) => {
    var input = req.query.username;
    var template = `
doctype
html
head
    title= 'Hello world'
body
    form(action='/' method='post')
        input#name.form-control(type='text)
        button.btn.btn-primary(type='submit') Submit
    p Hello `+ input
    var fn = pug.compile(template);
    var html = fn();
    res.send(html);
})

Below is an example of how to use a template engine without any risk of template injection. The user input is included via an interpolation expression #{username} whose value is provided as an option to the template, instead of being part of the template string itself:

const express = require('express')
var pug = require('pug');
const app = express()

app.post('/', (req, res) => {
    var input = req.query.username;
    var template = `
doctype
html
head
    title= 'Hello world'
body
    form(action='/' method='post')
        input#name.form-control(type='text)
        button.btn.btn-primary(type='submit') Submit
    p Hello #{username}`
    var fn = pug.compile(template);
    var html = fn({username: input});
    res.send(html);
})

References

javascript/ql/src/experimental/Security/CWE-094-dataURL/CodeInjection.qhelp

Code injection

Directly evaluating user input (for example, an HTTP request parameter) as code without properly sanitizing the input first allows an attacker arbitrary code execution. This can occur when user input is treated as JavaScript, or passed to a framework which interprets it as an expression to be evaluated. Examples include AngularJS expressions or JQuery selectors.

Recommendation

Avoid including user input in any expression which may be dynamically evaluated. If user input must be included, use context-specific escaping before including it. It is important that the correct escaping is used for the type of evaluation that will occur.

Example

The following example shows part of the page URL being evaluated as JavaScript code on the server. This allows an attacker to provide JavaScript within the URL and send it to server. client side attacks need victim users interaction like clicking on a attacker provided URL.

const { Worker } = require('node:worker_threads');
var app = require('express')();

app.post('/path', async function (req, res) {
    const payload = req.query.queryParameter // like:  payload = 'data:text/javascript,console.log("hello!");//'
    const payloadURL = new URL(payload)
    new Worker(payloadURL);
});

app.post('/path2', async function (req, res) {
    const payload = req.query.queryParameter // like:  payload = 'data:text/javascript,console.log("hello!");//'
    await import(payload)
});

References

javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-094/CodeInjection.qhelp

Code injection with additional heuristic sources

Directly evaluating user input (for example, an HTTP request parameter) as code without properly sanitizing the input first allows an attacker arbitrary code execution. This can occur when user input is treated as JavaScript, or passed to a framework which interprets it as an expression to be evaluated. Examples include AngularJS expressions or JQuery selectors.

Recommendation

Avoid including user input in any expression which may be dynamically evaluated. If user input must be included, use context-specific escaping before including it. It is important that the correct escaping is used for the type of evaluation that will occur.

Example

The following example shows part of the page URL being evaluated as JavaScript code. This allows an attacker to provide JavaScript within the URL. If an attacker can persuade a user to click on a link to such a URL, the attacker can evaluate arbitrary JavaScript in the browser of the user to, for example, steal cookies containing session information.

eval(document.location.href.substring(document.location.href.indexOf("default=")+8))

The following example shows a Pug template being constructed from user input, allowing attackers to run arbitrary code via a payload such as #{global.process.exit(1)}.

const express = require('express')
var pug = require('pug');
const app = express()

app.post('/', (req, res) => {
    var input = req.query.username;
    var template = `
doctype
html
head
    title= 'Hello world'
body
    form(action='/' method='post')
        input#name.form-control(type='text)
        button.btn.btn-primary(type='submit') Submit
    p Hello `+ input
    var fn = pug.compile(template);
    var html = fn();
    res.send(html);
})

Below is an example of how to use a template engine without any risk of template injection. The user input is included via an interpolation expression #{username} whose value is provided as an option to the template, instead of being part of the template string itself:

const express = require('express')
var pug = require('pug');
const app = express()

app.post('/', (req, res) => {
    var input = req.query.username;
    var template = `
doctype
html
head
    title= 'Hello world'
body
    form(action='/' method='post')
        input#name.form-control(type='text)
        button.btn.btn-primary(type='submit') Submit
    p Hello #{username}`
    var fn = pug.compile(template);
    var html = fn({username: input});
    res.send(html);
})

References

@erik-krogh
Copy link
Contributor

The Security/CWE-094-dataURL/CodeInjection.qlref test is failing in the CI.

I get the below diff, can you look into that?

-| test.js:7:16:7:25 | payloadURL | test.js:5:21:5:44 | req.que ... rameter | test.js:7:16:7:25 | payloadURL | payloadURL depends on a $@. | test.js:5:21:5:44 | req.que ... rameter | user-provided value |
-| test.js:10:16:10:25 | payloadURL | test.js:5:21:5:44 | req.que ... rameter | test.js:10:16:10:25 | payloadURL | payloadURL depends on a $@. | test.js:5:21:5:44 | req.que ... rameter | user-provided value |
-| test.js:18:18:18:24 | payload | test.js:17:21:17:44 | req.que ... rameter | test.js:18:18:18:24 | payload | payload depends on a $@. | test.js:17:21:17:44 | req.que ... rameter | user-provided value |
-| test.js:19:18:19:30 | payload + sth | test.js:17:21:17:44 | req.que ... rameter | test.js:19:18:19:30 | payload + sth | payload + sth depends on a $@. | test.js:17:21:17:44 | req.que ... rameter | user-provided value |
+| test.js:7:16:7:25 | payloadURL | test.js:5:21:5:44 | req.que ... rameter | test.js:7:16:7:25 | payloadURL | payloadURLThis command line depends on a $@. | test.js:5:21:5:44 | req.que ... rameter | user-provided value |
+| test.js:10:16:10:25 | payloadURL | test.js:5:21:5:44 | req.que ... rameter | test.js:10:16:10:25 | payloadURL | payloadURLThis command line depends on a $@. | test.js:5:21:5:44 | req.que ... rameter | user-provided value |
+| test.js:18:18:18:24 | payload | test.js:17:21:17:44 | req.que ... rameter | test.js:18:18:18:24 | payload | payloadThis command line depends on a $@. | test.js:17:21:17:44 | req.que ... rameter | user-provided value |
+| test.js:19:18:19:30 | payload + sth | test.js:17:21:17:44 | req.que ... rameter | test.js:19:18:19:30 | payload + sth | payload + sthThis command line depends on a $@. | test.js:17:21:17:44 | req.que ... rameter | user-provided value |

It's from the select part of your query.
The string you're constructing is sink.getNode() + "This command line depends on a $@.". I think you should remove that first sink.getNode() + part.

@am0o0
Copy link
Contributor Author

am0o0 commented Jun 7, 2024

@erik-krogh sorry I should've run tests before pushing changes to remote.

@erik-krogh
Copy link
Contributor

The format check is failing on javascript/ql/src/experimental/Security/CWE-094-dataURL/CodeInjection.ql.

You can run codeql query format -i <path-to-ql-file> to run the autoformatter.

@erik-krogh erik-krogh merged commit 555d7e5 into github:main Jun 20, 2024
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants