circle-ir 3.181.0 → 3.182.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/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +158 -0
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +47 -11
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/browser/circle-ir.js +186 -8
- package/dist/core/circle-ir-core.cjs +84 -8
- package/dist/core/circle-ir-core.js +84 -8
- package/dist/core/extractors/dfg.js +99 -0
- package/dist/core/extractors/dfg.js.map +1 -1
- package/package.json +1 -1
|
@@ -10020,6 +10020,10 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
|
10020
10020
|
} else if (child.type === "for_statement") {
|
|
10021
10021
|
const rangeClause = findChildByTypeGo(child, "range_clause");
|
|
10022
10022
|
if (rangeClause) {
|
|
10023
|
+
const right = rangeClause.childForFieldName("right");
|
|
10024
|
+
if (right) {
|
|
10025
|
+
extractGoUses(right, uses, scopeStack);
|
|
10026
|
+
}
|
|
10023
10027
|
const left = rangeClause.childForFieldName("left");
|
|
10024
10028
|
if (left) {
|
|
10025
10029
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
@@ -10027,6 +10031,7 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
|
10027
10031
|
}
|
|
10028
10032
|
} else if (child.type === "call_expression") {
|
|
10029
10033
|
extractGoUses(child, uses, scopeStack);
|
|
10034
|
+
recordGoOpaqueCodecDestDef(child, defs, scopeStack);
|
|
10030
10035
|
} else if (child.type === "return_statement") {
|
|
10031
10036
|
for (let i2 = 0; i2 < child.childCount; i2++) {
|
|
10032
10037
|
const expr = child.child(i2);
|
|
@@ -10119,6 +10124,63 @@ function findChildByTypeGo(node, type) {
|
|
|
10119
10124
|
}
|
|
10120
10125
|
return null;
|
|
10121
10126
|
}
|
|
10127
|
+
var GO_OPAQUE_CODEC_METHODS = /* @__PURE__ */ new Set([
|
|
10128
|
+
"Unmarshal",
|
|
10129
|
+
// json/xml/yaml/toml/gob
|
|
10130
|
+
"Decode",
|
|
10131
|
+
// json.NewDecoder(r).Decode(&dest), gob.Decoder.Decode
|
|
10132
|
+
"NewDecoder",
|
|
10133
|
+
// wrapper; handled via chained Decode above
|
|
10134
|
+
"UnmarshalYAML",
|
|
10135
|
+
"UnmarshalJSON",
|
|
10136
|
+
"UnmarshalText",
|
|
10137
|
+
"UnmarshalBinary"
|
|
10138
|
+
]);
|
|
10139
|
+
function recordGoOpaqueCodecDestDef(call, defs, scopeStack) {
|
|
10140
|
+
const fn = call.childForFieldName("function");
|
|
10141
|
+
if (!fn || fn.type !== "selector_expression") return;
|
|
10142
|
+
const fieldNode = fn.childForFieldName("field");
|
|
10143
|
+
if (!fieldNode) return;
|
|
10144
|
+
const method = getNodeText(fieldNode);
|
|
10145
|
+
if (!GO_OPAQUE_CODEC_METHODS.has(method)) return;
|
|
10146
|
+
const argsNode = call.childForFieldName("arguments");
|
|
10147
|
+
if (!argsNode) return;
|
|
10148
|
+
const args2 = [];
|
|
10149
|
+
for (let i2 = 0; i2 < argsNode.childCount; i2++) {
|
|
10150
|
+
const c = argsNode.child(i2);
|
|
10151
|
+
if (!c) continue;
|
|
10152
|
+
if (c.type === "," || c.type === "(" || c.type === ")") continue;
|
|
10153
|
+
args2.push(c);
|
|
10154
|
+
}
|
|
10155
|
+
if (args2.length === 0) return;
|
|
10156
|
+
const destArg = args2.length >= 2 ? args2[1] : args2[0];
|
|
10157
|
+
const destName = extractGoAddressableVarName(destArg);
|
|
10158
|
+
if (!destName || destName === "_") return;
|
|
10159
|
+
const line = call.startPosition.row + 1;
|
|
10160
|
+
const def = {
|
|
10161
|
+
id: defs.length + 1,
|
|
10162
|
+
variable: destName,
|
|
10163
|
+
kind: "local",
|
|
10164
|
+
line
|
|
10165
|
+
};
|
|
10166
|
+
defs.push(def);
|
|
10167
|
+
currentScope(scopeStack).set(destName, def.id);
|
|
10168
|
+
}
|
|
10169
|
+
function extractGoAddressableVarName(node) {
|
|
10170
|
+
if (node.type === "unary_expression") {
|
|
10171
|
+
const operand = node.childForFieldName("operand") ?? node.child(1) ?? null;
|
|
10172
|
+
if (operand) return extractGoAddressableVarName(operand);
|
|
10173
|
+
return null;
|
|
10174
|
+
}
|
|
10175
|
+
if (node.type === "identifier") {
|
|
10176
|
+
return getNodeText(node);
|
|
10177
|
+
}
|
|
10178
|
+
if (node.type === "selector_expression") {
|
|
10179
|
+
const field = node.childForFieldName("field");
|
|
10180
|
+
if (field) return getNodeText(field);
|
|
10181
|
+
}
|
|
10182
|
+
return null;
|
|
10183
|
+
}
|
|
10122
10184
|
|
|
10123
10185
|
// src/core/extractors/runtime-registrations.ts
|
|
10124
10186
|
var HTTP_VERB_METHODS = /* @__PURE__ */ new Set([
|
|
@@ -17002,15 +17064,21 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
17002
17064
|
const callsAtSink = callsByLine.get(sink.line) ?? [];
|
|
17003
17065
|
for (const call of callsAtSink) {
|
|
17004
17066
|
for (const arg of call.arguments) {
|
|
17005
|
-
if (
|
|
17006
|
-
if (sink.argPositions
|
|
17007
|
-
|
|
17008
|
-
continue;
|
|
17009
|
-
}
|
|
17067
|
+
if (sink.argPositions && sink.argPositions.length > 0) {
|
|
17068
|
+
if (!sink.argPositions.includes(arg.position)) {
|
|
17069
|
+
continue;
|
|
17010
17070
|
}
|
|
17011
|
-
|
|
17012
|
-
|
|
17071
|
+
}
|
|
17072
|
+
const candidateUses = arg.variable ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
|
|
17073
|
+
{
|
|
17074
|
+
for (const use of candidateUses) {
|
|
17075
|
+
if (use.def_id !== null) {
|
|
17013
17076
|
if (allTaintedDefIds.has(use.def_id)) {
|
|
17077
|
+
if (!arg.variable) {
|
|
17078
|
+
if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
|
|
17079
|
+
const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
|
|
17080
|
+
if (!re.test(arg.expression)) continue;
|
|
17081
|
+
}
|
|
17014
17082
|
const taintInfo = taintByDefId.get(use.def_id);
|
|
17015
17083
|
if (taintInfo) {
|
|
17016
17084
|
const isSanitized = checkSanitized(
|
|
@@ -17048,7 +17116,15 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
17048
17116
|
}
|
|
17049
17117
|
}
|
|
17050
17118
|
}
|
|
17051
|
-
|
|
17119
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17120
|
+
const deduped = [];
|
|
17121
|
+
for (const f of flows) {
|
|
17122
|
+
const key = `${f.source.line}|${f.sink.line}|${f.sink.type}|${f.path.map((p) => p.variable).join(">")}`;
|
|
17123
|
+
if (seen.has(key)) continue;
|
|
17124
|
+
seen.add(key);
|
|
17125
|
+
deduped.push(f);
|
|
17126
|
+
}
|
|
17127
|
+
return { taintedVars, flows: deduped, reachableSinks };
|
|
17052
17128
|
}
|
|
17053
17129
|
function findInitialTaint(sources, callsByLine, defsByLine) {
|
|
17054
17130
|
const tainted = [];
|
|
@@ -33641,6 +33717,18 @@ var TaintPropagationPass = class {
|
|
|
33641
33717
|
if (isFP) continue;
|
|
33642
33718
|
pushIfNew(f);
|
|
33643
33719
|
}
|
|
33720
|
+
if (ctx.language === "go" && typeof ctx.code === "string") {
|
|
33721
|
+
const goGlobalFlows = detectGoPackageGlobalFlows(
|
|
33722
|
+
ctx.code,
|
|
33723
|
+
calls,
|
|
33724
|
+
sources,
|
|
33725
|
+
sinks,
|
|
33726
|
+
constProp.unreachableLines
|
|
33727
|
+
) ?? [];
|
|
33728
|
+
for (const f of goGlobalFlows) {
|
|
33729
|
+
pushIfNew(f);
|
|
33730
|
+
}
|
|
33731
|
+
}
|
|
33644
33732
|
const sanitizedNames = constProp.sanitizedVars;
|
|
33645
33733
|
let finalFlows = sanitizedNames.size === 0 ? flows : flows.filter((f) => {
|
|
33646
33734
|
if (f.path.length === 0) return true;
|
|
@@ -33855,6 +33943,96 @@ function isInJavaSanitizedMethod(code, types, sinkLine, sinkType) {
|
|
|
33855
33943
|
}
|
|
33856
33944
|
return false;
|
|
33857
33945
|
}
|
|
33946
|
+
function detectGoPackageGlobalFlows(code, _calls, sources, sinks, unreachableLines) {
|
|
33947
|
+
const flows = [];
|
|
33948
|
+
if (!code || sinks.length === 0) return flows;
|
|
33949
|
+
const lines = code.split("\n");
|
|
33950
|
+
const packageVars = /* @__PURE__ */ new Set();
|
|
33951
|
+
let inVarBlock = false;
|
|
33952
|
+
for (const line of lines) {
|
|
33953
|
+
const stripped = line.replace(/\s+$/, "");
|
|
33954
|
+
if (!stripped) continue;
|
|
33955
|
+
if (/^[ \t]/.test(line)) {
|
|
33956
|
+
if (inVarBlock) {
|
|
33957
|
+
const m = stripped.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s+/);
|
|
33958
|
+
if (m) packageVars.add(m[1]);
|
|
33959
|
+
if (/^\s*\)/.test(stripped)) inVarBlock = false;
|
|
33960
|
+
}
|
|
33961
|
+
continue;
|
|
33962
|
+
}
|
|
33963
|
+
if (/^var\s*\(/.test(stripped)) {
|
|
33964
|
+
inVarBlock = true;
|
|
33965
|
+
continue;
|
|
33966
|
+
}
|
|
33967
|
+
if (inVarBlock && /^\)/.test(stripped)) {
|
|
33968
|
+
inVarBlock = false;
|
|
33969
|
+
continue;
|
|
33970
|
+
}
|
|
33971
|
+
const varOne = stripped.match(/^var\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
|
|
33972
|
+
if (varOne) packageVars.add(varOne[1]);
|
|
33973
|
+
}
|
|
33974
|
+
if (packageVars.size === 0) return flows;
|
|
33975
|
+
const sourceVarNames = /* @__PURE__ */ new Set();
|
|
33976
|
+
for (const s of sources) {
|
|
33977
|
+
if (typeof s.variable === "string" && s.variable.length > 0) {
|
|
33978
|
+
sourceVarNames.add(s.variable);
|
|
33979
|
+
}
|
|
33980
|
+
}
|
|
33981
|
+
const GO_SOURCE_RE = /\b(r\.(?:URL\.Query|Form|PostForm|Header|Cookie|Body)|mux\.Vars|c\.(?:Query|Param|PostForm|GetHeader|Cookie)|ctx\.(?:Query|Param|PostForm|GetHeader|Cookie))\b/;
|
|
33982
|
+
const writes = [];
|
|
33983
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
33984
|
+
const line = lines[i2];
|
|
33985
|
+
if (unreachableLines.has(i2 + 1)) continue;
|
|
33986
|
+
if (/:=/.test(line)) continue;
|
|
33987
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/);
|
|
33988
|
+
if (!m) continue;
|
|
33989
|
+
const [, lhs, rhs] = m;
|
|
33990
|
+
if (!packageVars.has(lhs)) continue;
|
|
33991
|
+
let taintedBy = null;
|
|
33992
|
+
if (GO_SOURCE_RE.test(rhs)) {
|
|
33993
|
+
const src = sources.find((s) => s.line === i2 + 1);
|
|
33994
|
+
taintedBy = src ? { line: src.line, type: src.type } : { line: i2 + 1, type: "http_param" };
|
|
33995
|
+
} else {
|
|
33996
|
+
for (const v of sourceVarNames) {
|
|
33997
|
+
const re = new RegExp(`(?<![A-Za-z0-9_$])${v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`);
|
|
33998
|
+
if (re.test(rhs)) {
|
|
33999
|
+
const src = sources.find((s) => s.variable === v);
|
|
34000
|
+
if (src) {
|
|
34001
|
+
taintedBy = { line: src.line, type: src.type };
|
|
34002
|
+
break;
|
|
34003
|
+
}
|
|
34004
|
+
}
|
|
34005
|
+
}
|
|
34006
|
+
}
|
|
34007
|
+
if (!taintedBy) continue;
|
|
34008
|
+
writes.push({ varName: lhs, line: i2 + 1, sourceLine: taintedBy.line, sourceType: taintedBy.type });
|
|
34009
|
+
}
|
|
34010
|
+
if (writes.length === 0) return flows;
|
|
34011
|
+
for (const sink of sinks) {
|
|
34012
|
+
if (unreachableLines.has(sink.line)) continue;
|
|
34013
|
+
const sinkLineText = lines[sink.line - 1] ?? "";
|
|
34014
|
+
if (!sinkLineText) continue;
|
|
34015
|
+
for (const w of writes) {
|
|
34016
|
+
if (w.line >= sink.line) continue;
|
|
34017
|
+
const re = new RegExp(`(?<![A-Za-z0-9_$])${w.varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`);
|
|
34018
|
+
if (!re.test(sinkLineText)) continue;
|
|
34019
|
+
flows.push({
|
|
34020
|
+
source_line: w.sourceLine,
|
|
34021
|
+
sink_line: sink.line,
|
|
34022
|
+
source_type: w.sourceType,
|
|
34023
|
+
sink_type: sink.type,
|
|
34024
|
+
path: [
|
|
34025
|
+
{ variable: w.varName, line: w.sourceLine, type: "source" },
|
|
34026
|
+
{ variable: w.varName, line: w.line, type: "assignment" },
|
|
34027
|
+
{ variable: w.varName, line: sink.line, type: "sink" }
|
|
34028
|
+
],
|
|
34029
|
+
confidence: 0.9,
|
|
34030
|
+
sanitized: false
|
|
34031
|
+
});
|
|
34032
|
+
}
|
|
34033
|
+
}
|
|
34034
|
+
return flows;
|
|
34035
|
+
}
|
|
33858
34036
|
function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines, code, types) {
|
|
33859
34037
|
const flows = [];
|
|
33860
34038
|
const callsByLine = /* @__PURE__ */ new Map();
|
|
@@ -10066,6 +10066,10 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
|
10066
10066
|
} else if (child.type === "for_statement") {
|
|
10067
10067
|
const rangeClause = findChildByTypeGo(child, "range_clause");
|
|
10068
10068
|
if (rangeClause) {
|
|
10069
|
+
const right = rangeClause.childForFieldName("right");
|
|
10070
|
+
if (right) {
|
|
10071
|
+
extractGoUses(right, uses, scopeStack);
|
|
10072
|
+
}
|
|
10069
10073
|
const left = rangeClause.childForFieldName("left");
|
|
10070
10074
|
if (left) {
|
|
10071
10075
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
@@ -10073,6 +10077,7 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
|
10073
10077
|
}
|
|
10074
10078
|
} else if (child.type === "call_expression") {
|
|
10075
10079
|
extractGoUses(child, uses, scopeStack);
|
|
10080
|
+
recordGoOpaqueCodecDestDef(child, defs, scopeStack);
|
|
10076
10081
|
} else if (child.type === "return_statement") {
|
|
10077
10082
|
for (let i2 = 0; i2 < child.childCount; i2++) {
|
|
10078
10083
|
const expr = child.child(i2);
|
|
@@ -10165,6 +10170,63 @@ function findChildByTypeGo(node, type) {
|
|
|
10165
10170
|
}
|
|
10166
10171
|
return null;
|
|
10167
10172
|
}
|
|
10173
|
+
var GO_OPAQUE_CODEC_METHODS = /* @__PURE__ */ new Set([
|
|
10174
|
+
"Unmarshal",
|
|
10175
|
+
// json/xml/yaml/toml/gob
|
|
10176
|
+
"Decode",
|
|
10177
|
+
// json.NewDecoder(r).Decode(&dest), gob.Decoder.Decode
|
|
10178
|
+
"NewDecoder",
|
|
10179
|
+
// wrapper; handled via chained Decode above
|
|
10180
|
+
"UnmarshalYAML",
|
|
10181
|
+
"UnmarshalJSON",
|
|
10182
|
+
"UnmarshalText",
|
|
10183
|
+
"UnmarshalBinary"
|
|
10184
|
+
]);
|
|
10185
|
+
function recordGoOpaqueCodecDestDef(call, defs, scopeStack) {
|
|
10186
|
+
const fn = call.childForFieldName("function");
|
|
10187
|
+
if (!fn || fn.type !== "selector_expression") return;
|
|
10188
|
+
const fieldNode = fn.childForFieldName("field");
|
|
10189
|
+
if (!fieldNode) return;
|
|
10190
|
+
const method = getNodeText(fieldNode);
|
|
10191
|
+
if (!GO_OPAQUE_CODEC_METHODS.has(method)) return;
|
|
10192
|
+
const argsNode = call.childForFieldName("arguments");
|
|
10193
|
+
if (!argsNode) return;
|
|
10194
|
+
const args2 = [];
|
|
10195
|
+
for (let i2 = 0; i2 < argsNode.childCount; i2++) {
|
|
10196
|
+
const c = argsNode.child(i2);
|
|
10197
|
+
if (!c) continue;
|
|
10198
|
+
if (c.type === "," || c.type === "(" || c.type === ")") continue;
|
|
10199
|
+
args2.push(c);
|
|
10200
|
+
}
|
|
10201
|
+
if (args2.length === 0) return;
|
|
10202
|
+
const destArg = args2.length >= 2 ? args2[1] : args2[0];
|
|
10203
|
+
const destName = extractGoAddressableVarName(destArg);
|
|
10204
|
+
if (!destName || destName === "_") return;
|
|
10205
|
+
const line = call.startPosition.row + 1;
|
|
10206
|
+
const def = {
|
|
10207
|
+
id: defs.length + 1,
|
|
10208
|
+
variable: destName,
|
|
10209
|
+
kind: "local",
|
|
10210
|
+
line
|
|
10211
|
+
};
|
|
10212
|
+
defs.push(def);
|
|
10213
|
+
currentScope(scopeStack).set(destName, def.id);
|
|
10214
|
+
}
|
|
10215
|
+
function extractGoAddressableVarName(node) {
|
|
10216
|
+
if (node.type === "unary_expression") {
|
|
10217
|
+
const operand = node.childForFieldName("operand") ?? node.child(1) ?? null;
|
|
10218
|
+
if (operand) return extractGoAddressableVarName(operand);
|
|
10219
|
+
return null;
|
|
10220
|
+
}
|
|
10221
|
+
if (node.type === "identifier") {
|
|
10222
|
+
return getNodeText(node);
|
|
10223
|
+
}
|
|
10224
|
+
if (node.type === "selector_expression") {
|
|
10225
|
+
const field = node.childForFieldName("field");
|
|
10226
|
+
if (field) return getNodeText(field);
|
|
10227
|
+
}
|
|
10228
|
+
return null;
|
|
10229
|
+
}
|
|
10168
10230
|
|
|
10169
10231
|
// src/analysis/config-loader.ts
|
|
10170
10232
|
function parseConfig(content) {
|
|
@@ -14987,15 +15049,21 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
14987
15049
|
const callsAtSink = callsByLine.get(sink.line) ?? [];
|
|
14988
15050
|
for (const call of callsAtSink) {
|
|
14989
15051
|
for (const arg of call.arguments) {
|
|
14990
|
-
if (
|
|
14991
|
-
if (sink.argPositions
|
|
14992
|
-
|
|
14993
|
-
continue;
|
|
14994
|
-
}
|
|
15052
|
+
if (sink.argPositions && sink.argPositions.length > 0) {
|
|
15053
|
+
if (!sink.argPositions.includes(arg.position)) {
|
|
15054
|
+
continue;
|
|
14995
15055
|
}
|
|
14996
|
-
|
|
14997
|
-
|
|
15056
|
+
}
|
|
15057
|
+
const candidateUses = arg.variable ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
|
|
15058
|
+
{
|
|
15059
|
+
for (const use of candidateUses) {
|
|
15060
|
+
if (use.def_id !== null) {
|
|
14998
15061
|
if (allTaintedDefIds.has(use.def_id)) {
|
|
15062
|
+
if (!arg.variable) {
|
|
15063
|
+
if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
|
|
15064
|
+
const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
|
|
15065
|
+
if (!re.test(arg.expression)) continue;
|
|
15066
|
+
}
|
|
14999
15067
|
const taintInfo = taintByDefId.get(use.def_id);
|
|
15000
15068
|
if (taintInfo) {
|
|
15001
15069
|
const isSanitized = checkSanitized(
|
|
@@ -15033,7 +15101,15 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
15033
15101
|
}
|
|
15034
15102
|
}
|
|
15035
15103
|
}
|
|
15036
|
-
|
|
15104
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15105
|
+
const deduped = [];
|
|
15106
|
+
for (const f of flows) {
|
|
15107
|
+
const key = `${f.source.line}|${f.sink.line}|${f.sink.type}|${f.path.map((p) => p.variable).join(">")}`;
|
|
15108
|
+
if (seen.has(key)) continue;
|
|
15109
|
+
seen.add(key);
|
|
15110
|
+
deduped.push(f);
|
|
15111
|
+
}
|
|
15112
|
+
return { taintedVars, flows: deduped, reachableSinks };
|
|
15037
15113
|
}
|
|
15038
15114
|
function findInitialTaint(sources, callsByLine, defsByLine) {
|
|
15039
15115
|
const tainted = [];
|
|
@@ -10000,6 +10000,10 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
|
10000
10000
|
} else if (child.type === "for_statement") {
|
|
10001
10001
|
const rangeClause = findChildByTypeGo(child, "range_clause");
|
|
10002
10002
|
if (rangeClause) {
|
|
10003
|
+
const right = rangeClause.childForFieldName("right");
|
|
10004
|
+
if (right) {
|
|
10005
|
+
extractGoUses(right, uses, scopeStack);
|
|
10006
|
+
}
|
|
10003
10007
|
const left = rangeClause.childForFieldName("left");
|
|
10004
10008
|
if (left) {
|
|
10005
10009
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
@@ -10007,6 +10011,7 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
|
10007
10011
|
}
|
|
10008
10012
|
} else if (child.type === "call_expression") {
|
|
10009
10013
|
extractGoUses(child, uses, scopeStack);
|
|
10014
|
+
recordGoOpaqueCodecDestDef(child, defs, scopeStack);
|
|
10010
10015
|
} else if (child.type === "return_statement") {
|
|
10011
10016
|
for (let i2 = 0; i2 < child.childCount; i2++) {
|
|
10012
10017
|
const expr = child.child(i2);
|
|
@@ -10099,6 +10104,63 @@ function findChildByTypeGo(node, type) {
|
|
|
10099
10104
|
}
|
|
10100
10105
|
return null;
|
|
10101
10106
|
}
|
|
10107
|
+
var GO_OPAQUE_CODEC_METHODS = /* @__PURE__ */ new Set([
|
|
10108
|
+
"Unmarshal",
|
|
10109
|
+
// json/xml/yaml/toml/gob
|
|
10110
|
+
"Decode",
|
|
10111
|
+
// json.NewDecoder(r).Decode(&dest), gob.Decoder.Decode
|
|
10112
|
+
"NewDecoder",
|
|
10113
|
+
// wrapper; handled via chained Decode above
|
|
10114
|
+
"UnmarshalYAML",
|
|
10115
|
+
"UnmarshalJSON",
|
|
10116
|
+
"UnmarshalText",
|
|
10117
|
+
"UnmarshalBinary"
|
|
10118
|
+
]);
|
|
10119
|
+
function recordGoOpaqueCodecDestDef(call, defs, scopeStack) {
|
|
10120
|
+
const fn = call.childForFieldName("function");
|
|
10121
|
+
if (!fn || fn.type !== "selector_expression") return;
|
|
10122
|
+
const fieldNode = fn.childForFieldName("field");
|
|
10123
|
+
if (!fieldNode) return;
|
|
10124
|
+
const method = getNodeText(fieldNode);
|
|
10125
|
+
if (!GO_OPAQUE_CODEC_METHODS.has(method)) return;
|
|
10126
|
+
const argsNode = call.childForFieldName("arguments");
|
|
10127
|
+
if (!argsNode) return;
|
|
10128
|
+
const args2 = [];
|
|
10129
|
+
for (let i2 = 0; i2 < argsNode.childCount; i2++) {
|
|
10130
|
+
const c = argsNode.child(i2);
|
|
10131
|
+
if (!c) continue;
|
|
10132
|
+
if (c.type === "," || c.type === "(" || c.type === ")") continue;
|
|
10133
|
+
args2.push(c);
|
|
10134
|
+
}
|
|
10135
|
+
if (args2.length === 0) return;
|
|
10136
|
+
const destArg = args2.length >= 2 ? args2[1] : args2[0];
|
|
10137
|
+
const destName = extractGoAddressableVarName(destArg);
|
|
10138
|
+
if (!destName || destName === "_") return;
|
|
10139
|
+
const line = call.startPosition.row + 1;
|
|
10140
|
+
const def = {
|
|
10141
|
+
id: defs.length + 1,
|
|
10142
|
+
variable: destName,
|
|
10143
|
+
kind: "local",
|
|
10144
|
+
line
|
|
10145
|
+
};
|
|
10146
|
+
defs.push(def);
|
|
10147
|
+
currentScope(scopeStack).set(destName, def.id);
|
|
10148
|
+
}
|
|
10149
|
+
function extractGoAddressableVarName(node) {
|
|
10150
|
+
if (node.type === "unary_expression") {
|
|
10151
|
+
const operand = node.childForFieldName("operand") ?? node.child(1) ?? null;
|
|
10152
|
+
if (operand) return extractGoAddressableVarName(operand);
|
|
10153
|
+
return null;
|
|
10154
|
+
}
|
|
10155
|
+
if (node.type === "identifier") {
|
|
10156
|
+
return getNodeText(node);
|
|
10157
|
+
}
|
|
10158
|
+
if (node.type === "selector_expression") {
|
|
10159
|
+
const field = node.childForFieldName("field");
|
|
10160
|
+
if (field) return getNodeText(field);
|
|
10161
|
+
}
|
|
10162
|
+
return null;
|
|
10163
|
+
}
|
|
10102
10164
|
|
|
10103
10165
|
// src/analysis/config-loader.ts
|
|
10104
10166
|
function parseConfig(content) {
|
|
@@ -14921,15 +14983,21 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
14921
14983
|
const callsAtSink = callsByLine.get(sink.line) ?? [];
|
|
14922
14984
|
for (const call of callsAtSink) {
|
|
14923
14985
|
for (const arg of call.arguments) {
|
|
14924
|
-
if (
|
|
14925
|
-
if (sink.argPositions
|
|
14926
|
-
|
|
14927
|
-
continue;
|
|
14928
|
-
}
|
|
14986
|
+
if (sink.argPositions && sink.argPositions.length > 0) {
|
|
14987
|
+
if (!sink.argPositions.includes(arg.position)) {
|
|
14988
|
+
continue;
|
|
14929
14989
|
}
|
|
14930
|
-
|
|
14931
|
-
|
|
14990
|
+
}
|
|
14991
|
+
const candidateUses = arg.variable ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
|
|
14992
|
+
{
|
|
14993
|
+
for (const use of candidateUses) {
|
|
14994
|
+
if (use.def_id !== null) {
|
|
14932
14995
|
if (allTaintedDefIds.has(use.def_id)) {
|
|
14996
|
+
if (!arg.variable) {
|
|
14997
|
+
if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
|
|
14998
|
+
const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
|
|
14999
|
+
if (!re.test(arg.expression)) continue;
|
|
15000
|
+
}
|
|
14933
15001
|
const taintInfo = taintByDefId.get(use.def_id);
|
|
14934
15002
|
if (taintInfo) {
|
|
14935
15003
|
const isSanitized = checkSanitized(
|
|
@@ -14967,7 +15035,15 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
14967
15035
|
}
|
|
14968
15036
|
}
|
|
14969
15037
|
}
|
|
14970
|
-
|
|
15038
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15039
|
+
const deduped = [];
|
|
15040
|
+
for (const f of flows) {
|
|
15041
|
+
const key = `${f.source.line}|${f.sink.line}|${f.sink.type}|${f.path.map((p) => p.variable).join(">")}`;
|
|
15042
|
+
if (seen.has(key)) continue;
|
|
15043
|
+
seen.add(key);
|
|
15044
|
+
deduped.push(f);
|
|
15045
|
+
}
|
|
15046
|
+
return { taintedVars, flows: deduped, reachableSinks };
|
|
14971
15047
|
}
|
|
14972
15048
|
function findInitialTaint(sources, callsByLine, defsByLine) {
|
|
14973
15049
|
const tainted = [];
|
|
@@ -1385,6 +1385,14 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
|
1385
1385
|
// range clause: for k, v := range expr
|
|
1386
1386
|
const rangeClause = findChildByTypeGo(child, 'range_clause');
|
|
1387
1387
|
if (rangeClause) {
|
|
1388
|
+
const right = rangeClause.childForFieldName('right');
|
|
1389
|
+
// Record uses of the range source on the same line as the loop-var
|
|
1390
|
+
// defs, so `computeChains` links the source def → loop-var def, which
|
|
1391
|
+
// is what carries taint into the loop body. Emit before defs so RHS
|
|
1392
|
+
// uses see the outer binding, not the new loop var. (Issue #243.)
|
|
1393
|
+
if (right) {
|
|
1394
|
+
extractGoUses(right, uses, scopeStack);
|
|
1395
|
+
}
|
|
1388
1396
|
const left = rangeClause.childForFieldName('left');
|
|
1389
1397
|
if (left) {
|
|
1390
1398
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
@@ -1394,6 +1402,14 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
|
1394
1402
|
else if (child.type === 'call_expression') {
|
|
1395
1403
|
// Extract uses from call arguments
|
|
1396
1404
|
extractGoUses(child, uses, scopeStack);
|
|
1405
|
+
// Opaque-codec destination re-def (cognium-dev #243).
|
|
1406
|
+
//
|
|
1407
|
+
// Calls like `json.Unmarshal(bytes, &dest)`, `xml.Unmarshal(...)`,
|
|
1408
|
+
// `gob.NewDecoder(r).Decode(&dest)`, `yaml.Unmarshal(...)` populate
|
|
1409
|
+
// `dest` via reflection. Model that as a re-definition of `dest` on
|
|
1410
|
+
// this line so `computeChains` links the source-arg use to a fresh
|
|
1411
|
+
// `dest` def and taint propagates through the codec.
|
|
1412
|
+
recordGoOpaqueCodecDestDef(child, defs, scopeStack);
|
|
1397
1413
|
}
|
|
1398
1414
|
else if (child.type === 'return_statement') {
|
|
1399
1415
|
// Extract uses from return expressions
|
|
@@ -1514,4 +1530,87 @@ function findChildByTypeGo(node, type) {
|
|
|
1514
1530
|
}
|
|
1515
1531
|
return null;
|
|
1516
1532
|
}
|
|
1533
|
+
/**
|
|
1534
|
+
* cognium-dev #243 — opaque codec destination re-defs for Go.
|
|
1535
|
+
*
|
|
1536
|
+
* Package/method pairs whose second (or only) arg is a destination that the
|
|
1537
|
+
* call populates via reflection. Modelling them as re-defs of the dest lets
|
|
1538
|
+
* `computeChains` link source-arg → dest and taint propagates through the
|
|
1539
|
+
* codec.
|
|
1540
|
+
*/
|
|
1541
|
+
const GO_OPAQUE_CODEC_METHODS = new Set([
|
|
1542
|
+
'Unmarshal', // json/xml/yaml/toml/gob
|
|
1543
|
+
'Decode', // json.NewDecoder(r).Decode(&dest), gob.Decoder.Decode
|
|
1544
|
+
'NewDecoder', // wrapper; handled via chained Decode above
|
|
1545
|
+
'UnmarshalYAML',
|
|
1546
|
+
'UnmarshalJSON',
|
|
1547
|
+
'UnmarshalText',
|
|
1548
|
+
'UnmarshalBinary',
|
|
1549
|
+
]);
|
|
1550
|
+
function recordGoOpaqueCodecDestDef(call, defs, scopeStack) {
|
|
1551
|
+
// Only handle direct selector calls: `pkg.Method(...)` or `receiver.Decode(...)`.
|
|
1552
|
+
const fn = call.childForFieldName('function');
|
|
1553
|
+
if (!fn || fn.type !== 'selector_expression')
|
|
1554
|
+
return;
|
|
1555
|
+
const fieldNode = fn.childForFieldName('field');
|
|
1556
|
+
if (!fieldNode)
|
|
1557
|
+
return;
|
|
1558
|
+
const method = getNodeText(fieldNode);
|
|
1559
|
+
if (!GO_OPAQUE_CODEC_METHODS.has(method))
|
|
1560
|
+
return;
|
|
1561
|
+
const argsNode = call.childForFieldName('arguments');
|
|
1562
|
+
if (!argsNode)
|
|
1563
|
+
return;
|
|
1564
|
+
// Positional args (skip commas / parens).
|
|
1565
|
+
const args = [];
|
|
1566
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
1567
|
+
const c = argsNode.child(i);
|
|
1568
|
+
if (!c)
|
|
1569
|
+
continue;
|
|
1570
|
+
if (c.type === ',' || c.type === '(' || c.type === ')')
|
|
1571
|
+
continue;
|
|
1572
|
+
args.push(c);
|
|
1573
|
+
}
|
|
1574
|
+
if (args.length === 0)
|
|
1575
|
+
return;
|
|
1576
|
+
// Destination convention:
|
|
1577
|
+
// - Unmarshal(bytes, &dest) → args[1]
|
|
1578
|
+
// - Decode(&dest) → args[0]
|
|
1579
|
+
const destArg = args.length >= 2 ? args[1] : args[0];
|
|
1580
|
+
const destName = extractGoAddressableVarName(destArg);
|
|
1581
|
+
if (!destName || destName === '_')
|
|
1582
|
+
return;
|
|
1583
|
+
const line = call.startPosition.row + 1;
|
|
1584
|
+
const def = {
|
|
1585
|
+
id: defs.length + 1,
|
|
1586
|
+
variable: destName,
|
|
1587
|
+
kind: 'local',
|
|
1588
|
+
line,
|
|
1589
|
+
};
|
|
1590
|
+
defs.push(def);
|
|
1591
|
+
currentScope(scopeStack).set(destName, def.id);
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Return the addressed variable name for an opaque-codec destination arg.
|
|
1595
|
+
* Handles `&x`, bare `x`, and `&pkg.Y`-style receivers (returns `Y` — the
|
|
1596
|
+
* codec still populates that concrete addressable location).
|
|
1597
|
+
*/
|
|
1598
|
+
function extractGoAddressableVarName(node) {
|
|
1599
|
+
if (node.type === 'unary_expression') {
|
|
1600
|
+
// &x — operand carries the identifier
|
|
1601
|
+
const operand = node.childForFieldName('operand') ?? node.child(1) ?? null;
|
|
1602
|
+
if (operand)
|
|
1603
|
+
return extractGoAddressableVarName(operand);
|
|
1604
|
+
return null;
|
|
1605
|
+
}
|
|
1606
|
+
if (node.type === 'identifier') {
|
|
1607
|
+
return getNodeText(node);
|
|
1608
|
+
}
|
|
1609
|
+
if (node.type === 'selector_expression') {
|
|
1610
|
+
const field = node.childForFieldName('field');
|
|
1611
|
+
if (field)
|
|
1612
|
+
return getNodeText(field);
|
|
1613
|
+
}
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1517
1616
|
//# sourceMappingURL=dfg.js.map
|