circle-ir 3.72.1 → 3.74.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/configs/sinks/golang.json +8 -8
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +15 -5
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +16 -0
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +58 -1
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +229 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +54 -0
- 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 +102 -0
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +419 -10
- package/dist/core/circle-ir-core.cjs +63 -5
- package/dist/core/circle-ir-core.js +63 -5
- package/dist/languages/plugins/go.d.ts.map +1 -1
- package/dist/languages/plugins/go.js +119 -3
- package/dist/languages/plugins/go.js.map +1 -1
- package/package.json +1 -1
|
@@ -12502,11 +12502,21 @@ var DEFAULT_SANITIZERS = [
|
|
|
12502
12502
|
// (defense-in-depth — mirrors Java getCanonicalPath in this table; the
|
|
12503
12503
|
// stricter Clean+HasPrefix guard recognition is tracked separately).
|
|
12504
12504
|
// EvalSymlinks is the Go equivalent of Java's Path.toRealPath.
|
|
12505
|
-
|
|
12506
|
-
|
|
12507
|
-
{ method: "
|
|
12508
|
-
{ method: "
|
|
12509
|
-
{ method: "
|
|
12505
|
+
// Sprint 24 (#102 FP-27): broadened to cover external_taint_escape (CWE-668)
|
|
12506
|
+
// fallback so canonicalised paths don't trigger the synthetic sink.
|
|
12507
|
+
{ method: "Base", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
|
|
12508
|
+
{ method: "Base", class: "path", removes: ["path_traversal", "external_taint_escape"] },
|
|
12509
|
+
{ method: "Clean", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
|
|
12510
|
+
{ method: "Clean", class: "path", removes: ["path_traversal", "external_taint_escape"] },
|
|
12511
|
+
{ method: "EvalSymlinks", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
|
|
12512
|
+
// Go html/template escape helpers (#102 FP-27) — registered explicitly because
|
|
12513
|
+
// configs/sinks/golang.json is not loaded at runtime.
|
|
12514
|
+
{ method: "EscapeString", class: "html", removes: ["xss", "external_taint_escape", "log_injection", "open_redirect"] },
|
|
12515
|
+
{ method: "HTMLEscapeString", class: "template", removes: ["xss", "external_taint_escape", "log_injection", "open_redirect"] },
|
|
12516
|
+
{ method: "JSEscapeString", class: "template", removes: ["xss", "external_taint_escape", "log_injection"] },
|
|
12517
|
+
{ method: "URLQueryEscaper", class: "template", removes: ["xss", "external_taint_escape", "open_redirect"] },
|
|
12518
|
+
{ method: "QueryEscape", class: "url", removes: ["xss", "external_taint_escape", "open_redirect"] },
|
|
12519
|
+
{ method: "PathEscape", class: "url", removes: ["xss", "external_taint_escape", "open_redirect"] },
|
|
12510
12520
|
// Log Injection sanitizers
|
|
12511
12521
|
{ method: "replace", removes: ["log_injection"] },
|
|
12512
12522
|
// Used to remove newlines/control chars
|
|
@@ -12985,6 +12995,15 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
12985
12995
|
s.variable = m[1];
|
|
12986
12996
|
}
|
|
12987
12997
|
}
|
|
12998
|
+
if (language === "go" && sourceLines) {
|
|
12999
|
+
const GO_ASSIGN_LHS = /^\s*(?:var\s+)?([A-Za-z_]\w*)(?:\s*,\s*[A-Za-z_]\w*)*\s*(?::\s*[A-Za-z_][\w.]*\s*)?(?::?=)(?!=)/;
|
|
13000
|
+
for (const s of result) {
|
|
13001
|
+
if (s.variable && s.variable.length > 0) continue;
|
|
13002
|
+
const lineText = sourceLines[s.line - 1] ?? "";
|
|
13003
|
+
const m = GO_ASSIGN_LHS.exec(lineText);
|
|
13004
|
+
if (m) s.variable = m[1];
|
|
13005
|
+
}
|
|
13006
|
+
}
|
|
12988
13007
|
return result;
|
|
12989
13008
|
}
|
|
12990
13009
|
function isInterproceduralTaintableType(typeName) {
|
|
@@ -13104,6 +13123,42 @@ function isSafePythonSubprocessCall(call, pattern, language) {
|
|
|
13104
13123
|
}
|
|
13105
13124
|
return true;
|
|
13106
13125
|
}
|
|
13126
|
+
function isSafeGoExecCommandCall(call, pattern, language) {
|
|
13127
|
+
if (language !== "go") return false;
|
|
13128
|
+
if (pattern.type !== "command_injection") return false;
|
|
13129
|
+
if (pattern.class !== "exec") return false;
|
|
13130
|
+
if (pattern.method !== "Command" && pattern.method !== "CommandContext") return false;
|
|
13131
|
+
const programArgPos = pattern.method === "CommandContext" ? 1 : 0;
|
|
13132
|
+
const programArg = call.arguments.find((a) => a.position === programArgPos);
|
|
13133
|
+
if (!programArg) return false;
|
|
13134
|
+
let program;
|
|
13135
|
+
if (programArg.literal !== null && programArg.literal !== void 0) {
|
|
13136
|
+
program = String(programArg.literal).split("/").pop() ?? String(programArg.literal);
|
|
13137
|
+
} else {
|
|
13138
|
+
const expr = (programArg.expression ?? "").trim();
|
|
13139
|
+
if (!(expr.startsWith('"') || expr.startsWith("`") || expr.startsWith("'"))) {
|
|
13140
|
+
return false;
|
|
13141
|
+
}
|
|
13142
|
+
const stripped = expr.slice(1, -1);
|
|
13143
|
+
program = stripped.split("/").pop() ?? stripped;
|
|
13144
|
+
}
|
|
13145
|
+
const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
|
|
13146
|
+
"sh",
|
|
13147
|
+
"bash",
|
|
13148
|
+
"zsh",
|
|
13149
|
+
"dash",
|
|
13150
|
+
"ash",
|
|
13151
|
+
"ksh",
|
|
13152
|
+
"cmd",
|
|
13153
|
+
"cmd.exe",
|
|
13154
|
+
"powershell",
|
|
13155
|
+
"pwsh",
|
|
13156
|
+
"powershell.exe",
|
|
13157
|
+
"pwsh.exe"
|
|
13158
|
+
]);
|
|
13159
|
+
if (SHELL_PROGRAMS.has(program)) return false;
|
|
13160
|
+
return true;
|
|
13161
|
+
}
|
|
13107
13162
|
var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
|
|
13108
13163
|
function argIsClassLiteral(call, position) {
|
|
13109
13164
|
const arg = call.arguments.find((a) => a.position === position);
|
|
@@ -13123,6 +13178,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
13123
13178
|
if (isSafePythonSubprocessCall(call, pattern, language)) {
|
|
13124
13179
|
continue;
|
|
13125
13180
|
}
|
|
13181
|
+
if (isSafeGoExecCommandCall(call, pattern, language)) {
|
|
13182
|
+
continue;
|
|
13183
|
+
}
|
|
13126
13184
|
if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
|
|
13127
13185
|
continue;
|
|
13128
13186
|
}
|
|
@@ -15544,8 +15602,32 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
|
|
|
15544
15602
|
"BufferedWriter",
|
|
15545
15603
|
"PrintStream",
|
|
15546
15604
|
"PrintWriter",
|
|
15547
|
-
"ObjectOutputStream"
|
|
15605
|
+
"ObjectOutputStream",
|
|
15548
15606
|
// ObjectInputStream IS a sink (deserialization), keep it out
|
|
15607
|
+
// Go database/sql query methods — when parameterised (?, $N, :name),
|
|
15608
|
+
// these are safe boundaries; the sql_injection check governs the unsafe
|
|
15609
|
+
// shape. Without this, parameterised queries fall through to
|
|
15610
|
+
// external_taint_escape. (cognium-dev #102 FP-19a)
|
|
15611
|
+
"Query",
|
|
15612
|
+
"QueryRow",
|
|
15613
|
+
"QueryContext",
|
|
15614
|
+
"QueryRowContext",
|
|
15615
|
+
"Exec",
|
|
15616
|
+
"ExecContext",
|
|
15617
|
+
// Go html/template escape helpers — explicit safe utilities.
|
|
15618
|
+
// Belt-and-suspenders with the sanitizer config so external_taint_escape
|
|
15619
|
+
// is suppressed regardless of class-prefix matching. (cognium-dev #102 FP-27)
|
|
15620
|
+
"EscapeString",
|
|
15621
|
+
"HTMLEscapeString",
|
|
15622
|
+
"JSEscapeString",
|
|
15623
|
+
"URLQueryEscaper",
|
|
15624
|
+
// Go os/exec — when isSafeGoExecCommandCall has already cleared the
|
|
15625
|
+
// command_injection sink (fixed program literal, non-shell), the call
|
|
15626
|
+
// should not fall through to external_taint_escape on the variadic args.
|
|
15627
|
+
// Genuine command_injection is still emitted via the configured sink.
|
|
15628
|
+
// (cognium-dev #102 FP-25)
|
|
15629
|
+
"Command",
|
|
15630
|
+
"CommandContext"
|
|
15549
15631
|
]);
|
|
15550
15632
|
const sanitizerMethods = /* @__PURE__ */ new Set();
|
|
15551
15633
|
for (const san of sanitizers) {
|
|
@@ -21326,14 +21408,19 @@ var GoPlugin = class extends BaseLanguagePlugin {
|
|
|
21326
21408
|
severity: "critical",
|
|
21327
21409
|
argPositions: [0]
|
|
21328
21410
|
},
|
|
21329
|
-
// Command Injection
|
|
21411
|
+
// Command Injection — argPositions intentionally empty so all variadic
|
|
21412
|
+
// positions are scanned. `exec.Command("sh", "-c", taintedCmd)` puts the
|
|
21413
|
+
// injection at arg[2]; `exec.Command(taintedName, args...)` puts it at
|
|
21414
|
+
// arg[0]; `exec.Command("git", taintedFlag)` is argument-injection at
|
|
21415
|
+
// arg[1] (CWE-88). All three shapes are dangerous when any positional
|
|
21416
|
+
// arg is tainted. (cognium-dev #53)
|
|
21330
21417
|
{
|
|
21331
21418
|
method: "Command",
|
|
21332
21419
|
class: "exec",
|
|
21333
21420
|
type: "command_injection",
|
|
21334
21421
|
cwe: "CWE-78",
|
|
21335
21422
|
severity: "critical",
|
|
21336
|
-
argPositions: [
|
|
21423
|
+
argPositions: []
|
|
21337
21424
|
},
|
|
21338
21425
|
{
|
|
21339
21426
|
method: "CommandContext",
|
|
@@ -21341,7 +21428,7 @@ var GoPlugin = class extends BaseLanguagePlugin {
|
|
|
21341
21428
|
type: "command_injection",
|
|
21342
21429
|
cwe: "CWE-78",
|
|
21343
21430
|
severity: "critical",
|
|
21344
|
-
argPositions: [
|
|
21431
|
+
argPositions: []
|
|
21345
21432
|
},
|
|
21346
21433
|
// Path Traversal
|
|
21347
21434
|
{
|
|
@@ -21436,6 +21523,117 @@ var GoPlugin = class extends BaseLanguagePlugin {
|
|
|
21436
21523
|
cwe: "CWE-502",
|
|
21437
21524
|
severity: "medium",
|
|
21438
21525
|
argPositions: [0]
|
|
21526
|
+
},
|
|
21527
|
+
// Log Injection — `log.Printf("event=%s", userInput)` allows
|
|
21528
|
+
// CRLF/log forging when the format string interpolates user data.
|
|
21529
|
+
// CWE-117. (cognium-dev #107)
|
|
21530
|
+
{
|
|
21531
|
+
method: "Print",
|
|
21532
|
+
class: "log",
|
|
21533
|
+
type: "log_injection",
|
|
21534
|
+
cwe: "CWE-117",
|
|
21535
|
+
severity: "medium",
|
|
21536
|
+
argPositions: []
|
|
21537
|
+
},
|
|
21538
|
+
{
|
|
21539
|
+
method: "Println",
|
|
21540
|
+
class: "log",
|
|
21541
|
+
type: "log_injection",
|
|
21542
|
+
cwe: "CWE-117",
|
|
21543
|
+
severity: "medium",
|
|
21544
|
+
argPositions: []
|
|
21545
|
+
},
|
|
21546
|
+
{
|
|
21547
|
+
method: "Printf",
|
|
21548
|
+
class: "log",
|
|
21549
|
+
type: "log_injection",
|
|
21550
|
+
cwe: "CWE-117",
|
|
21551
|
+
severity: "medium",
|
|
21552
|
+
argPositions: []
|
|
21553
|
+
},
|
|
21554
|
+
{
|
|
21555
|
+
method: "Fatal",
|
|
21556
|
+
class: "log",
|
|
21557
|
+
type: "log_injection",
|
|
21558
|
+
cwe: "CWE-117",
|
|
21559
|
+
severity: "medium",
|
|
21560
|
+
argPositions: []
|
|
21561
|
+
},
|
|
21562
|
+
{
|
|
21563
|
+
method: "Fatalln",
|
|
21564
|
+
class: "log",
|
|
21565
|
+
type: "log_injection",
|
|
21566
|
+
cwe: "CWE-117",
|
|
21567
|
+
severity: "medium",
|
|
21568
|
+
argPositions: []
|
|
21569
|
+
},
|
|
21570
|
+
{
|
|
21571
|
+
method: "Fatalf",
|
|
21572
|
+
class: "log",
|
|
21573
|
+
type: "log_injection",
|
|
21574
|
+
cwe: "CWE-117",
|
|
21575
|
+
severity: "medium",
|
|
21576
|
+
argPositions: []
|
|
21577
|
+
},
|
|
21578
|
+
{
|
|
21579
|
+
method: "Panic",
|
|
21580
|
+
class: "log",
|
|
21581
|
+
type: "log_injection",
|
|
21582
|
+
cwe: "CWE-117",
|
|
21583
|
+
severity: "medium",
|
|
21584
|
+
argPositions: []
|
|
21585
|
+
},
|
|
21586
|
+
{
|
|
21587
|
+
method: "Panicln",
|
|
21588
|
+
class: "log",
|
|
21589
|
+
type: "log_injection",
|
|
21590
|
+
cwe: "CWE-117",
|
|
21591
|
+
severity: "medium",
|
|
21592
|
+
argPositions: []
|
|
21593
|
+
},
|
|
21594
|
+
{
|
|
21595
|
+
method: "Panicf",
|
|
21596
|
+
class: "log",
|
|
21597
|
+
type: "log_injection",
|
|
21598
|
+
cwe: "CWE-117",
|
|
21599
|
+
severity: "medium",
|
|
21600
|
+
argPositions: []
|
|
21601
|
+
},
|
|
21602
|
+
// Server-Side Template Injection — `text/template` and `html/template`
|
|
21603
|
+
// template parse-time injection. When the *template source itself*
|
|
21604
|
+
// is tainted (not the data), the rendered output can execute
|
|
21605
|
+
// arbitrary template directives. CWE-94. (cognium-dev #108)
|
|
21606
|
+
{
|
|
21607
|
+
method: "Parse",
|
|
21608
|
+
class: "Template",
|
|
21609
|
+
type: "code_injection",
|
|
21610
|
+
cwe: "CWE-94",
|
|
21611
|
+
severity: "high",
|
|
21612
|
+
argPositions: [0]
|
|
21613
|
+
},
|
|
21614
|
+
{
|
|
21615
|
+
method: "ParseFiles",
|
|
21616
|
+
class: "template",
|
|
21617
|
+
type: "code_injection",
|
|
21618
|
+
cwe: "CWE-94",
|
|
21619
|
+
severity: "high",
|
|
21620
|
+
argPositions: []
|
|
21621
|
+
},
|
|
21622
|
+
{
|
|
21623
|
+
method: "ParseGlob",
|
|
21624
|
+
class: "template",
|
|
21625
|
+
type: "code_injection",
|
|
21626
|
+
cwe: "CWE-94",
|
|
21627
|
+
severity: "high",
|
|
21628
|
+
argPositions: [0]
|
|
21629
|
+
},
|
|
21630
|
+
{
|
|
21631
|
+
method: "ParseFS",
|
|
21632
|
+
class: "template",
|
|
21633
|
+
type: "code_injection",
|
|
21634
|
+
cwe: "CWE-94",
|
|
21635
|
+
severity: "high",
|
|
21636
|
+
argPositions: []
|
|
21439
21637
|
}
|
|
21440
21638
|
];
|
|
21441
21639
|
}
|
|
@@ -22498,6 +22696,11 @@ var LanguageSourcesPass = class {
|
|
|
22498
22696
|
ctx.addFinding(finding);
|
|
22499
22697
|
}
|
|
22500
22698
|
additionalSanitizers.push(...findBashRegexAllowlistSanitizers(code));
|
|
22699
|
+
additionalSanitizers.push(...findBashRealpathPrefixGuardSanitizers(code));
|
|
22700
|
+
}
|
|
22701
|
+
if (language === "go") {
|
|
22702
|
+
additionalSanitizers.push(...findGoMapAllowlistGuardSanitizers(code));
|
|
22703
|
+
additionalSanitizers.push(...findGoHtmlTemplateImportSanitizers(code));
|
|
22501
22704
|
}
|
|
22502
22705
|
attachSourceLineCode(additionalSources, additionalSinks, code);
|
|
22503
22706
|
return { additionalSources, additionalSinks, additionalSanitizers, pyTaintedVars, pySanitizedVars, jsTaintedVars };
|
|
@@ -23361,6 +23564,140 @@ function isSafeBashAllowlistRegex(literal) {
|
|
|
23361
23564
|
}
|
|
23362
23565
|
return consumed === body2.length;
|
|
23363
23566
|
}
|
|
23567
|
+
function findBashRealpathPrefixGuardSanitizers(code) {
|
|
23568
|
+
const sanitizers = [];
|
|
23569
|
+
const lines = code.split("\n");
|
|
23570
|
+
const caseOpen = /^\s*case\s+"?\$\{?\w+\}?"?\s+in\b/;
|
|
23571
|
+
const esacClose = /^\s*esac\b/;
|
|
23572
|
+
const armOpener = /^\s*([^)\s][^)]*?)\)/;
|
|
23573
|
+
const prefixArm = /^(?:"\$\{?\w+\}?"|"[^"]*"|\/[\w\-./]+|\$\{?\w+\}?|[\w\-./]+)(?:\/|\*)/;
|
|
23574
|
+
const catchAllArm = /^(?:\*|\\\*)$/;
|
|
23575
|
+
let i2 = 0;
|
|
23576
|
+
while (i2 < lines.length) {
|
|
23577
|
+
if (!caseOpen.test(lines[i2])) {
|
|
23578
|
+
i2++;
|
|
23579
|
+
continue;
|
|
23580
|
+
}
|
|
23581
|
+
let caseEnd = -1;
|
|
23582
|
+
for (let j = i2 + 1; j < lines.length; j++) {
|
|
23583
|
+
if (esacClose.test(lines[j])) {
|
|
23584
|
+
caseEnd = j;
|
|
23585
|
+
break;
|
|
23586
|
+
}
|
|
23587
|
+
}
|
|
23588
|
+
if (caseEnd === -1) {
|
|
23589
|
+
i2++;
|
|
23590
|
+
continue;
|
|
23591
|
+
}
|
|
23592
|
+
let hasPrefixArm = false;
|
|
23593
|
+
let hasTerminalCatchAll = false;
|
|
23594
|
+
for (let j = i2 + 1; j < caseEnd; j++) {
|
|
23595
|
+
const armMatch = armOpener.exec(lines[j]);
|
|
23596
|
+
if (!armMatch) continue;
|
|
23597
|
+
const pattern = armMatch[1].trim();
|
|
23598
|
+
if (catchAllArm.test(pattern)) {
|
|
23599
|
+
let bodyEnd = caseEnd;
|
|
23600
|
+
for (let k = j + 1; k < caseEnd; k++) {
|
|
23601
|
+
if (armOpener.test(lines[k])) {
|
|
23602
|
+
bodyEnd = k;
|
|
23603
|
+
break;
|
|
23604
|
+
}
|
|
23605
|
+
}
|
|
23606
|
+
const armBody = lines.slice(j, bodyEnd).join(" ");
|
|
23607
|
+
if (/\b(exit|return|die)\b/.test(armBody)) {
|
|
23608
|
+
hasTerminalCatchAll = true;
|
|
23609
|
+
}
|
|
23610
|
+
} else if (prefixArm.test(pattern)) {
|
|
23611
|
+
hasPrefixArm = true;
|
|
23612
|
+
}
|
|
23613
|
+
}
|
|
23614
|
+
if (hasPrefixArm && hasTerminalCatchAll) {
|
|
23615
|
+
for (let l = i2 + 1; l <= caseEnd + 1; l++) {
|
|
23616
|
+
sanitizers.push({
|
|
23617
|
+
type: "realpath_prefix_guard",
|
|
23618
|
+
method: "case",
|
|
23619
|
+
line: l,
|
|
23620
|
+
sanitizes: [
|
|
23621
|
+
"path_traversal",
|
|
23622
|
+
"command_injection",
|
|
23623
|
+
"code_injection",
|
|
23624
|
+
"ssrf",
|
|
23625
|
+
"open_redirect",
|
|
23626
|
+
"log_injection"
|
|
23627
|
+
]
|
|
23628
|
+
});
|
|
23629
|
+
}
|
|
23630
|
+
}
|
|
23631
|
+
i2 = caseEnd + 1;
|
|
23632
|
+
}
|
|
23633
|
+
return sanitizers;
|
|
23634
|
+
}
|
|
23635
|
+
function findGoMapAllowlistGuardSanitizers(code) {
|
|
23636
|
+
const sanitizers = [];
|
|
23637
|
+
const lines = code.split("\n");
|
|
23638
|
+
const guardOpen = /^\s*if\s+!\s*([A-Za-z_][A-Za-z0-9_]*)\s*\[\s*[A-Za-z_][A-Za-z0-9_]*\s*\]\s*\{/;
|
|
23639
|
+
const allowlistName = /^(?:[A-Z][A-Z0-9_]+|.*?(allowed|accepted|whitelist|permitted|valid|approved).*)$/i;
|
|
23640
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
23641
|
+
const m = guardOpen.exec(lines[i2]);
|
|
23642
|
+
if (!m) continue;
|
|
23643
|
+
const mapName = m[1];
|
|
23644
|
+
if (!allowlistName.test(mapName)) continue;
|
|
23645
|
+
let depth = 1;
|
|
23646
|
+
let closeLine = -1;
|
|
23647
|
+
let bodyHasTerminator = false;
|
|
23648
|
+
const maxScan = Math.min(lines.length, i2 + 26);
|
|
23649
|
+
for (let j = i2 + 1; j < maxScan; j++) {
|
|
23650
|
+
const line = lines[j];
|
|
23651
|
+
if (/\b(return|panic\s*\(|os\.Exit\s*\()/.test(line)) {
|
|
23652
|
+
bodyHasTerminator = true;
|
|
23653
|
+
}
|
|
23654
|
+
for (const ch of line) {
|
|
23655
|
+
if (ch === "{") depth++;
|
|
23656
|
+
else if (ch === "}") depth--;
|
|
23657
|
+
}
|
|
23658
|
+
if (depth === 0) {
|
|
23659
|
+
closeLine = j;
|
|
23660
|
+
break;
|
|
23661
|
+
}
|
|
23662
|
+
}
|
|
23663
|
+
if (closeLine === -1 || !bodyHasTerminator) continue;
|
|
23664
|
+
for (let l = closeLine + 2; l <= lines.length; l++) {
|
|
23665
|
+
sanitizers.push({
|
|
23666
|
+
type: "go_map_allowlist_guard",
|
|
23667
|
+
method: "if",
|
|
23668
|
+
line: l,
|
|
23669
|
+
sanitizes: [
|
|
23670
|
+
"ssrf",
|
|
23671
|
+
"open_redirect",
|
|
23672
|
+
"path_traversal",
|
|
23673
|
+
"sql_injection",
|
|
23674
|
+
"command_injection",
|
|
23675
|
+
"external_taint_escape"
|
|
23676
|
+
]
|
|
23677
|
+
});
|
|
23678
|
+
}
|
|
23679
|
+
}
|
|
23680
|
+
return sanitizers;
|
|
23681
|
+
}
|
|
23682
|
+
function findGoHtmlTemplateImportSanitizers(code) {
|
|
23683
|
+
const sanitizers = [];
|
|
23684
|
+
const hasHtmlTemplate = /["\s]html\/template["\s]/.test(code);
|
|
23685
|
+
const hasTextTemplate = /["\s]text\/template["\s]/.test(code);
|
|
23686
|
+
if (!hasHtmlTemplate) return sanitizers;
|
|
23687
|
+
if (hasTextTemplate) return sanitizers;
|
|
23688
|
+
const lines = code.split("\n");
|
|
23689
|
+
const execCall = /\.(Execute|ExecuteTemplate)\s*\(/;
|
|
23690
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
23691
|
+
if (!execCall.test(lines[i2])) continue;
|
|
23692
|
+
sanitizers.push({
|
|
23693
|
+
type: "html_template_auto_escape",
|
|
23694
|
+
method: "Execute",
|
|
23695
|
+
line: i2 + 1,
|
|
23696
|
+
sanitizes: ["xss", "external_taint_escape", "open_redirect"]
|
|
23697
|
+
});
|
|
23698
|
+
}
|
|
23699
|
+
return sanitizers;
|
|
23700
|
+
}
|
|
23364
23701
|
|
|
23365
23702
|
// src/analysis/passes/sink-filter-pass.ts
|
|
23366
23703
|
var JS_XSS_SANITIZERS = [
|
|
@@ -23858,6 +24195,38 @@ var TaintPropagationPass = class {
|
|
|
23858
24195
|
}
|
|
23859
24196
|
return true;
|
|
23860
24197
|
});
|
|
24198
|
+
if (sanitizers && sanitizers.length > 0) {
|
|
24199
|
+
const sanitizersByLine = /* @__PURE__ */ new Map();
|
|
24200
|
+
for (const san of sanitizers) {
|
|
24201
|
+
const arr = sanitizersByLine.get(san.line) ?? [];
|
|
24202
|
+
arr.push(san);
|
|
24203
|
+
sanitizersByLine.set(san.line, arr);
|
|
24204
|
+
}
|
|
24205
|
+
finalFlows = finalFlows.filter((f) => {
|
|
24206
|
+
if (f.sink_type === "external_taint_escape") {
|
|
24207
|
+
const lo = Math.min(f.source_line, f.sink_line);
|
|
24208
|
+
const hi = Math.max(f.source_line, f.sink_line);
|
|
24209
|
+
for (let line = lo; line <= hi; line++) {
|
|
24210
|
+
const sansAtLine = sanitizersByLine.get(line);
|
|
24211
|
+
if (!sansAtLine) continue;
|
|
24212
|
+
for (const san of sansAtLine) {
|
|
24213
|
+
if (san.sanitizes.includes(f.sink_type)) {
|
|
24214
|
+
return false;
|
|
24215
|
+
}
|
|
24216
|
+
}
|
|
24217
|
+
}
|
|
24218
|
+
return true;
|
|
24219
|
+
}
|
|
24220
|
+
const sansAtSink = sanitizersByLine.get(f.sink_line);
|
|
24221
|
+
if (!sansAtSink || sansAtSink.length === 0) return true;
|
|
24222
|
+
for (const san of sansAtSink) {
|
|
24223
|
+
if (san.sanitizes.includes(f.sink_type)) {
|
|
24224
|
+
return false;
|
|
24225
|
+
}
|
|
24226
|
+
}
|
|
24227
|
+
return true;
|
|
24228
|
+
});
|
|
24229
|
+
}
|
|
23861
24230
|
if (ctx.language === "java" && typeof ctx.code === "string") {
|
|
23862
24231
|
finalFlows = finalFlows.filter((f) => {
|
|
23863
24232
|
if (f.sink_type !== "path_traversal" && f.sink_type !== "xxe") return true;
|
|
@@ -24549,7 +24918,47 @@ var InterproceduralPass = class {
|
|
|
24549
24918
|
if (additionalSinks.length > 0) {
|
|
24550
24919
|
attachSourceLineCode([], additionalSinks, ctx.code);
|
|
24551
24920
|
}
|
|
24552
|
-
|
|
24921
|
+
let filteredAdditionalFlows = additionalFlows;
|
|
24922
|
+
let filteredAdditionalSinks = additionalSinks;
|
|
24923
|
+
if (sanitizers && sanitizers.length > 0) {
|
|
24924
|
+
const sanitizersByLine = /* @__PURE__ */ new Map();
|
|
24925
|
+
for (const san of sanitizers) {
|
|
24926
|
+
const arr = sanitizersByLine.get(san.line) ?? [];
|
|
24927
|
+
arr.push(san);
|
|
24928
|
+
sanitizersByLine.set(san.line, arr);
|
|
24929
|
+
}
|
|
24930
|
+
const sanitizedSinkKeys = /* @__PURE__ */ new Set();
|
|
24931
|
+
filteredAdditionalFlows = additionalFlows.filter((f) => {
|
|
24932
|
+
if (f.sink_type === "external_taint_escape") {
|
|
24933
|
+
const lo = Math.min(f.source_line, f.sink_line);
|
|
24934
|
+
const hi = Math.max(f.source_line, f.sink_line);
|
|
24935
|
+
for (let line = lo; line <= hi; line++) {
|
|
24936
|
+
const sansAtLine = sanitizersByLine.get(line);
|
|
24937
|
+
if (!sansAtLine) continue;
|
|
24938
|
+
for (const san of sansAtLine) {
|
|
24939
|
+
if (san.sanitizes.includes(f.sink_type)) {
|
|
24940
|
+
sanitizedSinkKeys.add(`${f.sink_line}:${f.sink_type}`);
|
|
24941
|
+
return false;
|
|
24942
|
+
}
|
|
24943
|
+
}
|
|
24944
|
+
}
|
|
24945
|
+
return true;
|
|
24946
|
+
}
|
|
24947
|
+
const sansAtSink = sanitizersByLine.get(f.sink_line);
|
|
24948
|
+
if (!sansAtSink || sansAtSink.length === 0) return true;
|
|
24949
|
+
for (const san of sansAtSink) {
|
|
24950
|
+
if (san.sanitizes.includes(f.sink_type)) {
|
|
24951
|
+
return false;
|
|
24952
|
+
}
|
|
24953
|
+
}
|
|
24954
|
+
return true;
|
|
24955
|
+
});
|
|
24956
|
+
filteredAdditionalSinks = additionalSinks.filter((s) => {
|
|
24957
|
+
if (s.type !== "external_taint_escape") return true;
|
|
24958
|
+
return !sanitizedSinkKeys.has(`${s.line}:${s.type}`);
|
|
24959
|
+
});
|
|
24960
|
+
}
|
|
24961
|
+
return { additionalSinks: filteredAdditionalSinks, additionalFlows: filteredAdditionalFlows, interprocedural };
|
|
24553
24962
|
}
|
|
24554
24963
|
};
|
|
24555
24964
|
|
|
@@ -11884,11 +11884,21 @@ var DEFAULT_SANITIZERS = [
|
|
|
11884
11884
|
// (defense-in-depth — mirrors Java getCanonicalPath in this table; the
|
|
11885
11885
|
// stricter Clean+HasPrefix guard recognition is tracked separately).
|
|
11886
11886
|
// EvalSymlinks is the Go equivalent of Java's Path.toRealPath.
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
{ method: "
|
|
11890
|
-
{ method: "
|
|
11891
|
-
{ method: "
|
|
11887
|
+
// Sprint 24 (#102 FP-27): broadened to cover external_taint_escape (CWE-668)
|
|
11888
|
+
// fallback so canonicalised paths don't trigger the synthetic sink.
|
|
11889
|
+
{ method: "Base", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
|
|
11890
|
+
{ method: "Base", class: "path", removes: ["path_traversal", "external_taint_escape"] },
|
|
11891
|
+
{ method: "Clean", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
|
|
11892
|
+
{ method: "Clean", class: "path", removes: ["path_traversal", "external_taint_escape"] },
|
|
11893
|
+
{ method: "EvalSymlinks", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
|
|
11894
|
+
// Go html/template escape helpers (#102 FP-27) — registered explicitly because
|
|
11895
|
+
// configs/sinks/golang.json is not loaded at runtime.
|
|
11896
|
+
{ method: "EscapeString", class: "html", removes: ["xss", "external_taint_escape", "log_injection", "open_redirect"] },
|
|
11897
|
+
{ method: "HTMLEscapeString", class: "template", removes: ["xss", "external_taint_escape", "log_injection", "open_redirect"] },
|
|
11898
|
+
{ method: "JSEscapeString", class: "template", removes: ["xss", "external_taint_escape", "log_injection"] },
|
|
11899
|
+
{ method: "URLQueryEscaper", class: "template", removes: ["xss", "external_taint_escape", "open_redirect"] },
|
|
11900
|
+
{ method: "QueryEscape", class: "url", removes: ["xss", "external_taint_escape", "open_redirect"] },
|
|
11901
|
+
{ method: "PathEscape", class: "url", removes: ["xss", "external_taint_escape", "open_redirect"] },
|
|
11892
11902
|
// Log Injection sanitizers
|
|
11893
11903
|
{ method: "replace", removes: ["log_injection"] },
|
|
11894
11904
|
// Used to remove newlines/control chars
|
|
@@ -12280,6 +12290,15 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
12280
12290
|
s.variable = m[1];
|
|
12281
12291
|
}
|
|
12282
12292
|
}
|
|
12293
|
+
if (language === "go" && sourceLines) {
|
|
12294
|
+
const GO_ASSIGN_LHS = /^\s*(?:var\s+)?([A-Za-z_]\w*)(?:\s*,\s*[A-Za-z_]\w*)*\s*(?::\s*[A-Za-z_][\w.]*\s*)?(?::?=)(?!=)/;
|
|
12295
|
+
for (const s of result) {
|
|
12296
|
+
if (s.variable && s.variable.length > 0) continue;
|
|
12297
|
+
const lineText = sourceLines[s.line - 1] ?? "";
|
|
12298
|
+
const m = GO_ASSIGN_LHS.exec(lineText);
|
|
12299
|
+
if (m) s.variable = m[1];
|
|
12300
|
+
}
|
|
12301
|
+
}
|
|
12283
12302
|
return result;
|
|
12284
12303
|
}
|
|
12285
12304
|
function isInterproceduralTaintableType(typeName) {
|
|
@@ -12399,6 +12418,42 @@ function isSafePythonSubprocessCall(call, pattern, language) {
|
|
|
12399
12418
|
}
|
|
12400
12419
|
return true;
|
|
12401
12420
|
}
|
|
12421
|
+
function isSafeGoExecCommandCall(call, pattern, language) {
|
|
12422
|
+
if (language !== "go") return false;
|
|
12423
|
+
if (pattern.type !== "command_injection") return false;
|
|
12424
|
+
if (pattern.class !== "exec") return false;
|
|
12425
|
+
if (pattern.method !== "Command" && pattern.method !== "CommandContext") return false;
|
|
12426
|
+
const programArgPos = pattern.method === "CommandContext" ? 1 : 0;
|
|
12427
|
+
const programArg = call.arguments.find((a) => a.position === programArgPos);
|
|
12428
|
+
if (!programArg) return false;
|
|
12429
|
+
let program;
|
|
12430
|
+
if (programArg.literal !== null && programArg.literal !== void 0) {
|
|
12431
|
+
program = String(programArg.literal).split("/").pop() ?? String(programArg.literal);
|
|
12432
|
+
} else {
|
|
12433
|
+
const expr = (programArg.expression ?? "").trim();
|
|
12434
|
+
if (!(expr.startsWith('"') || expr.startsWith("`") || expr.startsWith("'"))) {
|
|
12435
|
+
return false;
|
|
12436
|
+
}
|
|
12437
|
+
const stripped = expr.slice(1, -1);
|
|
12438
|
+
program = stripped.split("/").pop() ?? stripped;
|
|
12439
|
+
}
|
|
12440
|
+
const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
|
|
12441
|
+
"sh",
|
|
12442
|
+
"bash",
|
|
12443
|
+
"zsh",
|
|
12444
|
+
"dash",
|
|
12445
|
+
"ash",
|
|
12446
|
+
"ksh",
|
|
12447
|
+
"cmd",
|
|
12448
|
+
"cmd.exe",
|
|
12449
|
+
"powershell",
|
|
12450
|
+
"pwsh",
|
|
12451
|
+
"powershell.exe",
|
|
12452
|
+
"pwsh.exe"
|
|
12453
|
+
]);
|
|
12454
|
+
if (SHELL_PROGRAMS.has(program)) return false;
|
|
12455
|
+
return true;
|
|
12456
|
+
}
|
|
12402
12457
|
var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
|
|
12403
12458
|
function argIsClassLiteral(call, position) {
|
|
12404
12459
|
const arg = call.arguments.find((a) => a.position === position);
|
|
@@ -12418,6 +12473,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
12418
12473
|
if (isSafePythonSubprocessCall(call, pattern, language)) {
|
|
12419
12474
|
continue;
|
|
12420
12475
|
}
|
|
12476
|
+
if (isSafeGoExecCommandCall(call, pattern, language)) {
|
|
12477
|
+
continue;
|
|
12478
|
+
}
|
|
12421
12479
|
if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
|
|
12422
12480
|
continue;
|
|
12423
12481
|
}
|