circle-ir 3.215.0 → 4.0.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/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +68 -0
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +42 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +277 -0
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +18 -3
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +592 -5
- package/dist/core/circle-ir-core.cjs +338 -4
- package/dist/core/circle-ir-core.js +338 -4
- package/dist/core/extractors/calls.d.ts.map +1 -1
- package/dist/core/extractors/calls.js +133 -0
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/dfg.d.ts.map +1 -1
- package/dist/core/extractors/dfg.js +86 -1
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/types.d.ts.map +1 -1
- package/dist/core/extractors/types.js +89 -0
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/languages/plugins/csharp.d.ts +35 -0
- package/dist/languages/plugins/csharp.d.ts.map +1 -0
- package/dist/languages/plugins/csharp.js +72 -0
- package/dist/languages/plugins/csharp.js.map +1 -0
- package/dist/languages/plugins/index.d.ts +1 -0
- package/dist/languages/plugins/index.d.ts.map +1 -1
- package/dist/languages/plugins/index.js +3 -0
- package/dist/languages/plugins/index.js.map +1 -1
- package/dist/languages/types.d.ts +1 -1
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/wasm/tree-sitter-csharp.wasm +0 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sink-filter-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/sink-filter-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AACzG,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"sink-filter-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/sink-filter-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AACzG,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AA61B9E,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,sBAAsB;IACtB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,qBAAa,cAAe,YAAW,YAAY,CAAC,gBAAgB,CAAC;IACnE,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,gBAAgB;CAuoCxC;AAkRD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,KAAK,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AA4I1G,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EACjC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,EACxB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,OAAO,EAAE,OAAO,EAChB,GAAG,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,EACrB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAC3B,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAC/B,QAAQ,CAAC,EAAE,MAAM,GAChB,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CA6E5B;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EACjC,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,EAC3C,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,GACvB,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CA2C5B"}
|
|
@@ -355,6 +355,42 @@ const HTML_CONTENT_TYPE_RE = /text\/html|TEXT_HTML/;
|
|
|
355
355
|
// entries, so dropping the member-receiver form loses no modelled SSRF sink.
|
|
356
356
|
const FETCH_GLOBAL_RECEIVER_RE = /(?:^|[^.\w])(?:window|globalThis|self|global)\s*\.\s*fetch\s*\(/;
|
|
357
357
|
const FETCH_MEMBER_RECEIVER_RE = /[\w$)\]]\s*\.\s*fetch\s*\(/;
|
|
358
|
+
// cognium-ai#279 R-2 — `Array.prototype.find`/`filter`/… take a *function*
|
|
359
|
+
// predicate; a `nosql_injection` sink only makes sense when the argument is a
|
|
360
|
+
// query object. `user.accounts.find(a => a.providerId === x)` is an array scan,
|
|
361
|
+
// not a Mongo query. A function first argument (arrow or `function`) proves it
|
|
362
|
+
// is the Array method, mirroring the `setTimeout(fn)` "arg is a function ⇒ not a
|
|
363
|
+
// sink" test. Real `collection.find({ ... })` passes an object literal and is
|
|
364
|
+
// unaffected.
|
|
365
|
+
const NOSQL_ARRAY_METHODS = new Set(['find', 'filter', 'some', 'every', 'findIndex', 'findLast']);
|
|
366
|
+
const FUNCTION_FIRST_ARG_RE = /\.\s*(?:find|filter|some|every|findIndex|findLast)\s*\(\s*(?:async\s+)?(?:function\b|(?:\([^()]*\)|[A-Za-z_$][\w$]*)\s*=>)/;
|
|
367
|
+
// cognium-ai#279 R-1 — an argument that is an object literal with an ORM
|
|
368
|
+
// query-builder key (`where`/`select`/`populate`/…) is a *builder*, not a raw
|
|
369
|
+
// query string: a value in a `{ field, value }` position binds as a parameter
|
|
370
|
+
// and cannot carry an operator. `adapter.findOne({ where: [...] })` /
|
|
371
|
+
// `repo.findMany({ where: {...} })` are the Node analogue of #247's
|
|
372
|
+
// PreparedStatement class. A real `db.query("SELECT … WHERE …")` has no
|
|
373
|
+
// `key:` colon syntax and is unaffected.
|
|
374
|
+
const ORM_BUILDER_KEY_RE = /[{,]\s*(?:where|select|populate|include|orderBy|relations|attributes)\s*:/;
|
|
375
|
+
// cognium-ai#279 R-3 — SSRF requires a server-side requester; a `fetch`/`axios`
|
|
376
|
+
// call in a browser-rendered client component cannot be SSRF (the browser makes
|
|
377
|
+
// the request, subject to CORS, not the server). Suppress `ssrf` ONLY when all
|
|
378
|
+
// three hold, so real server-side fetches are never touched:
|
|
379
|
+
// (a) the file is a component extension (.jsx/.tsx/.vue/.svelte), AND
|
|
380
|
+
// (b) it carries a positive CLIENT signal (`"use client"`, React hooks,
|
|
381
|
+
// browser globals, JSX event handlers), AND
|
|
382
|
+
// (c) it carries NO server signal (`"use server"`, next/server, SSR data
|
|
383
|
+
// hooks, API-route handler, Node server imports).
|
|
384
|
+
// A Next.js Server Component or API route doing `fetch(userUrl)` has no client
|
|
385
|
+
// signal (and usually a server one), so it keeps firing — zero false negatives.
|
|
386
|
+
const BROWSER_COMPONENT_EXT_RE = /\.(?:jsx|tsx|vue|svelte)$/;
|
|
387
|
+
const CLIENT_SIGNAL_RE = /["']use client["']|\buse(?:State|Effect|Ref|Callback|Memo|Context|Reducer|LayoutEffect)\s*\(|\b(?:window|document|localStorage|sessionStorage|navigator)\s*\.|\son(?:Click|Change|Submit|Input|KeyDown|KeyUp|MouseOver|Focus|Blur)\s*=/;
|
|
388
|
+
const SERVER_SIGNAL_RE = /["']use server["']|\b(?:getServerSideProps|getStaticProps|getStaticPaths)\b|from\s+["']next\/server["']|\bNext(?:Api)?(?:Request|Response)\b|from\s+["'](?:node:)?(?:fs|child_process|http|https|net|dns|dgram)["']|\bexport\s+(?:default\s+)?(?:async\s+)?function\s+handler\s*\(/;
|
|
389
|
+
// cognium-ai#279 R-6 — a template-literal URL whose host is a fixed literal
|
|
390
|
+
// (`https://api.github.com/${path}`) is not SSRF: only the path is
|
|
391
|
+
// interpolated, the destination host is not attacker-controlled. Matches a
|
|
392
|
+
// backtick + scheme + a literal host run (no `${` before the first `/`).
|
|
393
|
+
const FIXED_HOST_TEMPLATE_RE = /`https?:\/\/[^/${}\s`]+[/`]/;
|
|
358
394
|
/**
|
|
359
395
|
* Returns the 1-indexed line numbers that a Java `if (x.contains("..")) throw/
|
|
360
396
|
* return` reject-guard makes path-traversal-safe (cognium-dev#269). A line is
|
|
@@ -408,6 +444,154 @@ function javaTraversalRejectGuardedLines(code) {
|
|
|
408
444
|
}
|
|
409
445
|
return covered;
|
|
410
446
|
}
|
|
447
|
+
// skillsregistry#49b — a fetch/HTTP call whose URL is constrained by an anchored
|
|
448
|
+
// host allow-list is not arbitrary-URL SSRF. Recognise reject-guards
|
|
449
|
+
// (throw/return) that pin the tainted URL to a fixed host set:
|
|
450
|
+
// if (!/^https:\/\/(www\.)?host\.com\//.test(url)) throw ...
|
|
451
|
+
// if (!url.startsWith("https://api.github.com/")) return ...
|
|
452
|
+
// The guard must name the scheme AND contain a literal domain label (a
|
|
453
|
+
// letter-led run followed by a dot) — a bare `^https?:\/\/` with no host does
|
|
454
|
+
// not constrain the destination and is deliberately NOT credited.
|
|
455
|
+
/** True if a regex source (from `/…/.test(url)`) pins the host to a literal domain. */
|
|
456
|
+
function isAnchoredHostAllowlistRegex(src) {
|
|
457
|
+
if (!src.startsWith('^'))
|
|
458
|
+
return false; // must be anchored
|
|
459
|
+
if (!/https?/i.test(src))
|
|
460
|
+
return false; // must name the scheme
|
|
461
|
+
return /[A-Za-z][A-Za-z0-9-]+\\?\./.test(src); // literal host label + (escaped) dot
|
|
462
|
+
}
|
|
463
|
+
/** True if a `startsWith` prefix pins scheme + a literal host (implicitly anchored). */
|
|
464
|
+
function isSchemeHostPrefix(prefix) {
|
|
465
|
+
if (!/^https?:\/\//i.test(prefix))
|
|
466
|
+
return false;
|
|
467
|
+
const afterScheme = prefix.replace(/^https?:\/\//i, '');
|
|
468
|
+
return /[A-Za-z][A-Za-z0-9-]*\./.test(afterScheme); // has a host label + dot
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Returns the 1-indexed line numbers made SSRF-safe by an anchored host
|
|
472
|
+
* allow-list reject-guard (skillsregistry#49b). Mirrors the Java traversal
|
|
473
|
+
* reject-guard: a line is covered when it references a variable that was
|
|
474
|
+
* host-guarded above (following one-hop derivations), and the guard has a
|
|
475
|
+
* throwing/returning body (reject polarity — the safe path is everything below).
|
|
476
|
+
*/
|
|
477
|
+
function jsSsrfHostGuardedLines(code) {
|
|
478
|
+
const covered = new Set();
|
|
479
|
+
const lines = code.split('\n');
|
|
480
|
+
// `if ( ! /regex/flags .test( VAR ) )`
|
|
481
|
+
const rejectRegexTest = /\bif\s*\(\s*!\s*\/((?:[^/\\]|\\.)*)\/[a-z]*\s*\.\s*test\s*\(\s*([A-Za-z_]\w*)\s*\)/;
|
|
482
|
+
// `if ( ! VAR.startsWith( "prefix" ) )`
|
|
483
|
+
const rejectStartsWith = /\bif\s*\(\s*!\s*([A-Za-z_]\w*)\s*\.\s*startsWith\s*\(\s*["'`]([^"'`]+)["'`]/;
|
|
484
|
+
const terminatorRe = /\b(throw|return)\b/;
|
|
485
|
+
const assignRe = /^\s*(?:const|let|var\s+)?\s*([A-Za-z_]\w*)\s*=(?!=)\s*(.*)$/;
|
|
486
|
+
for (let i = 0; i < lines.length; i++) {
|
|
487
|
+
let guardedVar = null;
|
|
488
|
+
const rm = rejectRegexTest.exec(lines[i]);
|
|
489
|
+
if (rm && isAnchoredHostAllowlistRegex(rm[1]))
|
|
490
|
+
guardedVar = rm[2];
|
|
491
|
+
if (!guardedVar) {
|
|
492
|
+
const sm = rejectStartsWith.exec(lines[i]);
|
|
493
|
+
if (sm && isSchemeHostPrefix(sm[2]))
|
|
494
|
+
guardedVar = sm[1];
|
|
495
|
+
}
|
|
496
|
+
if (!guardedVar)
|
|
497
|
+
continue;
|
|
498
|
+
let hasTerminator = terminatorRe.test(lines[i]);
|
|
499
|
+
if (!hasTerminator) {
|
|
500
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 6); j++) {
|
|
501
|
+
if (terminatorRe.test(lines[j])) {
|
|
502
|
+
hasTerminator = true;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
if (lines[j].includes('}'))
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (!hasTerminator)
|
|
510
|
+
continue;
|
|
511
|
+
const guarded = new Set([guardedVar]);
|
|
512
|
+
const mentionsGuarded = (text) => {
|
|
513
|
+
for (const name of guarded)
|
|
514
|
+
if (new RegExp(`\\b${name}\\b`).test(text))
|
|
515
|
+
return true;
|
|
516
|
+
return false;
|
|
517
|
+
};
|
|
518
|
+
for (let l = i + 1; l < lines.length; l++) {
|
|
519
|
+
const lineText = lines[l];
|
|
520
|
+
const assign = assignRe.exec(lineText);
|
|
521
|
+
if (assign) {
|
|
522
|
+
if (mentionsGuarded(assign[2]))
|
|
523
|
+
guarded.add(assign[1]);
|
|
524
|
+
else if (assign[1] !== guardedVar)
|
|
525
|
+
guarded.delete(assign[1]);
|
|
526
|
+
}
|
|
527
|
+
if (mentionsGuarded(lineText))
|
|
528
|
+
covered.add(l + 1);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return covered;
|
|
532
|
+
}
|
|
533
|
+
// C# sanitizers (Phase-1) and the sink types each neutralises. A tainted value
|
|
534
|
+
// that flows through one of these before reaching a sink of the matching type
|
|
535
|
+
// is safe. Method names are C#-distinctive (see config-loader C# sanitizers).
|
|
536
|
+
const CSHARP_SANITIZER_RES = [
|
|
537
|
+
{ re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: 'xss' },
|
|
538
|
+
{ re: /\bHtmlEncoder\s*\.\s*Encode\s*\(/, type: 'xss' },
|
|
539
|
+
{ re: /\bPath\s*\.\s*GetFileName\s*\(/, type: 'path_traversal' },
|
|
540
|
+
];
|
|
541
|
+
/**
|
|
542
|
+
* Compute, per sink type, the set of C# variables whose value has passed through
|
|
543
|
+
* a sanitizer for that type (cognium-dev C#/.NET Phase-1). Seeds from
|
|
544
|
+
* `V = <sanitizer_T>(…)` assignments, then takes a transitive closure over
|
|
545
|
+
* `W = … V …` so downstream derivations (`s = "<div>" + safe`) stay sanitized.
|
|
546
|
+
* Over-approximates on mixed-source lines (`s = safe + evil`) — an accepted
|
|
547
|
+
* MVP recall trade for text-scan sanitizer awareness; C#-scoped, so no effect
|
|
548
|
+
* on other languages.
|
|
549
|
+
*/
|
|
550
|
+
function csharpSanitizedVarsByType(code) {
|
|
551
|
+
const lines = code.split('\n');
|
|
552
|
+
const byType = new Map();
|
|
553
|
+
const assignRe = /^\s*(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);?\s*$/;
|
|
554
|
+
for (const { type } of CSHARP_SANITIZER_RES) {
|
|
555
|
+
if (!byType.has(type))
|
|
556
|
+
byType.set(type, new Set());
|
|
557
|
+
}
|
|
558
|
+
// Seed: direct `V = sanitizer(...)`.
|
|
559
|
+
for (const line of lines) {
|
|
560
|
+
const m = assignRe.exec(line);
|
|
561
|
+
if (!m)
|
|
562
|
+
continue;
|
|
563
|
+
const [, lhs, rhs] = m;
|
|
564
|
+
for (const { re, type } of CSHARP_SANITIZER_RES) {
|
|
565
|
+
if (re.test(rhs))
|
|
566
|
+
byType.get(type).add(lhs);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Transitive closure: `W = … V …` where V is already sanitized-for-T.
|
|
570
|
+
let changed = true;
|
|
571
|
+
let guard = 0;
|
|
572
|
+
while (changed && guard < lines.length + 2) {
|
|
573
|
+
changed = false;
|
|
574
|
+
guard++;
|
|
575
|
+
for (const line of lines) {
|
|
576
|
+
const m = assignRe.exec(line);
|
|
577
|
+
if (!m)
|
|
578
|
+
continue;
|
|
579
|
+
const [, lhs, rhs] = m;
|
|
580
|
+
for (const [type, set] of byType) {
|
|
581
|
+
if (set.has(lhs))
|
|
582
|
+
continue;
|
|
583
|
+
const refsSanitized = [...set].some(v => new RegExp(`(?<![\\w])${escapeRegex(v)}(?![\\w])`).test(rhs));
|
|
584
|
+
// Only carry sanitized-ness when the RHS did not itself apply the
|
|
585
|
+
// sanitizer (already seeded) and references a sanitized var.
|
|
586
|
+
if (refsSanitized) {
|
|
587
|
+
set.add(lhs);
|
|
588
|
+
changed = true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return byType;
|
|
594
|
+
}
|
|
411
595
|
// Recognises Java `String.matches("regex")` — implicitly anchored ^…$.
|
|
412
596
|
const JAVA_INLINE_MATCHES_RE = /\.\s*matches\s*\(\s*"((?:[^"\\]|\\.)*)"\s*\)/g;
|
|
413
597
|
function resolveJavaReceiverType(receiver, sinkLine, sourceLines) {
|
|
@@ -1579,6 +1763,99 @@ export class SinkFilterPass {
|
|
|
1579
1763
|
return !FETCH_MEMBER_RECEIVER_RE.test(sinkLineText);
|
|
1580
1764
|
});
|
|
1581
1765
|
}
|
|
1766
|
+
// Stage 15g — JS/TS nosql: `.find(fn)` etc. is Array.prototype iteration,
|
|
1767
|
+
// not a Mongo query. (cognium-ai#279 R-2) Drop a `nosql_injection` sink on an
|
|
1768
|
+
// array method whose first argument is a function (arrow / `function`);
|
|
1769
|
+
// real `collection.find({query})` passes an object and is kept.
|
|
1770
|
+
if (['javascript', 'typescript'].includes(language)) {
|
|
1771
|
+
const sourceLines = ctx.code.split('\n');
|
|
1772
|
+
filtered = filtered.filter(sink => {
|
|
1773
|
+
if (sink.type !== 'nosql_injection')
|
|
1774
|
+
return true;
|
|
1775
|
+
if (!NOSQL_ARRAY_METHODS.has(sink.method ?? ''))
|
|
1776
|
+
return true;
|
|
1777
|
+
const sinkLineText = sourceLines[sink.line - 1] ?? '';
|
|
1778
|
+
return !FUNCTION_FIRST_ARG_RE.test(sinkLineText);
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
// Stage 15h — JS/TS sql/nosql: ORM query-builder object argument.
|
|
1782
|
+
// (cognium-ai#279 R-1) Drop a sql/nosql injection sink whose call passes an
|
|
1783
|
+
// object literal with an ORM builder key (`where`/`select`/…) — a bound
|
|
1784
|
+
// parameter, not a raw query. Raw `db.query("… WHERE …")` has no `key:`
|
|
1785
|
+
// colon and is kept.
|
|
1786
|
+
if (['javascript', 'typescript'].includes(language)) {
|
|
1787
|
+
const sourceLines = ctx.code.split('\n');
|
|
1788
|
+
filtered = filtered.filter(sink => {
|
|
1789
|
+
if (sink.type !== 'sql_injection' && sink.type !== 'nosql_injection')
|
|
1790
|
+
return true;
|
|
1791
|
+
const sinkLineText = sourceLines[sink.line - 1] ?? '';
|
|
1792
|
+
return !ORM_BUILDER_KEY_RE.test(sinkLineText);
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
// Stage 15j — JS/TS ssrf: client-component file. (cognium-ai#279 R-3)
|
|
1796
|
+
// A fetch/axios call in a browser-rendered client component is not SSRF.
|
|
1797
|
+
// Gated on ext + positive client signal + no server signal (zero-FN).
|
|
1798
|
+
if (['javascript', 'typescript', 'tsx'].includes(language)) {
|
|
1799
|
+
const file = ctx.graph?.ir?.meta?.file ?? '';
|
|
1800
|
+
if (BROWSER_COMPONENT_EXT_RE.test(file) &&
|
|
1801
|
+
CLIENT_SIGNAL_RE.test(ctx.code) &&
|
|
1802
|
+
!SERVER_SIGNAL_RE.test(ctx.code)) {
|
|
1803
|
+
filtered = filtered.filter(sink => sink.type !== 'ssrf');
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
// Stage 15i — JS/TS ssrf: fixed-host URL template. (cognium-ai#279 R-6)
|
|
1807
|
+
// Drop an ssrf sink whose line builds the URL from a template literal with a
|
|
1808
|
+
// literal host (only the path is interpolated).
|
|
1809
|
+
if (['javascript', 'typescript'].includes(language)) {
|
|
1810
|
+
const sourceLines = ctx.code.split('\n');
|
|
1811
|
+
filtered = filtered.filter(sink => {
|
|
1812
|
+
if (sink.type !== 'ssrf')
|
|
1813
|
+
return true;
|
|
1814
|
+
const sinkLineText = sourceLines[sink.line - 1] ?? '';
|
|
1815
|
+
return !FIXED_HOST_TEMPLATE_RE.test(sinkLineText);
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
// Stage 15f — JS/TS ssrf: anchored host allow-list reject-guard.
|
|
1819
|
+
// (skillsregistry#49b) A URL pinned to a fixed host set by an anchored
|
|
1820
|
+
// `^https://host/`.test(url) / url.startsWith("https://host/") reject-guard
|
|
1821
|
+
// (throw/return) before the request cannot target an arbitrary host, so it
|
|
1822
|
+
// is not SSRF. Applied as a sink drop (reaches the generateFindings scan path
|
|
1823
|
+
// too). Scheme-only guards (`^https?://`) are not credited — they don't
|
|
1824
|
+
// constrain the destination.
|
|
1825
|
+
if (['javascript', 'typescript'].includes(language)) {
|
|
1826
|
+
const guardedLines = jsSsrfHostGuardedLines(ctx.code);
|
|
1827
|
+
if (guardedLines.size > 0) {
|
|
1828
|
+
filtered = filtered.filter(sink => !(sink.type === 'ssrf' && guardedLines.has(sink.line)));
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
// Stage 15k — C# sanitizer suppression (cognium-dev C#/.NET Phase-1).
|
|
1832
|
+
// C# rides the text-scan propagation, which is not sanitizer-aware; apply
|
|
1833
|
+
// sanitizers here (sink-type-aware, C#-scoped). Drop a sink when its line
|
|
1834
|
+
// inline-applies a sanitizer for that type, or when a variable on the sink
|
|
1835
|
+
// line has passed through a sanitizer for that type upstream.
|
|
1836
|
+
if (language === 'csharp') {
|
|
1837
|
+
const sourceLines = ctx.code.split('\n');
|
|
1838
|
+
const sanitizedByType = csharpSanitizedVarsByType(ctx.code);
|
|
1839
|
+
filtered = filtered.filter(sink => {
|
|
1840
|
+
const sinkLineText = sourceLines[sink.line - 1] ?? '';
|
|
1841
|
+
// (a) inline sanitizer on the sink line, e.g. Html.Raw(HtmlEncode(x)).
|
|
1842
|
+
for (const { re, type } of CSHARP_SANITIZER_RES) {
|
|
1843
|
+
if (type === sink.type && re.test(sinkLineText))
|
|
1844
|
+
return false;
|
|
1845
|
+
}
|
|
1846
|
+
// (b) a sink-line variable is sanitized-for-this-type upstream. Tested
|
|
1847
|
+
// per-var (not via extractSourceIdentifiers, which skips single-char
|
|
1848
|
+
// names) so `Html.Raw(s)` with a 1-letter var is covered.
|
|
1849
|
+
const sanitizedVars = sanitizedByType.get(sink.type);
|
|
1850
|
+
if (sanitizedVars && sanitizedVars.size > 0) {
|
|
1851
|
+
for (const v of sanitizedVars) {
|
|
1852
|
+
if (new RegExp(`(?<![\\w])${escapeRegex(v)}(?![\\w])`).test(sinkLineText))
|
|
1853
|
+
return false;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return true;
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1582
1859
|
// Stage 16 — JS log_injection (CWE-117) sanitizer suppression.
|
|
1583
1860
|
// (cognium-dev #216 sanitizer-wrapped FP — Sprint 52)
|
|
1584
1861
|
//
|