circle-ir 3.137.0 → 3.138.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 +72 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +580 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +432 -0
- package/package.json +1 -1
|
@@ -233,6 +233,16 @@ export class LanguageSourcesPass {
|
|
|
233
233
|
for (const finding of findGoLdapInjectionFindings(code, graph.ir.meta.file)) {
|
|
234
234
|
ctx.addFinding(finding);
|
|
235
235
|
}
|
|
236
|
+
// Sprint 89 (#189): Go insecure_deserialization — encoding/gob
|
|
237
|
+
// NewDecoder(r.Body).Decode(&v).
|
|
238
|
+
for (const finding of findGoGobDeserializationFindings(code, graph.ir.meta.file)) {
|
|
239
|
+
ctx.addFinding(finding);
|
|
240
|
+
}
|
|
241
|
+
// Sprint 90 (#189): Go XXE — encoding/xml NewDecoder(r.Body) with
|
|
242
|
+
// Strict=false or custom Entity map.
|
|
243
|
+
for (const finding of findGoXmlDecoderXxeFindings(code, graph.ir.meta.file)) {
|
|
244
|
+
ctx.addFinding(finding);
|
|
245
|
+
}
|
|
236
246
|
}
|
|
237
247
|
// -- Python: safe-handler sanitizer detectors (cognium-dev #114 Sprint 31) --
|
|
238
248
|
if (language === 'python') {
|
|
@@ -287,6 +297,10 @@ export class LanguageSourcesPass {
|
|
|
287
297
|
for (const finding of findPythonHeaderCrlfInjectionFindings(code, graph.ir.meta.file)) {
|
|
288
298
|
ctx.addFinding(finding);
|
|
289
299
|
}
|
|
300
|
+
// Sprint 90 (#189): Python SSTI — jinja2.Template(<tainted>).render(...).
|
|
301
|
+
for (const finding of findPythonJinjaTemplateSstiFindings(code, graph.ir.meta.file)) {
|
|
302
|
+
ctx.addFinding(finding);
|
|
303
|
+
}
|
|
290
304
|
}
|
|
291
305
|
// -- Rust: safe-handler sanitizer detectors (cognium-dev #115 Sprint 31) --
|
|
292
306
|
if (language === 'rust') {
|
|
@@ -383,6 +397,21 @@ export class LanguageSourcesPass {
|
|
|
383
397
|
for (const finding of findJsLdapInjectionFindings(code, graph.ir.meta.file)) {
|
|
384
398
|
ctx.addFinding(finding);
|
|
385
399
|
}
|
|
400
|
+
// Sprint 89 (#189): JS insecure_deserialization —
|
|
401
|
+
// JSON.parse(req.body) on raw / text bodies.
|
|
402
|
+
for (const finding of findJsJsonParseBodyFindings(code, graph.ir.meta.file)) {
|
|
403
|
+
ctx.addFinding(finding);
|
|
404
|
+
}
|
|
405
|
+
// Sprint 89 (#189): JS xpath_injection — DOM
|
|
406
|
+
// document.evaluate(<tainted>, ...).
|
|
407
|
+
for (const finding of findJsDomXpathInjectionFindings(code, graph.ir.meta.file)) {
|
|
408
|
+
ctx.addFinding(finding);
|
|
409
|
+
}
|
|
410
|
+
// Sprint 90 (#189): JS template_injection — Handlebars.compile /
|
|
411
|
+
// ejs.render / ejs.compile with a tainted source.
|
|
412
|
+
for (const finding of findJsTemplateInjectionSstiFindings(code, graph.ir.meta.file)) {
|
|
413
|
+
ctx.addFinding(finding);
|
|
414
|
+
}
|
|
386
415
|
}
|
|
387
416
|
// -- Java: Sprint 73 (#216 Pattern A) — Jackson readValue / Gson
|
|
388
417
|
// fromJson recognized as ETE terminator (does not affect
|
|
@@ -7131,4 +7160,555 @@ export function findRustLogInjectionFindings(code, file) {
|
|
|
7131
7160
|
}
|
|
7132
7161
|
return findings;
|
|
7133
7162
|
}
|
|
7163
|
+
/**
|
|
7164
|
+
* Sprint 89 detector A (#189) — Go insecure deserialization via
|
|
7165
|
+
* `encoding/gob` `gob.NewDecoder(<req-body>).Decode(&v)`.
|
|
7166
|
+
*
|
|
7167
|
+
* The `gob` package will reconstruct arbitrary Go values, and any
|
|
7168
|
+
* registered concrete type can be instantiated by the attacker through
|
|
7169
|
+
* `gob.Register(...)` side effects. Decoding directly from
|
|
7170
|
+
* `r.Body` (`*http.Request`) without authenticated framing is unsafe.
|
|
7171
|
+
* CWE-502.
|
|
7172
|
+
*/
|
|
7173
|
+
export function findGoGobDeserializationFindings(code, file) {
|
|
7174
|
+
const findings = [];
|
|
7175
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
7176
|
+
return findings;
|
|
7177
|
+
if (!/\bgob\s*\.\s*NewDecoder\s*\(/.test(code))
|
|
7178
|
+
return findings;
|
|
7179
|
+
if (!/\*http\.Request\b/.test(code) && !/\bhttp\.HandlerFunc\b/.test(code)) {
|
|
7180
|
+
return findings;
|
|
7181
|
+
}
|
|
7182
|
+
const lines = code.split('\n');
|
|
7183
|
+
// Track variables holding a *gob.Decoder constructed from req body.
|
|
7184
|
+
const decoderFromBody = new Set();
|
|
7185
|
+
const newDecoderAssignRe = /^(\w+)\s*(?::=|=)\s*gob\s*\.\s*NewDecoder\s*\(\s*([^)]+)\)/;
|
|
7186
|
+
for (const raw of lines) {
|
|
7187
|
+
const t = raw.trim();
|
|
7188
|
+
if (t.startsWith('//'))
|
|
7189
|
+
continue;
|
|
7190
|
+
const m = t.match(newDecoderAssignRe);
|
|
7191
|
+
if (!m)
|
|
7192
|
+
continue;
|
|
7193
|
+
const lhs = m[1];
|
|
7194
|
+
const arg = m[2];
|
|
7195
|
+
if (/\b\w+\s*\.\s*Body\b/.test(arg)) {
|
|
7196
|
+
decoderFromBody.add(lhs);
|
|
7197
|
+
}
|
|
7198
|
+
}
|
|
7199
|
+
const seen = new Set();
|
|
7200
|
+
if (decoderFromBody.size > 0) {
|
|
7201
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7202
|
+
const line = lines[i];
|
|
7203
|
+
if (line.trim().startsWith('//'))
|
|
7204
|
+
continue;
|
|
7205
|
+
const m = line.match(/\b(\w+)\s*\.\s*Decode\s*\(/);
|
|
7206
|
+
if (!m)
|
|
7207
|
+
continue;
|
|
7208
|
+
if (!decoderFromBody.has(m[1]))
|
|
7209
|
+
continue;
|
|
7210
|
+
if (seen.has(i + 1))
|
|
7211
|
+
continue;
|
|
7212
|
+
seen.add(i + 1);
|
|
7213
|
+
findings.push({
|
|
7214
|
+
id: `insecure_deserialization-${file}-${i + 1}-go-gob`,
|
|
7215
|
+
pass: 'language-sources',
|
|
7216
|
+
category: 'security',
|
|
7217
|
+
rule_id: 'insecure_deserialization',
|
|
7218
|
+
cwe: 'CWE-502',
|
|
7219
|
+
severity: 'critical',
|
|
7220
|
+
level: 'error',
|
|
7221
|
+
message: 'Insecure deserialization: `gob.NewDecoder(req.Body).Decode(...)` ' +
|
|
7222
|
+
'reconstructs arbitrary registered Go types from attacker-controlled ' +
|
|
7223
|
+
'bytes. Use an authenticated framing (signed/MAC payloads), avoid ' +
|
|
7224
|
+
'decoding interface{} values, or switch to a schema-bound format ' +
|
|
7225
|
+
'(JSON with explicit types, Protocol Buffers).',
|
|
7226
|
+
file,
|
|
7227
|
+
line: i + 1,
|
|
7228
|
+
snippet: line.trim(),
|
|
7229
|
+
});
|
|
7230
|
+
}
|
|
7231
|
+
}
|
|
7232
|
+
// Inline form — `gob.NewDecoder(req.Body).Decode(&v)`.
|
|
7233
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7234
|
+
const line = lines[i];
|
|
7235
|
+
if (line.trim().startsWith('//'))
|
|
7236
|
+
continue;
|
|
7237
|
+
if (!/\bgob\s*\.\s*NewDecoder\s*\(\s*\w+\s*\.\s*Body\s*\)\s*\.\s*Decode\s*\(/.test(line)) {
|
|
7238
|
+
continue;
|
|
7239
|
+
}
|
|
7240
|
+
if (seen.has(i + 1))
|
|
7241
|
+
continue;
|
|
7242
|
+
seen.add(i + 1);
|
|
7243
|
+
findings.push({
|
|
7244
|
+
id: `insecure_deserialization-${file}-${i + 1}-go-gob-inline`,
|
|
7245
|
+
pass: 'language-sources',
|
|
7246
|
+
category: 'security',
|
|
7247
|
+
rule_id: 'insecure_deserialization',
|
|
7248
|
+
cwe: 'CWE-502',
|
|
7249
|
+
severity: 'critical',
|
|
7250
|
+
level: 'error',
|
|
7251
|
+
message: 'Insecure deserialization: `gob.NewDecoder(req.Body).Decode(...)` ' +
|
|
7252
|
+
'reconstructs arbitrary registered Go types from attacker-controlled ' +
|
|
7253
|
+
'bytes. Use an authenticated framing (signed/MAC payloads), avoid ' +
|
|
7254
|
+
'decoding interface{} values, or switch to a schema-bound format ' +
|
|
7255
|
+
'(JSON with explicit types, Protocol Buffers).',
|
|
7256
|
+
file,
|
|
7257
|
+
line: i + 1,
|
|
7258
|
+
snippet: line.trim(),
|
|
7259
|
+
});
|
|
7260
|
+
}
|
|
7261
|
+
return findings;
|
|
7262
|
+
}
|
|
7263
|
+
/**
|
|
7264
|
+
* Sprint 89 detector B (#189) — JS insecure deserialization via
|
|
7265
|
+
* `JSON.parse(req.body)`.
|
|
7266
|
+
*
|
|
7267
|
+
* Express applications that register `express.text(...)` or
|
|
7268
|
+
* `bodyParser.raw(...)` middleware leave `req.body` as an
|
|
7269
|
+
* attacker-controlled string. Passing it through `JSON.parse(...)`
|
|
7270
|
+
* exposes prototype-pollution paths if the parsed object is then
|
|
7271
|
+
* merged into trusted state or used as a property bag. CWE-502 /
|
|
7272
|
+
* CWE-1321 (prototype-pollution adjacent).
|
|
7273
|
+
*
|
|
7274
|
+
* Conservative gate: only fire on `JSON.parse(<expr>)` where
|
|
7275
|
+
* `<expr>` resolves to `req.body` (the only express extractor that
|
|
7276
|
+
* is genuinely a raw string when text/raw bodyparsing is used).
|
|
7277
|
+
*/
|
|
7278
|
+
export function findJsJsonParseBodyFindings(code, file) {
|
|
7279
|
+
const findings = [];
|
|
7280
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
7281
|
+
return findings;
|
|
7282
|
+
if (!/\bJSON\s*\.\s*parse\s*\(/.test(code))
|
|
7283
|
+
return findings;
|
|
7284
|
+
if (!/\breq\s*\.\s*body\b/.test(code))
|
|
7285
|
+
return findings;
|
|
7286
|
+
const lines = code.split('\n');
|
|
7287
|
+
const reqBodyRe = /\breq\s*\.\s*body\b/;
|
|
7288
|
+
const taintedVars = new Set();
|
|
7289
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
7290
|
+
const before = taintedVars.size;
|
|
7291
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7292
|
+
const t = lines[i].trim();
|
|
7293
|
+
if (t.startsWith('//'))
|
|
7294
|
+
continue;
|
|
7295
|
+
const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
|
|
7296
|
+
if (!m)
|
|
7297
|
+
continue;
|
|
7298
|
+
const lhs = m[1];
|
|
7299
|
+
const rhs = m[2];
|
|
7300
|
+
if (taintedVars.has(lhs))
|
|
7301
|
+
continue;
|
|
7302
|
+
if (reqBodyRe.test(rhs)) {
|
|
7303
|
+
taintedVars.add(lhs);
|
|
7304
|
+
continue;
|
|
7305
|
+
}
|
|
7306
|
+
for (const v of taintedVars) {
|
|
7307
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
7308
|
+
taintedVars.add(lhs);
|
|
7309
|
+
break;
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
7312
|
+
}
|
|
7313
|
+
if (taintedVars.size === before)
|
|
7314
|
+
break;
|
|
7315
|
+
}
|
|
7316
|
+
const callRe = /\bJSON\s*\.\s*parse\s*\(\s*([^)]+?)\s*\)/g;
|
|
7317
|
+
const seen = new Set();
|
|
7318
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7319
|
+
const line = lines[i];
|
|
7320
|
+
if (line.trim().startsWith('//'))
|
|
7321
|
+
continue;
|
|
7322
|
+
callRe.lastIndex = 0;
|
|
7323
|
+
let m;
|
|
7324
|
+
while ((m = callRe.exec(line)) !== null) {
|
|
7325
|
+
const arg = m[1];
|
|
7326
|
+
let tainted = reqBodyRe.test(arg);
|
|
7327
|
+
if (!tainted) {
|
|
7328
|
+
for (const v of taintedVars) {
|
|
7329
|
+
if (new RegExp(`\\b${v}\\b`).test(arg)) {
|
|
7330
|
+
tainted = true;
|
|
7331
|
+
break;
|
|
7332
|
+
}
|
|
7333
|
+
}
|
|
7334
|
+
}
|
|
7335
|
+
if (!tainted)
|
|
7336
|
+
continue;
|
|
7337
|
+
if (seen.has(i + 1))
|
|
7338
|
+
continue;
|
|
7339
|
+
seen.add(i + 1);
|
|
7340
|
+
findings.push({
|
|
7341
|
+
id: `insecure_deserialization-${file}-${i + 1}-js-jsonparse-body`,
|
|
7342
|
+
pass: 'language-sources',
|
|
7343
|
+
category: 'security',
|
|
7344
|
+
rule_id: 'insecure_deserialization',
|
|
7345
|
+
cwe: 'CWE-502',
|
|
7346
|
+
severity: 'high',
|
|
7347
|
+
level: 'warning',
|
|
7348
|
+
message: '`JSON.parse(req.body)` deserializes attacker-controlled bytes ' +
|
|
7349
|
+
'directly. With `express.text()` / `bodyParser.raw()` middleware ' +
|
|
7350
|
+
'`req.body` is an unvetted string; the parsed object can carry a ' +
|
|
7351
|
+
'`__proto__` payload that pollutes downstream property lookups. ' +
|
|
7352
|
+
'Validate against a schema (zod/ajv) before consuming the value.',
|
|
7353
|
+
file,
|
|
7354
|
+
line: i + 1,
|
|
7355
|
+
snippet: line.trim(),
|
|
7356
|
+
});
|
|
7357
|
+
}
|
|
7358
|
+
}
|
|
7359
|
+
return findings;
|
|
7360
|
+
}
|
|
7361
|
+
/**
|
|
7362
|
+
* Sprint 89 detector C (#189) — JS DOM XPath injection via
|
|
7363
|
+
* `document.evaluate(<tainted>, ...)`.
|
|
7364
|
+
*
|
|
7365
|
+
* The DOM `XPathEvaluator.evaluate()` API takes a string XPath
|
|
7366
|
+
* expression as its first argument. When that string is built from
|
|
7367
|
+
* `location.search` / `location.hash` / URLSearchParams without
|
|
7368
|
+
* escaping, the attacker can rewrite the query (e.g. injecting
|
|
7369
|
+
* `' or '1'='1`-style predicates) and exfiltrate other nodes. CWE-643.
|
|
7370
|
+
*
|
|
7371
|
+
* Gate: file must reference `XPathResult` (strong DOM-XPath signal)
|
|
7372
|
+
* AND call `.evaluate(`, AND contain a browser taint source
|
|
7373
|
+
* (`location.search`, `location.hash`, `URLSearchParams`).
|
|
7374
|
+
*/
|
|
7375
|
+
export function findJsDomXpathInjectionFindings(code, file) {
|
|
7376
|
+
const findings = [];
|
|
7377
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
7378
|
+
return findings;
|
|
7379
|
+
if (!/\.\s*evaluate\s*\(/.test(code))
|
|
7380
|
+
return findings;
|
|
7381
|
+
if (!/\bXPathResult\b/.test(code))
|
|
7382
|
+
return findings;
|
|
7383
|
+
const browserSourceRe = /\b(?:location\s*\.\s*(?:search|hash|href)|URLSearchParams|window\s*\.\s*name|document\s*\.\s*cookie)\b/;
|
|
7384
|
+
if (!browserSourceRe.test(code))
|
|
7385
|
+
return findings;
|
|
7386
|
+
const lines = code.split('\n');
|
|
7387
|
+
const taintedVars = new Set();
|
|
7388
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
7389
|
+
const before = taintedVars.size;
|
|
7390
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7391
|
+
const t = lines[i].trim();
|
|
7392
|
+
if (t.startsWith('//'))
|
|
7393
|
+
continue;
|
|
7394
|
+
const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
|
|
7395
|
+
if (!m)
|
|
7396
|
+
continue;
|
|
7397
|
+
const lhs = m[1];
|
|
7398
|
+
const rhs = m[2];
|
|
7399
|
+
if (taintedVars.has(lhs))
|
|
7400
|
+
continue;
|
|
7401
|
+
if (browserSourceRe.test(rhs)) {
|
|
7402
|
+
taintedVars.add(lhs);
|
|
7403
|
+
continue;
|
|
7404
|
+
}
|
|
7405
|
+
for (const v of taintedVars) {
|
|
7406
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
7407
|
+
taintedVars.add(lhs);
|
|
7408
|
+
break;
|
|
7409
|
+
}
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7412
|
+
if (taintedVars.size === before)
|
|
7413
|
+
break;
|
|
7414
|
+
}
|
|
7415
|
+
if (taintedVars.size === 0)
|
|
7416
|
+
return findings;
|
|
7417
|
+
const evalRe = /\.\s*evaluate\s*\(\s*([^,)]+)/;
|
|
7418
|
+
const seen = new Set();
|
|
7419
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7420
|
+
const line = lines[i];
|
|
7421
|
+
if (line.trim().startsWith('//'))
|
|
7422
|
+
continue;
|
|
7423
|
+
const m = line.match(evalRe);
|
|
7424
|
+
if (!m)
|
|
7425
|
+
continue;
|
|
7426
|
+
const arg = m[1].trim();
|
|
7427
|
+
let tainted = false;
|
|
7428
|
+
for (const v of taintedVars) {
|
|
7429
|
+
if (new RegExp(`\\b${v}\\b`).test(arg)) {
|
|
7430
|
+
tainted = true;
|
|
7431
|
+
break;
|
|
7432
|
+
}
|
|
7433
|
+
}
|
|
7434
|
+
if (!tainted)
|
|
7435
|
+
continue;
|
|
7436
|
+
if (seen.has(i + 1))
|
|
7437
|
+
continue;
|
|
7438
|
+
seen.add(i + 1);
|
|
7439
|
+
findings.push({
|
|
7440
|
+
id: `xpath_injection-${file}-${i + 1}-js-dom-evaluate`,
|
|
7441
|
+
pass: 'language-sources',
|
|
7442
|
+
category: 'security',
|
|
7443
|
+
rule_id: 'xpath_injection',
|
|
7444
|
+
cwe: 'CWE-643',
|
|
7445
|
+
severity: 'high',
|
|
7446
|
+
level: 'warning',
|
|
7447
|
+
message: 'XPath injection: `document.evaluate(<tainted>, ...)` lets the ' +
|
|
7448
|
+
'attacker break out of the XPath expression and read sibling ' +
|
|
7449
|
+
'nodes. Bind user input through XPath variables / parameterized ' +
|
|
7450
|
+
'expressions, or escape with an allowlist before concatenation.',
|
|
7451
|
+
file,
|
|
7452
|
+
line: i + 1,
|
|
7453
|
+
snippet: line.trim(),
|
|
7454
|
+
});
|
|
7455
|
+
}
|
|
7456
|
+
return findings;
|
|
7457
|
+
}
|
|
7458
|
+
/**
|
|
7459
|
+
* Sprint 90 detector A (#189) — Go XXE via `encoding/xml` decoder with
|
|
7460
|
+
* `d.Strict = false` (allows DTD-like constructs and custom Entity
|
|
7461
|
+
* resolution to be configured on the decoder). When the source stream
|
|
7462
|
+
* is `*http.Request.Body`, the attacker can submit a payload that
|
|
7463
|
+
* triggers entity-expansion / external-entity reads through the
|
|
7464
|
+
* `Entity` map. CWE-611 / CWE-776.
|
|
7465
|
+
*/
|
|
7466
|
+
export function findGoXmlDecoderXxeFindings(code, file) {
|
|
7467
|
+
const findings = [];
|
|
7468
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
7469
|
+
return findings;
|
|
7470
|
+
if (!/\bxml\s*\.\s*NewDecoder\s*\(/.test(code))
|
|
7471
|
+
return findings;
|
|
7472
|
+
if (!/\*http\.Request\b/.test(code) && !/\bhttp\.HandlerFunc\b/.test(code)) {
|
|
7473
|
+
return findings;
|
|
7474
|
+
}
|
|
7475
|
+
const lines = code.split('\n');
|
|
7476
|
+
// Track decoder vars constructed from req.Body.
|
|
7477
|
+
const decoderFromBody = new Map(); // var -> line idx
|
|
7478
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7479
|
+
const t = lines[i].trim();
|
|
7480
|
+
if (t.startsWith('//'))
|
|
7481
|
+
continue;
|
|
7482
|
+
const m = t.match(/^(\w+)\s*(?::=|=)\s*xml\s*\.\s*NewDecoder\s*\(\s*([^)]+)\)/);
|
|
7483
|
+
if (!m)
|
|
7484
|
+
continue;
|
|
7485
|
+
if (/\b\w+\s*\.\s*Body\b/.test(m[2]))
|
|
7486
|
+
decoderFromBody.set(m[1], i);
|
|
7487
|
+
}
|
|
7488
|
+
if (decoderFromBody.size === 0)
|
|
7489
|
+
return findings;
|
|
7490
|
+
// Only fire if downstream we see `<dec>.Strict = false` or
|
|
7491
|
+
// `<dec>.Entity = ...` (entity-map override).
|
|
7492
|
+
const seen = new Set();
|
|
7493
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7494
|
+
const line = lines[i];
|
|
7495
|
+
if (line.trim().startsWith('//'))
|
|
7496
|
+
continue;
|
|
7497
|
+
const m = line.match(/\b(\w+)\s*\.\s*(?:Strict\s*=\s*false|Entity\s*=)\b/);
|
|
7498
|
+
if (!m)
|
|
7499
|
+
continue;
|
|
7500
|
+
if (!decoderFromBody.has(m[1]))
|
|
7501
|
+
continue;
|
|
7502
|
+
if (seen.has(i + 1))
|
|
7503
|
+
continue;
|
|
7504
|
+
seen.add(i + 1);
|
|
7505
|
+
findings.push({
|
|
7506
|
+
id: `xml_entity_expansion-${file}-${i + 1}-go-xml-decoder`,
|
|
7507
|
+
pass: 'language-sources',
|
|
7508
|
+
category: 'security',
|
|
7509
|
+
rule_id: 'xml_entity_expansion',
|
|
7510
|
+
cwe: 'CWE-611',
|
|
7511
|
+
severity: 'high',
|
|
7512
|
+
level: 'warning',
|
|
7513
|
+
message: 'XXE: Go `xml.NewDecoder(req.Body)` with `Strict = false` (or a ' +
|
|
7514
|
+
'custom `Entity` map) allows entity references that the standard ' +
|
|
7515
|
+
'library normally rejects. Keep `Strict = true` and avoid setting ' +
|
|
7516
|
+
'`Entity` from attacker-controlled inputs.',
|
|
7517
|
+
file,
|
|
7518
|
+
line: i + 1,
|
|
7519
|
+
snippet: line.trim(),
|
|
7520
|
+
});
|
|
7521
|
+
}
|
|
7522
|
+
return findings;
|
|
7523
|
+
}
|
|
7524
|
+
/**
|
|
7525
|
+
* Sprint 90 detector B (#189) — Python SSTI via Jinja2
|
|
7526
|
+
* `Template(<tainted>).render(...)`.
|
|
7527
|
+
*
|
|
7528
|
+
* Constructing a `jinja2.Template` from attacker-controlled source
|
|
7529
|
+
* gives the attacker the entire Jinja sandbox-escape surface
|
|
7530
|
+
* (`{{ ''.__class__.__mro__[1].__subclasses__() ... }}`). CWE-1336.
|
|
7531
|
+
*
|
|
7532
|
+
* Gate: file must import `jinja2.Template` AND construct it from a
|
|
7533
|
+
* Flask/FastAPI request extractor (request.args/form/values/json/data).
|
|
7534
|
+
*/
|
|
7535
|
+
export function findPythonJinjaTemplateSstiFindings(code, file) {
|
|
7536
|
+
const findings = [];
|
|
7537
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
7538
|
+
return findings;
|
|
7539
|
+
if (!/\bfrom\s+jinja2\s+import\s+[^#\n]*\bTemplate\b/.test(code) &&
|
|
7540
|
+
!/\bjinja2\s*\.\s*Template\b/.test(code)) {
|
|
7541
|
+
return findings;
|
|
7542
|
+
}
|
|
7543
|
+
const reqExtractRe = /\brequest\s*\.\s*(?:args|form|values|json|data|cookies|headers)\b/;
|
|
7544
|
+
if (!reqExtractRe.test(code))
|
|
7545
|
+
return findings;
|
|
7546
|
+
const lines = code.split('\n');
|
|
7547
|
+
const taintedVars = new Set();
|
|
7548
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
7549
|
+
const before = taintedVars.size;
|
|
7550
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7551
|
+
const t = lines[i].trim();
|
|
7552
|
+
if (t.startsWith('#'))
|
|
7553
|
+
continue;
|
|
7554
|
+
const m = t.match(/^(\w+)\s*=\s*(.+?)$/);
|
|
7555
|
+
if (!m)
|
|
7556
|
+
continue;
|
|
7557
|
+
const lhs = m[1];
|
|
7558
|
+
const rhs = m[2];
|
|
7559
|
+
if (taintedVars.has(lhs))
|
|
7560
|
+
continue;
|
|
7561
|
+
if (reqExtractRe.test(rhs)) {
|
|
7562
|
+
taintedVars.add(lhs);
|
|
7563
|
+
continue;
|
|
7564
|
+
}
|
|
7565
|
+
for (const v of taintedVars) {
|
|
7566
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
7567
|
+
taintedVars.add(lhs);
|
|
7568
|
+
break;
|
|
7569
|
+
}
|
|
7570
|
+
}
|
|
7571
|
+
}
|
|
7572
|
+
if (taintedVars.size === before)
|
|
7573
|
+
break;
|
|
7574
|
+
}
|
|
7575
|
+
if (taintedVars.size === 0)
|
|
7576
|
+
return findings;
|
|
7577
|
+
const ctorRe = /\b(?:jinja2\s*\.\s*)?Template\s*\(\s*([^)]+?)\s*\)/;
|
|
7578
|
+
const seen = new Set();
|
|
7579
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7580
|
+
const line = lines[i];
|
|
7581
|
+
if (line.trim().startsWith('#'))
|
|
7582
|
+
continue;
|
|
7583
|
+
const m = line.match(ctorRe);
|
|
7584
|
+
if (!m)
|
|
7585
|
+
continue;
|
|
7586
|
+
const arg = m[1].trim();
|
|
7587
|
+
let tainted = reqExtractRe.test(arg);
|
|
7588
|
+
if (!tainted) {
|
|
7589
|
+
for (const v of taintedVars) {
|
|
7590
|
+
if (new RegExp(`\\b${v}\\b`).test(arg)) {
|
|
7591
|
+
tainted = true;
|
|
7592
|
+
break;
|
|
7593
|
+
}
|
|
7594
|
+
}
|
|
7595
|
+
}
|
|
7596
|
+
if (!tainted)
|
|
7597
|
+
continue;
|
|
7598
|
+
if (seen.has(i + 1))
|
|
7599
|
+
continue;
|
|
7600
|
+
seen.add(i + 1);
|
|
7601
|
+
findings.push({
|
|
7602
|
+
id: `template_injection-${file}-${i + 1}-py-jinja-template`,
|
|
7603
|
+
pass: 'language-sources',
|
|
7604
|
+
category: 'security',
|
|
7605
|
+
rule_id: 'template_injection',
|
|
7606
|
+
cwe: 'CWE-1336',
|
|
7607
|
+
severity: 'critical',
|
|
7608
|
+
level: 'error',
|
|
7609
|
+
message: 'Server-side template injection: `jinja2.Template(<tainted>).render()` ' +
|
|
7610
|
+
'compiles attacker-controlled source. Sandbox-escape gadgets such as ' +
|
|
7611
|
+
'`{{ ().__class__.__bases__[0].__subclasses__() }}` lead to RCE. ' +
|
|
7612
|
+
'Render fixed templates with user data passed as context variables.',
|
|
7613
|
+
file,
|
|
7614
|
+
line: i + 1,
|
|
7615
|
+
snippet: line.trim(),
|
|
7616
|
+
});
|
|
7617
|
+
}
|
|
7618
|
+
return findings;
|
|
7619
|
+
}
|
|
7620
|
+
/**
|
|
7621
|
+
* Sprint 90 detector C (#189) — JS SSTI via `Handlebars.compile(<tainted>)`
|
|
7622
|
+
* or `ejs.render(<tainted>, ...)` / `ejs.compile(<tainted>)`.
|
|
7623
|
+
*
|
|
7624
|
+
* Compiling an attacker-controlled template lets the attacker execute
|
|
7625
|
+
* arbitrary code through helper-shadowing / prototype gadgets
|
|
7626
|
+
* (Handlebars CVE-2019-19919 chains, EJS render-options escape). CWE-1336.
|
|
7627
|
+
*/
|
|
7628
|
+
export function findJsTemplateInjectionSstiFindings(code, file) {
|
|
7629
|
+
const findings = [];
|
|
7630
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
7631
|
+
return findings;
|
|
7632
|
+
if (!/\bHandlebars\s*\.\s*compile\s*\(/.test(code) &&
|
|
7633
|
+
!/\bejs\s*\.\s*(?:render|compile)\s*\(/.test(code)) {
|
|
7634
|
+
return findings;
|
|
7635
|
+
}
|
|
7636
|
+
if (!/\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/.test(code)) {
|
|
7637
|
+
return findings;
|
|
7638
|
+
}
|
|
7639
|
+
const lines = code.split('\n');
|
|
7640
|
+
const reqExtractRe = /\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/;
|
|
7641
|
+
const taintedVars = new Set();
|
|
7642
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
7643
|
+
const before = taintedVars.size;
|
|
7644
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7645
|
+
const t = lines[i].trim();
|
|
7646
|
+
if (t.startsWith('//'))
|
|
7647
|
+
continue;
|
|
7648
|
+
const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
|
|
7649
|
+
if (!m)
|
|
7650
|
+
continue;
|
|
7651
|
+
const lhs = m[1];
|
|
7652
|
+
const rhs = m[2];
|
|
7653
|
+
if (taintedVars.has(lhs))
|
|
7654
|
+
continue;
|
|
7655
|
+
if (reqExtractRe.test(rhs)) {
|
|
7656
|
+
taintedVars.add(lhs);
|
|
7657
|
+
continue;
|
|
7658
|
+
}
|
|
7659
|
+
for (const v of taintedVars) {
|
|
7660
|
+
if (new RegExp(`\\b${v}\\b`).test(rhs)) {
|
|
7661
|
+
taintedVars.add(lhs);
|
|
7662
|
+
break;
|
|
7663
|
+
}
|
|
7664
|
+
}
|
|
7665
|
+
}
|
|
7666
|
+
if (taintedVars.size === before)
|
|
7667
|
+
break;
|
|
7668
|
+
}
|
|
7669
|
+
if (taintedVars.size === 0)
|
|
7670
|
+
return findings;
|
|
7671
|
+
const callRe = /\b(Handlebars\s*\.\s*compile|ejs\s*\.\s*(?:render|compile))\s*\(\s*([^,)]+)/;
|
|
7672
|
+
const seen = new Set();
|
|
7673
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7674
|
+
const line = lines[i];
|
|
7675
|
+
if (line.trim().startsWith('//'))
|
|
7676
|
+
continue;
|
|
7677
|
+
const m = line.match(callRe);
|
|
7678
|
+
if (!m)
|
|
7679
|
+
continue;
|
|
7680
|
+
const arg = m[2].trim();
|
|
7681
|
+
let tainted = reqExtractRe.test(arg);
|
|
7682
|
+
if (!tainted) {
|
|
7683
|
+
for (const v of taintedVars) {
|
|
7684
|
+
if (new RegExp(`\\b${v}\\b`).test(arg)) {
|
|
7685
|
+
tainted = true;
|
|
7686
|
+
break;
|
|
7687
|
+
}
|
|
7688
|
+
}
|
|
7689
|
+
}
|
|
7690
|
+
if (!tainted)
|
|
7691
|
+
continue;
|
|
7692
|
+
if (seen.has(i + 1))
|
|
7693
|
+
continue;
|
|
7694
|
+
seen.add(i + 1);
|
|
7695
|
+
findings.push({
|
|
7696
|
+
id: `template_injection-${file}-${i + 1}-js-${m[1].replace(/[\s.]/g, '').toLowerCase()}`,
|
|
7697
|
+
pass: 'language-sources',
|
|
7698
|
+
category: 'security',
|
|
7699
|
+
rule_id: 'template_injection',
|
|
7700
|
+
cwe: 'CWE-1336',
|
|
7701
|
+
severity: 'critical',
|
|
7702
|
+
level: 'error',
|
|
7703
|
+
message: 'Server-side template injection: compiling/rendering a template ' +
|
|
7704
|
+
'whose source is attacker-controlled (`Handlebars.compile(...)` / ' +
|
|
7705
|
+
'`ejs.render(...)`) opens helper-shadowing and prototype-gadget RCE ' +
|
|
7706
|
+
'paths. Use a fixed template and pass user data as context.',
|
|
7707
|
+
file,
|
|
7708
|
+
line: i + 1,
|
|
7709
|
+
snippet: line.trim(),
|
|
7710
|
+
});
|
|
7711
|
+
}
|
|
7712
|
+
return findings;
|
|
7713
|
+
}
|
|
7134
7714
|
//# sourceMappingURL=language-sources-pass.js.map
|