circle-ir 3.134.0 → 3.136.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/passes/language-sources-pass.d.ts +37 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +431 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +317 -0
- package/package.json +1 -1
|
@@ -388,4 +388,41 @@ export declare function findJavaMongoNosqlInjectionFindings(code: string, file:
|
|
|
388
388
|
* a value transitively derived from Flask/Django/FastAPI request input.
|
|
389
389
|
*/
|
|
390
390
|
export declare function findPythonMongoengineWhereNosqlInjectionFindings(code: string, file: string): SastFinding[];
|
|
391
|
+
/**
|
|
392
|
+
* Sprint 85 detector (#189) — Java SSRF via `new URL(<tainted>)` →
|
|
393
|
+
* `.openStream()` / `.openConnection()` / `.getContent()` receiver chains
|
|
394
|
+
* that the configured `URL.openStream` / `URL.openConnection` sinks
|
|
395
|
+
* recognize but the cross-statement flow construction misses (URL value
|
|
396
|
+
* passes through an intermediate local variable). Also fires when the
|
|
397
|
+
* tainted URL is gated only by a weak allowlist — a scheme-only check
|
|
398
|
+
* such as `url.startsWith("https://")` is NOT a sanitizer because the
|
|
399
|
+
* host is still attacker-controlled.
|
|
400
|
+
*/
|
|
401
|
+
export declare function findJavaUrlOpenStreamSsrfFindings(code: string, file: string): SastFinding[];
|
|
402
|
+
/**
|
|
403
|
+
* Sprint 86 detector A (#189) — Python uncontrolled format-string.
|
|
404
|
+
*
|
|
405
|
+
* Two shapes:
|
|
406
|
+
* 1. `<tainted> % args` — old-style percent formatting
|
|
407
|
+
* 2. `<tainted>.format(args)` — `str.format` API
|
|
408
|
+
*
|
|
409
|
+
* When the format string itself comes from HTTP request input, an
|
|
410
|
+
* attacker can probe arbitrary attributes/items via `str.format`
|
|
411
|
+
* (`{0.__class__.__init__.__globals__}`) or crash the process via
|
|
412
|
+
* malformed `%` specifiers. CWE-134.
|
|
413
|
+
*/
|
|
414
|
+
export declare function findPythonTaintedFormatStringFindings(code: string, file: string): SastFinding[];
|
|
415
|
+
/**
|
|
416
|
+
* Sprint 86 detector B (#189) — JavaScript uncontrolled format-string
|
|
417
|
+
* via Node `util.format(<tainted>, ...args)`. `util.format` honours
|
|
418
|
+
* `%s`/`%d`/`%j`/`%O` specifiers; a tainted format string can fingerprint
|
|
419
|
+
* argument types and is the documented sink for CWE-134 in Node.
|
|
420
|
+
*/
|
|
421
|
+
export declare function findJsUtilFormatFormatStringFindings(code: string, file: string): SastFinding[];
|
|
422
|
+
/**
|
|
423
|
+
* Sprint 86 detector C (#189) — Python HTTP header CRLF injection via
|
|
424
|
+
* Flask/Werkzeug `response.headers['X-Custom'] = <tainted concat>` or
|
|
425
|
+
* `response.headers.add(...)` / `response.headers.set(...)`. CWE-113.
|
|
426
|
+
*/
|
|
427
|
+
export declare function findPythonHeaderCrlfInjectionFindings(code: string, file: string): SastFinding[];
|
|
391
428
|
//# sourceMappingURL=language-sources-pass.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"language-sources-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/language-sources-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAwB,WAAW,EAAO,MAAM,sBAAsB,CAAC;AAC3H,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAqB9E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;IA0C/B,CAAC;AAiCF,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,WAAW,EAAE,CAAC;IACjC,eAAe,EAAE,SAAS,EAAE,CAAC;IAC7B;;;;OAIG;IACH,oBAAoB,EAAE,cAAc,EAAE,CAAC;IACvC;;;OAGG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC;;;OAGG;IACH,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B;;;OAGG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAMD,qBAAa,mBAAoB,YAAW,YAAY,CAAC,qBAAqB,CAAC;IAC7E,QAAQ,CAAC,IAAI,sBAAsB;IACnC,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,qBAAqB;
|
|
1
|
+
{"version":3,"file":"language-sources-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/language-sources-pass.ts"],"names":[],"mappings":"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"}
|
|
@@ -271,6 +271,17 @@ export class LanguageSourcesPass {
|
|
|
271
271
|
for (const finding of findPythonMongoengineWhereNosqlInjectionFindings(code, graph.ir.meta.file)) {
|
|
272
272
|
ctx.addFinding(finding);
|
|
273
273
|
}
|
|
274
|
+
// Sprint 86 (#189): Python format_string — `<tainted> % args` and
|
|
275
|
+
// `<tainted>.format(args)` where the format template comes from
|
|
276
|
+
// an HTTP request extractor.
|
|
277
|
+
for (const finding of findPythonTaintedFormatStringFindings(code, graph.ir.meta.file)) {
|
|
278
|
+
ctx.addFinding(finding);
|
|
279
|
+
}
|
|
280
|
+
// Sprint 86 (#189): Python crlf / header injection —
|
|
281
|
+
// `response.headers['X-Custom'] = <tainted>` (Flask/Werkzeug).
|
|
282
|
+
for (const finding of findPythonHeaderCrlfInjectionFindings(code, graph.ir.meta.file)) {
|
|
283
|
+
ctx.addFinding(finding);
|
|
284
|
+
}
|
|
274
285
|
}
|
|
275
286
|
// -- Rust: safe-handler sanitizer detectors (cognium-dev #115 Sprint 31) --
|
|
276
287
|
if (language === 'rust') {
|
|
@@ -347,6 +358,11 @@ export class LanguageSourcesPass {
|
|
|
347
358
|
for (const finding of findJsIndirectEvalCodeInjectionFindings(code, graph.ir.meta.file)) {
|
|
348
359
|
ctx.addFinding(finding);
|
|
349
360
|
}
|
|
361
|
+
// Sprint 86 (#189): JS format_string — `util.format(<tainted>, ...)`
|
|
362
|
+
// where the format template comes from an HTTP request extractor.
|
|
363
|
+
for (const finding of findJsUtilFormatFormatStringFindings(code, graph.ir.meta.file)) {
|
|
364
|
+
ctx.addFinding(finding);
|
|
365
|
+
}
|
|
350
366
|
}
|
|
351
367
|
// -- Java: Sprint 73 (#216 Pattern A) — Jackson readValue / Gson
|
|
352
368
|
// fromJson recognized as ETE terminator (does not affect
|
|
@@ -375,6 +391,12 @@ export class LanguageSourcesPass {
|
|
|
375
391
|
for (const finding of findJavaMongoNosqlInjectionFindings(code, graph.ir.meta.file)) {
|
|
376
392
|
ctx.addFinding(finding);
|
|
377
393
|
}
|
|
394
|
+
// Sprint 85 (#189): Java ssrf — `new URL(<tainted>)` →
|
|
395
|
+
// `.openStream()` / `.openConnection()` / `.getContent()` receiver
|
|
396
|
+
// chains (basic_fetch + weak_allowlist variants).
|
|
397
|
+
for (const finding of findJavaUrlOpenStreamSsrfFindings(code, graph.ir.meta.file)) {
|
|
398
|
+
ctx.addFinding(finding);
|
|
399
|
+
}
|
|
378
400
|
}
|
|
379
401
|
// Sprint 70 (#151): cross-language env-secret → external-network exfiltration.
|
|
380
402
|
// Pattern findings only — no taint flow required (composed-flow shape that
|
|
@@ -6017,4 +6039,413 @@ export function findPythonMongoengineWhereNosqlInjectionFindings(code, file) {
|
|
|
6017
6039
|
}
|
|
6018
6040
|
return findings;
|
|
6019
6041
|
}
|
|
6042
|
+
/**
|
|
6043
|
+
* Sprint 85 detector (#189) — Java SSRF via `new URL(<tainted>)` →
|
|
6044
|
+
* `.openStream()` / `.openConnection()` / `.getContent()` receiver chains
|
|
6045
|
+
* that the configured `URL.openStream` / `URL.openConnection` sinks
|
|
6046
|
+
* recognize but the cross-statement flow construction misses (URL value
|
|
6047
|
+
* passes through an intermediate local variable). Also fires when the
|
|
6048
|
+
* tainted URL is gated only by a weak allowlist — a scheme-only check
|
|
6049
|
+
* such as `url.startsWith("https://")` is NOT a sanitizer because the
|
|
6050
|
+
* host is still attacker-controlled.
|
|
6051
|
+
*/
|
|
6052
|
+
export function findJavaUrlOpenStreamSsrfFindings(code, file) {
|
|
6053
|
+
const findings = [];
|
|
6054
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
6055
|
+
return findings;
|
|
6056
|
+
// Gate: require URL construction + a fetch-style method call.
|
|
6057
|
+
if (!/\bnew\s+URL\s*\(/.test(code) &&
|
|
6058
|
+
!/\bURI\s*\.\s*create\s*\(/.test(code)) {
|
|
6059
|
+
return findings;
|
|
6060
|
+
}
|
|
6061
|
+
if (!/\.\s*(?:openStream|openConnection|getContent)\s*\(/.test(code)) {
|
|
6062
|
+
return findings;
|
|
6063
|
+
}
|
|
6064
|
+
const lines = code.split('\n');
|
|
6065
|
+
const reqExtractRe = /\b\w+\s*\.\s*(?:getParameter|getParameterValues|getHeader|getHeaders|getCookies|getReader|getQueryString|getRequestURI|getInputStream|getPart|getParts)\s*\(/;
|
|
6066
|
+
// Whole-file 3-pass taint propagation. Tracks straightforward
|
|
6067
|
+
// `Type name = expr;` / `name = expr;` assignments. Notably
|
|
6068
|
+
// includes the `new URL(<tainted>)` wrapper — substring match on
|
|
6069
|
+
// the rhs is sufficient because no built-in URL constructor erases
|
|
6070
|
+
// taint.
|
|
6071
|
+
const taintedVars = new Set();
|
|
6072
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
6073
|
+
const before = taintedVars.size;
|
|
6074
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6075
|
+
const t = lines[i].trim();
|
|
6076
|
+
const a = t.match(/^(?:final\s+)?(?:[\w<>?,\[\]]+\s+)?(\w+)\s*=\s*(.+?);?$/);
|
|
6077
|
+
if (!a)
|
|
6078
|
+
continue;
|
|
6079
|
+
const lhs = a[1];
|
|
6080
|
+
const rhs = a[2];
|
|
6081
|
+
if (taintedVars.has(lhs))
|
|
6082
|
+
continue;
|
|
6083
|
+
if (reqExtractRe.test(rhs)) {
|
|
6084
|
+
taintedVars.add(lhs);
|
|
6085
|
+
continue;
|
|
6086
|
+
}
|
|
6087
|
+
for (const v of taintedVars) {
|
|
6088
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
6089
|
+
taintedVars.add(lhs);
|
|
6090
|
+
break;
|
|
6091
|
+
}
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
if (taintedVars.size === before)
|
|
6095
|
+
break;
|
|
6096
|
+
}
|
|
6097
|
+
if (taintedVars.size === 0)
|
|
6098
|
+
return findings;
|
|
6099
|
+
const sinkRe = /\.\s*(openStream|openConnection|getContent)\s*\(/;
|
|
6100
|
+
const seen = new Set();
|
|
6101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6102
|
+
const line = lines[i];
|
|
6103
|
+
const m = line.match(sinkRe);
|
|
6104
|
+
if (!m)
|
|
6105
|
+
continue;
|
|
6106
|
+
const op = m[1];
|
|
6107
|
+
const sinkIdx = line.indexOf(`.${op}`);
|
|
6108
|
+
if (sinkIdx < 0)
|
|
6109
|
+
continue;
|
|
6110
|
+
// Identify the receiver span: everything to the left of the `.op(`
|
|
6111
|
+
// call. The receiver may be a chained call (e.g.
|
|
6112
|
+
// `new URL(url).openStream()`) — treat the entire pre-call span as
|
|
6113
|
+
// the analysis target and ask whether any tainted var appears in it.
|
|
6114
|
+
const head = line.substring(0, sinkIdx + 1);
|
|
6115
|
+
let tainted = reqExtractRe.test(head);
|
|
6116
|
+
if (!tainted) {
|
|
6117
|
+
for (const v of taintedVars) {
|
|
6118
|
+
if (new RegExp(`\\b${v}\\b`).test(head)) {
|
|
6119
|
+
tainted = true;
|
|
6120
|
+
break;
|
|
6121
|
+
}
|
|
6122
|
+
}
|
|
6123
|
+
}
|
|
6124
|
+
if (!tainted)
|
|
6125
|
+
continue;
|
|
6126
|
+
const key = `${i + 1}:${op}`;
|
|
6127
|
+
if (seen.has(key))
|
|
6128
|
+
continue;
|
|
6129
|
+
seen.add(key);
|
|
6130
|
+
findings.push({
|
|
6131
|
+
id: `ssrf-${file}-${i + 1}-java-url-${op.toLowerCase()}`,
|
|
6132
|
+
pass: 'language-sources',
|
|
6133
|
+
category: 'security',
|
|
6134
|
+
rule_id: 'ssrf',
|
|
6135
|
+
cwe: 'CWE-918',
|
|
6136
|
+
severity: 'critical',
|
|
6137
|
+
level: 'error',
|
|
6138
|
+
message: `SSRF: Java \`URL.${op}()\` invoked on a URL derived from servlet ` +
|
|
6139
|
+
'request input. Even when an `if (url.startsWith("https://"))` ' +
|
|
6140
|
+
'guard is present, the host portion remains attacker-controlled — ' +
|
|
6141
|
+
'use a strict host allowlist (parse the URL, check `getHost()`) ' +
|
|
6142
|
+
'before issuing the request.',
|
|
6143
|
+
file,
|
|
6144
|
+
line: i + 1,
|
|
6145
|
+
snippet: line.trim(),
|
|
6146
|
+
});
|
|
6147
|
+
}
|
|
6148
|
+
return findings;
|
|
6149
|
+
}
|
|
6150
|
+
/**
|
|
6151
|
+
* Sprint 86 detector A (#189) — Python uncontrolled format-string.
|
|
6152
|
+
*
|
|
6153
|
+
* Two shapes:
|
|
6154
|
+
* 1. `<tainted> % args` — old-style percent formatting
|
|
6155
|
+
* 2. `<tainted>.format(args)` — `str.format` API
|
|
6156
|
+
*
|
|
6157
|
+
* When the format string itself comes from HTTP request input, an
|
|
6158
|
+
* attacker can probe arbitrary attributes/items via `str.format`
|
|
6159
|
+
* (`{0.__class__.__init__.__globals__}`) or crash the process via
|
|
6160
|
+
* malformed `%` specifiers. CWE-134.
|
|
6161
|
+
*/
|
|
6162
|
+
export function findPythonTaintedFormatStringFindings(code, file) {
|
|
6163
|
+
const findings = [];
|
|
6164
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
6165
|
+
return findings;
|
|
6166
|
+
// Gate: the file must use a Flask/Django/FastAPI request extractor.
|
|
6167
|
+
if (!/\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)|flask\.request)\b/.test(code)) {
|
|
6168
|
+
return findings;
|
|
6169
|
+
}
|
|
6170
|
+
const lines = code.split('\n');
|
|
6171
|
+
const reqExtractRe = /\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)\b|flask\.request\b)/;
|
|
6172
|
+
// 3-pass whole-file taint propagation across simple `name = expr`
|
|
6173
|
+
// assignments.
|
|
6174
|
+
const taintedVars = new Set();
|
|
6175
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
6176
|
+
const before = taintedVars.size;
|
|
6177
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6178
|
+
const t = lines[i].trim();
|
|
6179
|
+
if (t.startsWith('#'))
|
|
6180
|
+
continue;
|
|
6181
|
+
const a = t.match(/^(\w+)\s*=\s*(.+?)$/);
|
|
6182
|
+
if (!a)
|
|
6183
|
+
continue;
|
|
6184
|
+
const lhs = a[1];
|
|
6185
|
+
const rhs = a[2];
|
|
6186
|
+
if (taintedVars.has(lhs))
|
|
6187
|
+
continue;
|
|
6188
|
+
if (reqExtractRe.test(rhs)) {
|
|
6189
|
+
taintedVars.add(lhs);
|
|
6190
|
+
continue;
|
|
6191
|
+
}
|
|
6192
|
+
for (const v of taintedVars) {
|
|
6193
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
6194
|
+
taintedVars.add(lhs);
|
|
6195
|
+
break;
|
|
6196
|
+
}
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
if (taintedVars.size === before)
|
|
6200
|
+
break;
|
|
6201
|
+
}
|
|
6202
|
+
if (taintedVars.size === 0)
|
|
6203
|
+
return findings;
|
|
6204
|
+
// Percent format: `<name> % <rhs>` where `<name>` is tainted and not a
|
|
6205
|
+
// string literal. Avoid false-fire on `2 % x` etc. by gating on
|
|
6206
|
+
// taintedVar appearing as the LHS of `%`.
|
|
6207
|
+
const percentRe = /\b(\w+)\s*%\s*[\(\[\{"'\w]/;
|
|
6208
|
+
// str.format(...): `<name>.format(`
|
|
6209
|
+
const dotFormatRe = /\b(\w+)\s*\.\s*format\s*\(/;
|
|
6210
|
+
const seen = new Set();
|
|
6211
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6212
|
+
const line = lines[i];
|
|
6213
|
+
const t = line.trim();
|
|
6214
|
+
if (t.startsWith('#'))
|
|
6215
|
+
continue;
|
|
6216
|
+
// Percent format
|
|
6217
|
+
const pm = line.match(percentRe);
|
|
6218
|
+
if (pm && taintedVars.has(pm[1])) {
|
|
6219
|
+
// Avoid string literal LHS (already a literal would have been
|
|
6220
|
+
// assigned as a literal, but we double-check the var was tainted —
|
|
6221
|
+
// which the check above asserts).
|
|
6222
|
+
const key = `${i + 1}:percent`;
|
|
6223
|
+
if (!seen.has(key)) {
|
|
6224
|
+
seen.add(key);
|
|
6225
|
+
findings.push({
|
|
6226
|
+
id: `format_string-${file}-${i + 1}-py-percent`,
|
|
6227
|
+
pass: 'language-sources',
|
|
6228
|
+
category: 'security',
|
|
6229
|
+
rule_id: 'format_string',
|
|
6230
|
+
cwe: 'CWE-134',
|
|
6231
|
+
severity: 'high',
|
|
6232
|
+
level: 'error',
|
|
6233
|
+
message: 'Format-string injection: Python `<tainted> % args` uses an ' +
|
|
6234
|
+
'HTTP-request-controlled format string. Malformed specifiers can ' +
|
|
6235
|
+
'crash the handler (TypeError) and `%(name)s` access can reveal ' +
|
|
6236
|
+
'arbitrary mapping keys. Use a literal format string and pass ' +
|
|
6237
|
+
'untrusted values as arguments only.',
|
|
6238
|
+
file,
|
|
6239
|
+
line: i + 1,
|
|
6240
|
+
snippet: line.trim(),
|
|
6241
|
+
});
|
|
6242
|
+
}
|
|
6243
|
+
}
|
|
6244
|
+
// str.format
|
|
6245
|
+
const dm = line.match(dotFormatRe);
|
|
6246
|
+
if (dm && taintedVars.has(dm[1])) {
|
|
6247
|
+
const key = `${i + 1}:dotformat`;
|
|
6248
|
+
if (!seen.has(key)) {
|
|
6249
|
+
seen.add(key);
|
|
6250
|
+
findings.push({
|
|
6251
|
+
id: `format_string-${file}-${i + 1}-py-strformat`,
|
|
6252
|
+
pass: 'language-sources',
|
|
6253
|
+
category: 'security',
|
|
6254
|
+
rule_id: 'format_string',
|
|
6255
|
+
cwe: 'CWE-134',
|
|
6256
|
+
severity: 'high',
|
|
6257
|
+
level: 'error',
|
|
6258
|
+
message: 'Format-string injection: Python `<tainted>.format(...)` lets ' +
|
|
6259
|
+
'the attacker control the format template. Field-access shapes ' +
|
|
6260
|
+
'such as `{0.__class__.__init__.__globals__}` can leak module ' +
|
|
6261
|
+
'globals (e.g. secret keys). Use a literal template and pass ' +
|
|
6262
|
+
'untrusted values as positional/keyword arguments only.',
|
|
6263
|
+
file,
|
|
6264
|
+
line: i + 1,
|
|
6265
|
+
snippet: line.trim(),
|
|
6266
|
+
});
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
return findings;
|
|
6271
|
+
}
|
|
6272
|
+
/**
|
|
6273
|
+
* Sprint 86 detector B (#189) — JavaScript uncontrolled format-string
|
|
6274
|
+
* via Node `util.format(<tainted>, ...args)`. `util.format` honours
|
|
6275
|
+
* `%s`/`%d`/`%j`/`%O` specifiers; a tainted format string can fingerprint
|
|
6276
|
+
* argument types and is the documented sink for CWE-134 in Node.
|
|
6277
|
+
*/
|
|
6278
|
+
export function findJsUtilFormatFormatStringFindings(code, file) {
|
|
6279
|
+
const findings = [];
|
|
6280
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
6281
|
+
return findings;
|
|
6282
|
+
if (!/\butil\s*\.\s*format\s*\(/.test(code))
|
|
6283
|
+
return findings;
|
|
6284
|
+
if (!/\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/.test(code)) {
|
|
6285
|
+
return findings;
|
|
6286
|
+
}
|
|
6287
|
+
const lines = code.split('\n');
|
|
6288
|
+
const reqExtractRe = /\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/;
|
|
6289
|
+
const taintedVars = new Set();
|
|
6290
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
6291
|
+
const before = taintedVars.size;
|
|
6292
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6293
|
+
const t = lines[i].trim();
|
|
6294
|
+
if (t.startsWith('//'))
|
|
6295
|
+
continue;
|
|
6296
|
+
const a = t.match(/^(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?$/);
|
|
6297
|
+
if (!a)
|
|
6298
|
+
continue;
|
|
6299
|
+
const lhs = a[1];
|
|
6300
|
+
const rhs = a[2];
|
|
6301
|
+
if (taintedVars.has(lhs))
|
|
6302
|
+
continue;
|
|
6303
|
+
if (reqExtractRe.test(rhs)) {
|
|
6304
|
+
taintedVars.add(lhs);
|
|
6305
|
+
continue;
|
|
6306
|
+
}
|
|
6307
|
+
for (const v of taintedVars) {
|
|
6308
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
6309
|
+
taintedVars.add(lhs);
|
|
6310
|
+
break;
|
|
6311
|
+
}
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
if (taintedVars.size === before)
|
|
6315
|
+
break;
|
|
6316
|
+
}
|
|
6317
|
+
if (taintedVars.size === 0)
|
|
6318
|
+
return findings;
|
|
6319
|
+
// util.format(<firstArg>, ...) — check whether <firstArg> is tainted.
|
|
6320
|
+
const callRe = /\butil\s*\.\s*format\s*\(\s*([\w.]+)\s*[,)]/;
|
|
6321
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6322
|
+
const line = lines[i];
|
|
6323
|
+
const m = line.match(callRe);
|
|
6324
|
+
if (!m)
|
|
6325
|
+
continue;
|
|
6326
|
+
const arg = m[1];
|
|
6327
|
+
// First token of dotted path is the var name.
|
|
6328
|
+
const root = arg.split('.')[0];
|
|
6329
|
+
if (!taintedVars.has(root) && !reqExtractRe.test(arg))
|
|
6330
|
+
continue;
|
|
6331
|
+
findings.push({
|
|
6332
|
+
id: `format_string-${file}-${i + 1}-js-util-format`,
|
|
6333
|
+
pass: 'language-sources',
|
|
6334
|
+
category: 'security',
|
|
6335
|
+
rule_id: 'format_string',
|
|
6336
|
+
cwe: 'CWE-134',
|
|
6337
|
+
severity: 'medium',
|
|
6338
|
+
level: 'error',
|
|
6339
|
+
message: 'Format-string injection: Node `util.format(<tainted>, ...)` uses ' +
|
|
6340
|
+
'an HTTP-request-controlled format string. The attacker can ' +
|
|
6341
|
+
'manipulate `%s`/`%j`/`%O` specifiers to alter the rendered ' +
|
|
6342
|
+
'output. Pass user input as a subsequent argument with a literal ' +
|
|
6343
|
+
'format string.',
|
|
6344
|
+
file,
|
|
6345
|
+
line: i + 1,
|
|
6346
|
+
snippet: line.trim(),
|
|
6347
|
+
});
|
|
6348
|
+
}
|
|
6349
|
+
return findings;
|
|
6350
|
+
}
|
|
6351
|
+
/**
|
|
6352
|
+
* Sprint 86 detector C (#189) — Python HTTP header CRLF injection via
|
|
6353
|
+
* Flask/Werkzeug `response.headers['X-Custom'] = <tainted concat>` or
|
|
6354
|
+
* `response.headers.add(...)` / `response.headers.set(...)`. CWE-113.
|
|
6355
|
+
*/
|
|
6356
|
+
export function findPythonHeaderCrlfInjectionFindings(code, file) {
|
|
6357
|
+
const findings = [];
|
|
6358
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
6359
|
+
return findings;
|
|
6360
|
+
if (!/\b\w+\s*\.\s*headers\b/.test(code))
|
|
6361
|
+
return findings;
|
|
6362
|
+
if (!/\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)|flask\.request)\b/.test(code)) {
|
|
6363
|
+
return findings;
|
|
6364
|
+
}
|
|
6365
|
+
const lines = code.split('\n');
|
|
6366
|
+
const reqExtractRe = /\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)\b|flask\.request\b)/;
|
|
6367
|
+
const taintedVars = new Set();
|
|
6368
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
6369
|
+
const before = taintedVars.size;
|
|
6370
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6371
|
+
const t = lines[i].trim();
|
|
6372
|
+
if (t.startsWith('#'))
|
|
6373
|
+
continue;
|
|
6374
|
+
const a = t.match(/^(\w+)\s*=\s*(.+?)$/);
|
|
6375
|
+
if (!a)
|
|
6376
|
+
continue;
|
|
6377
|
+
const lhs = a[1];
|
|
6378
|
+
const rhs = a[2];
|
|
6379
|
+
if (taintedVars.has(lhs))
|
|
6380
|
+
continue;
|
|
6381
|
+
if (reqExtractRe.test(rhs)) {
|
|
6382
|
+
taintedVars.add(lhs);
|
|
6383
|
+
continue;
|
|
6384
|
+
}
|
|
6385
|
+
for (const v of taintedVars) {
|
|
6386
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
6387
|
+
taintedVars.add(lhs);
|
|
6388
|
+
break;
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
if (taintedVars.size === before)
|
|
6393
|
+
break;
|
|
6394
|
+
}
|
|
6395
|
+
if (taintedVars.size === 0)
|
|
6396
|
+
return findings;
|
|
6397
|
+
// Patterns:
|
|
6398
|
+
// resp.headers['X-Custom'] = <expr>
|
|
6399
|
+
// resp.headers.add('X-Custom', <expr>)
|
|
6400
|
+
// resp.headers.set('X-Custom', <expr>)
|
|
6401
|
+
// resp.headers.update({'X-Custom': <expr>})
|
|
6402
|
+
const subscriptRe = /\b\w+\s*\.\s*headers\s*\[\s*['"][^'"]+['"]\s*\]\s*=\s*(.+)$/;
|
|
6403
|
+
const methodRe = /\b\w+\s*\.\s*headers\s*\.\s*(?:add|set|setdefault|append)\s*\(\s*['"][^'"]+['"]\s*,\s*(.+?)\)/;
|
|
6404
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6405
|
+
const line = lines[i];
|
|
6406
|
+
const t = line.trim();
|
|
6407
|
+
if (t.startsWith('#'))
|
|
6408
|
+
continue;
|
|
6409
|
+
let expr = null;
|
|
6410
|
+
const sm = line.match(subscriptRe);
|
|
6411
|
+
if (sm)
|
|
6412
|
+
expr = sm[1];
|
|
6413
|
+
else {
|
|
6414
|
+
const mm = line.match(methodRe);
|
|
6415
|
+
if (mm)
|
|
6416
|
+
expr = mm[1];
|
|
6417
|
+
}
|
|
6418
|
+
if (!expr)
|
|
6419
|
+
continue;
|
|
6420
|
+
let tainted = reqExtractRe.test(expr);
|
|
6421
|
+
if (!tainted) {
|
|
6422
|
+
for (const v of taintedVars) {
|
|
6423
|
+
if (new RegExp(`\\b${v}\\b`).test(expr)) {
|
|
6424
|
+
tainted = true;
|
|
6425
|
+
break;
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
6429
|
+
if (!tainted)
|
|
6430
|
+
continue;
|
|
6431
|
+
findings.push({
|
|
6432
|
+
id: `crlf-${file}-${i + 1}-py-headers`,
|
|
6433
|
+
pass: 'language-sources',
|
|
6434
|
+
category: 'security',
|
|
6435
|
+
rule_id: 'crlf',
|
|
6436
|
+
cwe: 'CWE-113',
|
|
6437
|
+
severity: 'medium',
|
|
6438
|
+
level: 'error',
|
|
6439
|
+
message: 'CRLF / header injection: Python Flask/Werkzeug ' +
|
|
6440
|
+
'`response.headers[...] = <tainted>` lets an attacker inject a ' +
|
|
6441
|
+
'`\\r\\n` sequence and forge additional headers or split the ' +
|
|
6442
|
+
'response body. Validate the value (reject control characters) or ' +
|
|
6443
|
+
'use a fixed allowlist.',
|
|
6444
|
+
file,
|
|
6445
|
+
line: i + 1,
|
|
6446
|
+
snippet: line.trim(),
|
|
6447
|
+
});
|
|
6448
|
+
}
|
|
6449
|
+
return findings;
|
|
6450
|
+
}
|
|
6020
6451
|
//# sourceMappingURL=language-sources-pass.js.map
|