Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The server-side and client-side request forgery queries (`js/request-forgery` and `js/client-side-request-forgery`) now treat the second (`base`) argument of the `URL` constructor as a taint step. This can reveal previously missed alerts in code such as `new URL("/path", base)`, where `base` is user-controlled.
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,45 @@ module RequestForgery {
override string getKind() { result = kind }
}

/** Gets a call to the `URL` constructor, either the global or the one from `url`. */
private DataFlow::NewNode urlConstructorCall() {
result = DataFlow::globalVarRef("URL").getAnInstantiation()
or
result = API::moduleImport("url").getMember("URL").getAnInstantiation()
}

/**
* Holds if `nd` is an absolute URL, that is, a string with a URL scheme such as
* `https://example.com`, or a `URL` object, which is always absolute.
*/
private predicate isAbsoluteUrl(DataFlow::Node nd) {
nd.getStringValue().regexpMatch("(?i)[a-z][a-z0-9+.-]*:.*")
or
nd.getALocalSource() = urlConstructorCall()
}

/**
* Holds if there is a taint step from `pred` to `succ` for request forgery.
*/
predicate isAdditionalRequestForgeryStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::NewNode url | url = DataFlow::globalVarRef("URL").getAnInstantiation() |
succ = url and
pred = url.getArgument(0)
(
pred = url.getArgument(0)
or
pred = url.getArgument(1) and not isAbsoluteUrl(url.getArgument(0))
)
)
or
exists(DataFlow::NewNode url |
url = API::moduleImport("url").getMember("URL").getAnInstantiation()
|
succ = url and
pred = url.getArgument(0)
(
pred = url.getArgument(0)
or
pred = url.getArgument(1) and not isAbsoluteUrl(url.getArgument(0))
)
)
or
exists(HtmlSanitizerCall call |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@
| serverSide.js:143:5:143:26 | axios.g ... t.href) | serverSide.js:139:19:139:31 | req.query.url | serverSide.js:143:15:143:25 | target.href | The $@ of this request depends on a $@. | serverSide.js:143:15:143:25 | target.href | URL | serverSide.js:139:19:139:31 | req.query.url | user-provided value |
| serverSide.js:145:5:145:25 | axios.g ... dedUrl) | serverSide.js:139:19:139:31 | req.query.url | serverSide.js:145:15:145:24 | encodedUrl | The $@ of this request depends on a $@. | serverSide.js:145:15:145:24 | encodedUrl | URL | serverSide.js:139:19:139:31 | req.query.url | user-provided value |
| serverSide.js:147:5:147:25 | axios.g ... pedUrl) | serverSide.js:139:19:139:31 | req.query.url | serverSide.js:147:15:147:24 | escapedUrl | The $@ of this request depends on a $@. | serverSide.js:147:15:147:24 | escapedUrl | URL | serverSide.js:139:19:139:31 | req.query.url | user-provided value |
| serverSide.js:151:1:151:15 | request(custom) | serverSide.js:150:16:150:51 | require ... ource() | serverSide.js:151:9:151:14 | custom | The $@ of this request depends on a $@. | serverSide.js:151:9:151:14 | custom | URL | serverSide.js:150:16:150:51 | require ... ource() | user-provided value |
| serverSide.js:153:5:153:26 | axios.g ... t.href) | serverSide.js:151:18:151:31 | req.query.base | serverSide.js:153:15:153:25 | target.href | The $@ of this request depends on a $@. | serverSide.js:153:15:153:25 | target.href | URL | serverSide.js:151:18:151:31 | req.query.base | user-provided value |
| serverSide.js:160:5:160:26 | axios.g ... t.href) | serverSide.js:158:18:158:31 | req.query.base | serverSide.js:160:15:160:25 | target.href | The $@ of this request depends on a $@. | serverSide.js:160:15:160:25 | target.href | URL | serverSide.js:158:18:158:31 | req.query.base | user-provided value |
| serverSide.js:184:5:184:26 | axios.g ... t.href) | serverSide.js:182:25:182:37 | req.query.url | serverSide.js:184:15:184:25 | target.href | The $@ of this request depends on a $@. | serverSide.js:184:15:184:25 | target.href | URL | serverSide.js:182:25:182:37 | req.query.url | user-provided value |
| serverSide.js:188:1:188:15 | request(custom) | serverSide.js:187:16:187:51 | require ... ource() | serverSide.js:188:9:188:14 | custom | The $@ of this request depends on a $@. | serverSide.js:188:9:188:14 | custom | URL | serverSide.js:187:16:187:51 | require ... ource() | user-provided value |
| vercel.ts:5:26:5:35 | fetch(url) | vercel.ts:4:15:4:23 | req.query | vercel.ts:5:32:5:34 | url | The $@ of this request depends on a $@. | vercel.ts:5:32:5:34 | url | URL | vercel.ts:4:15:4:23 | req.query | user-provided value |
| vercel.ts:5:26:5:35 | fetch(url) | vercel.ts:4:15:4:27 | req.query.url | vercel.ts:5:32:5:34 | url | The $@ of this request depends on a $@. | vercel.ts:5:32:5:34 | url | URL | vercel.ts:4:15:4:27 | req.query.url | user-provided value |
edges
Expand Down Expand Up @@ -147,8 +150,27 @@ edges
| serverSide.js:146:11:146:20 | escapedUrl | serverSide.js:147:15:147:24 | escapedUrl | provenance | |
| serverSide.js:146:24:146:36 | escape(input) | serverSide.js:146:11:146:20 | escapedUrl | provenance | |
| serverSide.js:146:31:146:35 | input | serverSide.js:146:24:146:36 | escape(input) | provenance | |
| serverSide.js:150:7:150:12 | custom | serverSide.js:151:9:151:14 | custom | provenance | |
| serverSide.js:150:16:150:51 | require ... ource() | serverSide.js:150:7:150:12 | custom | provenance | |
| serverSide.js:151:11:151:14 | base | serverSide.js:152:37:152:40 | base | provenance | |
| serverSide.js:151:18:151:31 | req.query.base | serverSide.js:151:11:151:14 | base | provenance | |
| serverSide.js:152:11:152:16 | target | serverSide.js:153:15:153:20 | target | provenance | |
| serverSide.js:152:20:152:41 | new URL ... , base) | serverSide.js:152:11:152:16 | target | provenance | |
| serverSide.js:152:37:152:40 | base | serverSide.js:152:20:152:41 | new URL ... , base) | provenance | Config |
| serverSide.js:153:15:153:20 | target | serverSide.js:153:15:153:25 | target.href | provenance | |
| serverSide.js:158:11:158:14 | base | serverSide.js:159:37:159:40 | base | provenance | |
| serverSide.js:158:18:158:31 | req.query.base | serverSide.js:158:11:158:14 | base | provenance | |
| serverSide.js:159:11:159:16 | target | serverSide.js:160:15:160:20 | target | provenance | |
| serverSide.js:159:20:159:41 | new URL ... , base) | serverSide.js:159:11:159:16 | target | provenance | |
| serverSide.js:159:37:159:40 | base | serverSide.js:159:20:159:41 | new URL ... , base) | provenance | Config |
| serverSide.js:160:15:160:20 | target | serverSide.js:160:15:160:25 | target.href | provenance | |
| serverSide.js:182:11:182:13 | abs | serverSide.js:183:28:183:30 | abs | provenance | |
| serverSide.js:182:17:182:38 | new URL ... ry.url) | serverSide.js:182:11:182:13 | abs | provenance | |
| serverSide.js:182:25:182:37 | req.query.url | serverSide.js:182:17:182:38 | new URL ... ry.url) | provenance | Config |
| serverSide.js:183:11:183:16 | target | serverSide.js:184:15:184:20 | target | provenance | |
| serverSide.js:183:20:183:56 | new URL ... ample') | serverSide.js:183:11:183:16 | target | provenance | |
| serverSide.js:183:28:183:30 | abs | serverSide.js:183:20:183:56 | new URL ... ample') | provenance | Config |
| serverSide.js:184:15:184:20 | target | serverSide.js:184:15:184:25 | target.href | provenance | |
| serverSide.js:187:7:187:12 | custom | serverSide.js:188:9:188:14 | custom | provenance | |
| serverSide.js:187:16:187:51 | require ... ource() | serverSide.js:187:7:187:12 | custom | provenance | |
| vercel.ts:4:9:4:11 | url | vercel.ts:5:32:5:34 | url | provenance | |
| vercel.ts:4:15:4:23 | req.query | vercel.ts:4:9:4:11 | url | provenance | |
| vercel.ts:4:15:4:27 | req.query.url | vercel.ts:4:9:4:11 | url | provenance | |
Expand Down Expand Up @@ -279,9 +301,31 @@ nodes
| serverSide.js:146:24:146:36 | escape(input) | semmle.label | escape(input) |
| serverSide.js:146:31:146:35 | input | semmle.label | input |
| serverSide.js:147:15:147:24 | escapedUrl | semmle.label | escapedUrl |
| serverSide.js:150:7:150:12 | custom | semmle.label | custom |
| serverSide.js:150:16:150:51 | require ... ource() | semmle.label | require ... ource() |
| serverSide.js:151:9:151:14 | custom | semmle.label | custom |
| serverSide.js:151:11:151:14 | base | semmle.label | base |
| serverSide.js:151:18:151:31 | req.query.base | semmle.label | req.query.base |
| serverSide.js:152:11:152:16 | target | semmle.label | target |
| serverSide.js:152:20:152:41 | new URL ... , base) | semmle.label | new URL ... , base) |
| serverSide.js:152:37:152:40 | base | semmle.label | base |
| serverSide.js:153:15:153:20 | target | semmle.label | target |
| serverSide.js:153:15:153:25 | target.href | semmle.label | target.href |
| serverSide.js:158:11:158:14 | base | semmle.label | base |
| serverSide.js:158:18:158:31 | req.query.base | semmle.label | req.query.base |
| serverSide.js:159:11:159:16 | target | semmle.label | target |
| serverSide.js:159:20:159:41 | new URL ... , base) | semmle.label | new URL ... , base) |
| serverSide.js:159:37:159:40 | base | semmle.label | base |
| serverSide.js:160:15:160:20 | target | semmle.label | target |
| serverSide.js:160:15:160:25 | target.href | semmle.label | target.href |
| serverSide.js:182:11:182:13 | abs | semmle.label | abs |
| serverSide.js:182:17:182:38 | new URL ... ry.url) | semmle.label | new URL ... ry.url) |
| serverSide.js:182:25:182:37 | req.query.url | semmle.label | req.query.url |
| serverSide.js:183:11:183:16 | target | semmle.label | target |
| serverSide.js:183:20:183:56 | new URL ... ample') | semmle.label | new URL ... ample') |
| serverSide.js:183:28:183:30 | abs | semmle.label | abs |
| serverSide.js:184:15:184:20 | target | semmle.label | target |
| serverSide.js:184:15:184:25 | target.href | semmle.label | target.href |
| serverSide.js:187:7:187:12 | custom | semmle.label | custom |
| serverSide.js:187:16:187:51 | require ... ource() | semmle.label | require ... ource() |
| serverSide.js:188:9:188:14 | custom | semmle.label | custom |
| vercel.ts:4:9:4:11 | url | semmle.label | url |
| vercel.ts:4:15:4:23 | req.query | semmle.label | req.query |
| vercel.ts:4:15:4:27 | req.query.url | semmle.label | req.query.url |
Expand Down
37 changes: 37 additions & 0 deletions javascript/ql/test/query-tests/Security/CWE-918/serverSide.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,42 @@ var server2 = http.createServer(function (req, res) {
axios.get(escapedUrl); // $ Alert[js/request-forgery]
});

var server3 = http.createServer(function (req, res) {
const base = req.query.base; // $ Source[js/request-forgery]
const target = new URL('/path', base);
axios.get(target.href); // $ Alert[js/request-forgery]
});

var server4 = http.createServer(function (req, res) {
const { URL } = require('url');
const base = req.query.base; // $ Source[js/request-forgery]
const target = new URL('/path', base);
axios.get(target.href); // $ Alert[js/request-forgery]
});

var server5 = http.createServer(function (req, res) {
const target = new URL('https://fixed.example/path', req.query.base);
axios.get(target.href);
});

var server6 = http.createServer(function (req, res) {
const abs = new URL('https://fixed.example/path');
const target = new URL(abs, req.query.base);
axios.get(target.href);
});

var server7 = http.createServer(function (req, res) {
const { URL } = require('url');
const abs = new URL('https://fixed.example/path');
const target = new URL(abs, req.query.base);
axios.get(target.href);
});

var server8 = http.createServer(function (req, res) {
const abs = new URL(req.query.url); // $ Source[js/request-forgery]
const target = new URL(abs, 'https://fixed.example');
axios.get(target.href); // $ Alert[js/request-forgery]
});

const custom = require('testlib').getServerSource(); // $ Source[js/request-forgery]
request(custom); // $ Alert[js/request-forgery]