circle-ir 3.192.0 → 3.194.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/prompt-injection-safety-gate-pass.d.ts +75 -0
- package/dist/analysis/passes/prompt-injection-safety-gate-pass.d.ts.map +1 -0
- package/dist/analysis/passes/prompt-injection-safety-gate-pass.js +353 -0
- package/dist/analysis/passes/prompt-injection-safety-gate-pass.js.map +1 -0
- package/dist/analysis/passes/speculative-prompt-param-source-pass.d.ts +67 -0
- package/dist/analysis/passes/speculative-prompt-param-source-pass.d.ts.map +1 -0
- package/dist/analysis/passes/speculative-prompt-param-source-pass.js +193 -0
- package/dist/analysis/passes/speculative-prompt-param-source-pass.js.map +1 -0
- package/dist/analyzer.d.ts +19 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +30 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +374 -4
- package/dist/core/circle-ir-core.cjs +31 -2
- package/dist/core/circle-ir-core.js +31 -2
- package/dist/core/extractors/dfg.js +46 -2
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/resolution/cross-file.d.ts +64 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +184 -10
- package/dist/resolution/cross-file.js.map +1 -1
- package/package.json +1 -1
|
@@ -9935,6 +9935,7 @@ var GO_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
9935
9935
|
function buildGoDFG(tree) {
|
|
9936
9936
|
const defs = [];
|
|
9937
9937
|
const uses = [];
|
|
9938
|
+
const extraChains = [];
|
|
9938
9939
|
let defIdCounter = 1;
|
|
9939
9940
|
let useIdCounter = 1;
|
|
9940
9941
|
const scopeStack = [/* @__PURE__ */ new Map()];
|
|
@@ -9956,7 +9957,7 @@ function buildGoDFG(tree) {
|
|
|
9956
9957
|
}
|
|
9957
9958
|
const body2 = func2.childForFieldName("body");
|
|
9958
9959
|
if (body2) {
|
|
9959
|
-
processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
|
|
9960
|
+
processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
|
|
9960
9961
|
defIdCounter = defs.length + 1;
|
|
9961
9962
|
useIdCounter = uses.length + 1;
|
|
9962
9963
|
}
|
|
@@ -9971,6 +9972,15 @@ function buildGoDFG(tree) {
|
|
|
9971
9972
|
}
|
|
9972
9973
|
}
|
|
9973
9974
|
const chains = computeChains(defs, uses);
|
|
9975
|
+
const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
|
|
9976
|
+
for (const ch of extraChains) {
|
|
9977
|
+
const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
|
|
9978
|
+
if (!seen.has(key)) {
|
|
9979
|
+
seen.add(key);
|
|
9980
|
+
chains.push(ch);
|
|
9981
|
+
}
|
|
9982
|
+
}
|
|
9983
|
+
chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
|
|
9974
9984
|
return { defs, uses, chains };
|
|
9975
9985
|
}
|
|
9976
9986
|
function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
@@ -9995,28 +10005,47 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
|
9995
10005
|
}
|
|
9996
10006
|
}
|
|
9997
10007
|
}
|
|
9998
|
-
function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
10008
|
+
function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
|
|
10009
|
+
const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
|
|
10010
|
+
if (right.endPosition.row <= right.startPosition.row) return;
|
|
10011
|
+
for (let ui = useStart; ui < useEnd; ui++) {
|
|
10012
|
+
const u = uses[ui];
|
|
10013
|
+
if (!u || u.def_id === null) continue;
|
|
10014
|
+
for (let di = defStart; di < defEnd; di++) {
|
|
10015
|
+
const d = defs[di];
|
|
10016
|
+
if (d && d.kind === "local" && d.id !== u.def_id) {
|
|
10017
|
+
extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
|
|
10018
|
+
}
|
|
10019
|
+
}
|
|
10020
|
+
}
|
|
10021
|
+
};
|
|
9999
10022
|
walkTree(node, (child) => {
|
|
10000
10023
|
if (child.type === "short_var_declaration") {
|
|
10001
10024
|
const left = child.childForFieldName("left");
|
|
10002
10025
|
const right = child.childForFieldName("right");
|
|
10026
|
+
const usesBefore = uses.length;
|
|
10003
10027
|
if (right) {
|
|
10004
10028
|
extractGoUses(right, uses, scopeStack);
|
|
10005
10029
|
}
|
|
10030
|
+
const defsBefore = defs.length;
|
|
10006
10031
|
if (left) {
|
|
10007
10032
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
10008
10033
|
}
|
|
10034
|
+
if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
10009
10035
|
} else if (child.type === "var_declaration") {
|
|
10010
10036
|
processGoVarDecl(child, defs, scopeStack, counters);
|
|
10011
10037
|
} else if (child.type === "assignment_statement") {
|
|
10012
10038
|
const left = child.childForFieldName("left");
|
|
10013
10039
|
const right = child.childForFieldName("right");
|
|
10040
|
+
const usesBefore = uses.length;
|
|
10014
10041
|
if (right) {
|
|
10015
10042
|
extractGoUses(right, uses, scopeStack);
|
|
10016
10043
|
}
|
|
10044
|
+
const defsBefore = defs.length;
|
|
10017
10045
|
if (left) {
|
|
10018
10046
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
10019
10047
|
}
|
|
10048
|
+
if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
10020
10049
|
} else if (child.type === "for_statement") {
|
|
10021
10050
|
const rangeClause = findChildByTypeGo(child, "range_clause");
|
|
10022
10051
|
if (rangeClause) {
|
|
@@ -28591,12 +28620,12 @@ function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
|
|
|
28591
28620
|
function findJavaPathGetFileNameSanitizers(code) {
|
|
28592
28621
|
const sanitizers = [];
|
|
28593
28622
|
const lines = code.split("\n");
|
|
28594
|
-
const
|
|
28623
|
+
const assignmentRe2 = /^\s*(?:(?:final\s+)?[A-Za-z_][\w.<>?,\s\[\]]*?\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
28595
28624
|
const rhsHasGetFileNameRe = /\.\s*getFileName\s*\(\s*\)/;
|
|
28596
28625
|
const rhsHasPathsChainRe = /\b(?:Paths\s*\.\s*get|Path\s*\.\s*of)\s*\(/;
|
|
28597
28626
|
const candidates = [];
|
|
28598
28627
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
28599
|
-
const m =
|
|
28628
|
+
const m = assignmentRe2.exec(lines[i2]);
|
|
28600
28629
|
if (!m) continue;
|
|
28601
28630
|
const rhs = m[2];
|
|
28602
28631
|
if (!rhsHasGetFileNameRe.test(rhs)) continue;
|
|
@@ -33831,6 +33860,333 @@ var DeserializationSafetyGatePass = class {
|
|
|
33831
33860
|
}
|
|
33832
33861
|
};
|
|
33833
33862
|
|
|
33863
|
+
// src/analysis/passes/prompt-injection-safety-gate-pass.ts
|
|
33864
|
+
function isDelimiterLiteral(lit) {
|
|
33865
|
+
const s = lit.trim();
|
|
33866
|
+
if (s.length === 0) return true;
|
|
33867
|
+
return /^<\/?[\w.-]+\s*\/?>$/.test(s) || /^\[\/?[\w.-]+\]$/.test(s) || /^`{1,3}[\w-]*$/.test(s) || /^"""[\w-]*$/.test(s) || /^[#*=_~-]{2,}$/.test(s) || /^<{2,}[\w-]*$/.test(s) || /^[\w-]*>{2,}$/.test(s);
|
|
33868
|
+
}
|
|
33869
|
+
function scanValue(code, start2) {
|
|
33870
|
+
let depth = 0;
|
|
33871
|
+
let str = null;
|
|
33872
|
+
let out2 = "";
|
|
33873
|
+
for (let i2 = start2; i2 < code.length; i2++) {
|
|
33874
|
+
const c = code[i2];
|
|
33875
|
+
if (str) {
|
|
33876
|
+
out2 += c;
|
|
33877
|
+
if (c === str && code[i2 - 1] !== "\\") str = null;
|
|
33878
|
+
continue;
|
|
33879
|
+
}
|
|
33880
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
33881
|
+
str = c;
|
|
33882
|
+
out2 += c;
|
|
33883
|
+
continue;
|
|
33884
|
+
}
|
|
33885
|
+
if (c === "(" || c === "[" || c === "{") {
|
|
33886
|
+
depth++;
|
|
33887
|
+
out2 += c;
|
|
33888
|
+
continue;
|
|
33889
|
+
}
|
|
33890
|
+
if (c === ")" || c === "]" || c === "}") {
|
|
33891
|
+
if (depth === 0) break;
|
|
33892
|
+
depth--;
|
|
33893
|
+
out2 += c;
|
|
33894
|
+
continue;
|
|
33895
|
+
}
|
|
33896
|
+
if (c === "," && depth === 0) break;
|
|
33897
|
+
out2 += c;
|
|
33898
|
+
}
|
|
33899
|
+
return out2.trim();
|
|
33900
|
+
}
|
|
33901
|
+
function stringLiterals(expr) {
|
|
33902
|
+
return [...expr.matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? "");
|
|
33903
|
+
}
|
|
33904
|
+
function hasIdentifier(expr) {
|
|
33905
|
+
return /[A-Za-z_$][\w$]*/.test(expr.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, " "));
|
|
33906
|
+
}
|
|
33907
|
+
function isPureLiteral(valueExpr) {
|
|
33908
|
+
const stripped = valueExpr.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, " ");
|
|
33909
|
+
return !/[A-Za-z_$][\w$]*/.test(stripped);
|
|
33910
|
+
}
|
|
33911
|
+
function isInstructionConcat(valueExpr) {
|
|
33912
|
+
if (!valueExpr.includes("+")) return false;
|
|
33913
|
+
if (!hasIdentifier(valueExpr)) return false;
|
|
33914
|
+
const lits = stringLiterals(valueExpr);
|
|
33915
|
+
if (lits.length === 0) return false;
|
|
33916
|
+
return !lits.every(isDelimiterLiteral);
|
|
33917
|
+
}
|
|
33918
|
+
function isMixedTemplate(v) {
|
|
33919
|
+
const fstr = v.match(/\bf(["'])((?:\\.|(?!\1).)*)\1/);
|
|
33920
|
+
if (fstr) {
|
|
33921
|
+
const body2 = fstr[2];
|
|
33922
|
+
if (/\{[^}]+\}/.test(body2) && /\w/.test(body2.replace(/\{[^}]*\}/g, ""))) return true;
|
|
33923
|
+
}
|
|
33924
|
+
const tmpl = v.match(/`([^`]*)`/);
|
|
33925
|
+
if (tmpl) {
|
|
33926
|
+
const body2 = tmpl[1];
|
|
33927
|
+
if (/\$\{[^}]+\}/.test(body2) && /\w/.test(body2.replace(/\$\{[^}]*\}/g, ""))) return true;
|
|
33928
|
+
}
|
|
33929
|
+
return false;
|
|
33930
|
+
}
|
|
33931
|
+
function assignmentRe(varName, flags2 = "") {
|
|
33932
|
+
return new RegExp(`\\b${varName}\\s*(?::=|(?<![=<>!])=(?!=))\\s*`, flags2);
|
|
33933
|
+
}
|
|
33934
|
+
function resolveAssignmentRHS(varName, codeLines) {
|
|
33935
|
+
const re = assignmentRe(varName);
|
|
33936
|
+
for (const line of codeLines) {
|
|
33937
|
+
const m = line.match(re);
|
|
33938
|
+
if (m && m.index !== void 0) {
|
|
33939
|
+
return scanValue(line, m.index + m[0].length);
|
|
33940
|
+
}
|
|
33941
|
+
}
|
|
33942
|
+
return void 0;
|
|
33943
|
+
}
|
|
33944
|
+
function classifyExpr(v) {
|
|
33945
|
+
if (isMixedTemplate(v)) return "unsafe";
|
|
33946
|
+
if (v.includes("+")) {
|
|
33947
|
+
if (isInstructionConcat(v)) return "unsafe";
|
|
33948
|
+
const lits = stringLiterals(v);
|
|
33949
|
+
if (lits.length > 0 && lits.every(isDelimiterLiteral) && hasIdentifier(v)) return "safe";
|
|
33950
|
+
return "unknown";
|
|
33951
|
+
}
|
|
33952
|
+
return "unknown";
|
|
33953
|
+
}
|
|
33954
|
+
function classifyContentPlacement(val, codeLines) {
|
|
33955
|
+
const v = val.trim();
|
|
33956
|
+
if (/^[A-Za-z_$][\w$]*$/.test(v)) {
|
|
33957
|
+
const rhs = resolveAssignmentRHS(v, codeLines);
|
|
33958
|
+
if (rhs === void 0) return "unknown";
|
|
33959
|
+
return classifyExpr(rhs);
|
|
33960
|
+
}
|
|
33961
|
+
return classifyExpr(v);
|
|
33962
|
+
}
|
|
33963
|
+
function classifyPromptCall(callCode, codeLines) {
|
|
33964
|
+
const contentRe = /["']?[Cc]ontent["']?\s*[:=]\s*/g;
|
|
33965
|
+
let m;
|
|
33966
|
+
let sawDynamic = false;
|
|
33967
|
+
let allSafe = true;
|
|
33968
|
+
while ((m = contentRe.exec(callCode)) !== null) {
|
|
33969
|
+
const val = scanValue(callCode, m.index + m[0].length);
|
|
33970
|
+
if (val === "" || isPureLiteral(val)) continue;
|
|
33971
|
+
sawDynamic = true;
|
|
33972
|
+
const ctx = callCode.slice(Math.max(0, m.index - 80), m.index);
|
|
33973
|
+
const roleM = ctx.match(/["']?[Rr]ole["']?\s*[:=]\s*["'](\w+)["'][^"']*$/);
|
|
33974
|
+
const role = roleM ? roleM[1].toLowerCase() : "user";
|
|
33975
|
+
if (role === "system" || role === "assistant") return "unsafe";
|
|
33976
|
+
const placement = classifyContentPlacement(val, codeLines);
|
|
33977
|
+
if (placement === "unsafe") return "unsafe";
|
|
33978
|
+
if (placement !== "safe") allSafe = false;
|
|
33979
|
+
}
|
|
33980
|
+
if (!sawDynamic) return "unknown";
|
|
33981
|
+
return allSafe ? "safe" : "unknown";
|
|
33982
|
+
}
|
|
33983
|
+
function scanRhsAcrossLines(text, start2) {
|
|
33984
|
+
let depth = 0;
|
|
33985
|
+
let str = null;
|
|
33986
|
+
let out2 = "";
|
|
33987
|
+
for (let i2 = start2; i2 < text.length; i2++) {
|
|
33988
|
+
const c = text[i2];
|
|
33989
|
+
if (str) {
|
|
33990
|
+
out2 += c;
|
|
33991
|
+
if (c === str && text[i2 - 1] !== "\\") str = null;
|
|
33992
|
+
continue;
|
|
33993
|
+
}
|
|
33994
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
33995
|
+
str = c;
|
|
33996
|
+
out2 += c;
|
|
33997
|
+
continue;
|
|
33998
|
+
}
|
|
33999
|
+
if (c === "(" || c === "[" || c === "{") {
|
|
34000
|
+
depth++;
|
|
34001
|
+
out2 += c;
|
|
34002
|
+
continue;
|
|
34003
|
+
}
|
|
34004
|
+
if (c === ")" || c === "]" || c === "}") {
|
|
34005
|
+
if (depth === 0) break;
|
|
34006
|
+
depth--;
|
|
34007
|
+
out2 += c;
|
|
34008
|
+
continue;
|
|
34009
|
+
}
|
|
34010
|
+
if (c === "\n" && depth === 0) break;
|
|
34011
|
+
out2 += c;
|
|
34012
|
+
}
|
|
34013
|
+
return out2.trim();
|
|
34014
|
+
}
|
|
34015
|
+
function resolveBuilderRhs(varName, joined) {
|
|
34016
|
+
const re = assignmentRe(varName, "g");
|
|
34017
|
+
const m = re.exec(joined);
|
|
34018
|
+
if (m) return scanRhsAcrossLines(joined, m.index + m[0].length);
|
|
34019
|
+
return void 0;
|
|
34020
|
+
}
|
|
34021
|
+
function collectBuilderRegion(callCode, joined) {
|
|
34022
|
+
const parts2 = [];
|
|
34023
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34024
|
+
const stack = [...callCode.matchAll(/[A-Za-z_$][\w$]*/g)].map((x) => x[0]);
|
|
34025
|
+
let budget = 40;
|
|
34026
|
+
while (stack.length > 0 && budget-- > 0) {
|
|
34027
|
+
const v = stack.pop();
|
|
34028
|
+
if (seen.has(v)) continue;
|
|
34029
|
+
seen.add(v);
|
|
34030
|
+
const rhs = resolveBuilderRhs(v, joined);
|
|
34031
|
+
if (rhs) {
|
|
34032
|
+
parts2.push(rhs);
|
|
34033
|
+
for (const mm of rhs.matchAll(/[A-Za-z_$][\w$]*/g)) stack.push(mm[0]);
|
|
34034
|
+
}
|
|
34035
|
+
}
|
|
34036
|
+
return parts2.join("\n");
|
|
34037
|
+
}
|
|
34038
|
+
function classifyPromptSink(callCode, codeLines) {
|
|
34039
|
+
const direct = classifyPromptCall(callCode, codeLines);
|
|
34040
|
+
if (direct !== "unknown") return direct;
|
|
34041
|
+
const region = collectBuilderRegion(callCode, codeLines.join("\n"));
|
|
34042
|
+
if (!region) return "unknown";
|
|
34043
|
+
return classifyPromptCall(region, codeLines);
|
|
34044
|
+
}
|
|
34045
|
+
var PromptInjectionSafetyGatePass = class {
|
|
34046
|
+
name = "prompt-injection-safety-gate";
|
|
34047
|
+
category = "security";
|
|
34048
|
+
run(ctx) {
|
|
34049
|
+
const { graph, code } = ctx;
|
|
34050
|
+
const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
|
|
34051
|
+
if (sinks.length === 0) return { droppedSafe: 0 };
|
|
34052
|
+
const codeLines = code.split("\n");
|
|
34053
|
+
let droppedSafe = 0;
|
|
34054
|
+
const kept = sinks.filter((sink) => {
|
|
34055
|
+
if (sink.type !== "prompt_injection") return true;
|
|
34056
|
+
const callCode = sink.code ?? codeLines[sink.line - 1] ?? "";
|
|
34057
|
+
if (!callCode) return true;
|
|
34058
|
+
if (classifyPromptSink(callCode, codeLines) === "safe") {
|
|
34059
|
+
droppedSafe++;
|
|
34060
|
+
return false;
|
|
34061
|
+
}
|
|
34062
|
+
return true;
|
|
34063
|
+
});
|
|
34064
|
+
if (droppedSafe > 0) {
|
|
34065
|
+
sinks.length = 0;
|
|
34066
|
+
sinks.push(...kept);
|
|
34067
|
+
}
|
|
34068
|
+
return { droppedSafe };
|
|
34069
|
+
}
|
|
34070
|
+
};
|
|
34071
|
+
|
|
34072
|
+
// src/analysis/passes/speculative-prompt-param-source-pass.ts
|
|
34073
|
+
function returnOrAssignRhs(text) {
|
|
34074
|
+
const t = text.trim();
|
|
34075
|
+
const ret = t.match(/^return\s+(.+?);?$/);
|
|
34076
|
+
if (ret) return ret[1];
|
|
34077
|
+
const asg = t.match(/^[A-Za-z_$][\w$.]*\s*(?::=|(?<![=<>!])=(?!=))\s*(.+?);?$/);
|
|
34078
|
+
if (asg) return asg[1];
|
|
34079
|
+
return void 0;
|
|
34080
|
+
}
|
|
34081
|
+
function looksLikePromptText(lit) {
|
|
34082
|
+
const s = lit.trim();
|
|
34083
|
+
if (/\b(you are|you're|assistant|system prompt|follow (the )?(policy|instructions|rules)|act as|your task|instructions?\s*:|ignore (all )?previous|do not reveal|respond (to|with|as)|answer the (question|user)|you (must|should|will)|helpful (ai|assistant))\b/i.test(s)) {
|
|
34084
|
+
return true;
|
|
34085
|
+
}
|
|
34086
|
+
const words = s.split(/\s+/).filter(Boolean);
|
|
34087
|
+
return words.length >= 4 && s.length >= 20;
|
|
34088
|
+
}
|
|
34089
|
+
function literalsOf(expr) {
|
|
34090
|
+
return [...expr.matchAll(/"([^"]*)"|'([^']*)'|`([^`]*)`/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
|
|
34091
|
+
}
|
|
34092
|
+
function detectPromptConstructionFlows(codeLines, types) {
|
|
34093
|
+
const flows = [];
|
|
34094
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34095
|
+
for (const type of types) {
|
|
34096
|
+
for (const method of type.methods) {
|
|
34097
|
+
const params = (method.parameters ?? []).map((p) => ({ name: p.name, line: p.line ?? method.start_line })).filter((p) => p.name && p.name !== "_");
|
|
34098
|
+
if (params.length === 0) continue;
|
|
34099
|
+
for (let line = method.start_line; line <= method.end_line; line++) {
|
|
34100
|
+
const text = codeLines[line - 1] ?? "";
|
|
34101
|
+
const rhs = returnOrAssignRhs(text);
|
|
34102
|
+
if (!rhs || !isInstructionConcat(rhs)) continue;
|
|
34103
|
+
if (!literalsOf(rhs).some(looksLikePromptText)) continue;
|
|
34104
|
+
const param = params.find((p) => new RegExp(`\\b${p.name}\\b`).test(rhs));
|
|
34105
|
+
if (!param) continue;
|
|
34106
|
+
const key = `${param.line}|${line}`;
|
|
34107
|
+
if (seen.has(key)) continue;
|
|
34108
|
+
seen.add(key);
|
|
34109
|
+
flows.push({
|
|
34110
|
+
source_line: param.line,
|
|
34111
|
+
sink_line: line,
|
|
34112
|
+
source_type: "http_body",
|
|
34113
|
+
sink_type: "prompt_injection",
|
|
34114
|
+
path: [
|
|
34115
|
+
{ variable: param.name, line: param.line, type: "source" },
|
|
34116
|
+
{ variable: param.name, line, type: "sink" }
|
|
34117
|
+
],
|
|
34118
|
+
confidence: 1,
|
|
34119
|
+
sanitized: false
|
|
34120
|
+
});
|
|
34121
|
+
}
|
|
34122
|
+
}
|
|
34123
|
+
}
|
|
34124
|
+
return flows;
|
|
34125
|
+
}
|
|
34126
|
+
function looksLikeTextParam(type) {
|
|
34127
|
+
if (!type) return true;
|
|
34128
|
+
const t = type.trim();
|
|
34129
|
+
if (t.startsWith("*") || t.startsWith("&")) return false;
|
|
34130
|
+
if (/\b(Client|Context|Writer|Reader|Conn|DB|Logger|Handler|Server|Request|Response|Pool|Session|Engine|Service|Repository|Config)\b/.test(t)) {
|
|
34131
|
+
return false;
|
|
34132
|
+
}
|
|
34133
|
+
return /(?:^|\b)(string|str|String|text|any|object|interface\{\}|\[\]byte|\[\]string|List\[str\]|Optional\[str\])(?:\b|$)/i.test(t) || // TS unions / plain identifiers without a handle keyword default to text.
|
|
34134
|
+
/^[A-Za-z_$][\w$<>[\], |]*$/.test(t);
|
|
34135
|
+
}
|
|
34136
|
+
var SpeculativePromptParamSourcePass = class {
|
|
34137
|
+
constructor(enabled) {
|
|
34138
|
+
this.enabled = enabled;
|
|
34139
|
+
}
|
|
34140
|
+
enabled;
|
|
34141
|
+
name = "speculative-prompt-param-source";
|
|
34142
|
+
category = "security";
|
|
34143
|
+
run(ctx) {
|
|
34144
|
+
if (!this.enabled) return { added: 0 };
|
|
34145
|
+
if (!ctx.hasResult("sink-filter")) return { added: 0 };
|
|
34146
|
+
const sinkFilter = ctx.getResult("sink-filter");
|
|
34147
|
+
const { types } = ctx.graph.ir;
|
|
34148
|
+
const promptSinks = sinkFilter.sinks.filter(
|
|
34149
|
+
(s) => s.type === "prompt_injection"
|
|
34150
|
+
);
|
|
34151
|
+
if (promptSinks.length === 0) return { added: 0 };
|
|
34152
|
+
const existing = new Set(
|
|
34153
|
+
sinkFilter.sources.filter((s) => typeof s.variable === "string").map((s) => `${s.variable}:${s.line}`)
|
|
34154
|
+
);
|
|
34155
|
+
const added = [];
|
|
34156
|
+
for (const type of types) {
|
|
34157
|
+
for (const method of type.methods) {
|
|
34158
|
+
const hasPromptSink = promptSinks.some(
|
|
34159
|
+
(s) => s.line >= method.start_line && s.line <= method.end_line
|
|
34160
|
+
);
|
|
34161
|
+
if (!hasPromptSink) continue;
|
|
34162
|
+
for (const param of method.parameters) {
|
|
34163
|
+
if (!param.name || param.name === "_") continue;
|
|
34164
|
+
if (!looksLikeTextParam(param.type)) continue;
|
|
34165
|
+
const line = param.line ?? method.start_line;
|
|
34166
|
+
const key = `${param.name}:${line}`;
|
|
34167
|
+
if (existing.has(key)) continue;
|
|
34168
|
+
existing.add(key);
|
|
34169
|
+
added.push({
|
|
34170
|
+
// `http_body` reaches `prompt_injection` via the flow
|
|
34171
|
+
// generators; a generic untrusted request-shaped type.
|
|
34172
|
+
type: "http_body",
|
|
34173
|
+
location: `speculative untrusted parameter '${param.name}' in ${method.name}`,
|
|
34174
|
+
severity: "high",
|
|
34175
|
+
line,
|
|
34176
|
+
confidence: 1,
|
|
34177
|
+
variable: param.name,
|
|
34178
|
+
in_method: method.name
|
|
34179
|
+
});
|
|
34180
|
+
}
|
|
34181
|
+
}
|
|
34182
|
+
}
|
|
34183
|
+
if (added.length > 0) {
|
|
34184
|
+
sinkFilter.sources.push(...added);
|
|
34185
|
+
}
|
|
34186
|
+
return { added: added.length };
|
|
34187
|
+
}
|
|
34188
|
+
};
|
|
34189
|
+
|
|
33834
34190
|
// src/analysis/passes/cli-main-reflection-suppress-pass.ts
|
|
33835
34191
|
var REFLECTION_SINK_METHODS = /* @__PURE__ */ new Set([
|
|
33836
34192
|
"forName",
|
|
@@ -44077,6 +44433,9 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
44077
44433
|
if (!disabledPasses.has("sink-semantics")) pipeline.add(new SinkSemanticsPass());
|
|
44078
44434
|
if (!disabledPasses.has("deserialization-safety-gate"))
|
|
44079
44435
|
pipeline.add(new DeserializationSafetyGatePass(options.dependencyContext));
|
|
44436
|
+
if (!disabledPasses.has("prompt-injection-safety-gate"))
|
|
44437
|
+
pipeline.add(new PromptInjectionSafetyGatePass());
|
|
44438
|
+
pipeline.add(new SpeculativePromptParamSourcePass(options.speculativeParamSources === true));
|
|
44080
44439
|
if (!disabledPasses.has("cli-main-reflection-suppress"))
|
|
44081
44440
|
pipeline.add(new CliMainReflectionSuppressPass());
|
|
44082
44441
|
if (!disabledPasses.has("library-profile-sink-gate"))
|
|
@@ -44156,6 +44515,17 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
44156
44515
|
flows: interProc.additionalFlows,
|
|
44157
44516
|
interprocedural: interProc.interprocedural
|
|
44158
44517
|
};
|
|
44518
|
+
if (options.speculativeParamSources === true) {
|
|
44519
|
+
const existingFlowKeys = new Set(
|
|
44520
|
+
(taint.flows ?? []).map((f) => `${f.source_line}|${f.sink_line}|${f.sink_type}`)
|
|
44521
|
+
);
|
|
44522
|
+
const constructionFlows = detectPromptConstructionFlows(code.split("\n"), types).filter(
|
|
44523
|
+
(f) => !existingFlowKeys.has(`${f.source_line}|${f.sink_line}|${f.sink_type}`)
|
|
44524
|
+
);
|
|
44525
|
+
if (constructionFlows.length > 0) {
|
|
44526
|
+
taint.flows = [...taint.flows ?? [], ...constructionFlows];
|
|
44527
|
+
}
|
|
44528
|
+
}
|
|
44159
44529
|
if (taint.flows && taint.flows.length > 0 && taint.sinks.length > 0) {
|
|
44160
44530
|
const sinkTagsByKey = /* @__PURE__ */ new Map();
|
|
44161
44531
|
for (const s of taint.sinks) {
|
|
@@ -9981,6 +9981,7 @@ var GO_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
9981
9981
|
function buildGoDFG(tree) {
|
|
9982
9982
|
const defs = [];
|
|
9983
9983
|
const uses = [];
|
|
9984
|
+
const extraChains = [];
|
|
9984
9985
|
let defIdCounter = 1;
|
|
9985
9986
|
let useIdCounter = 1;
|
|
9986
9987
|
const scopeStack = [/* @__PURE__ */ new Map()];
|
|
@@ -10002,7 +10003,7 @@ function buildGoDFG(tree) {
|
|
|
10002
10003
|
}
|
|
10003
10004
|
const body2 = func2.childForFieldName("body");
|
|
10004
10005
|
if (body2) {
|
|
10005
|
-
processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
|
|
10006
|
+
processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
|
|
10006
10007
|
defIdCounter = defs.length + 1;
|
|
10007
10008
|
useIdCounter = uses.length + 1;
|
|
10008
10009
|
}
|
|
@@ -10017,6 +10018,15 @@ function buildGoDFG(tree) {
|
|
|
10017
10018
|
}
|
|
10018
10019
|
}
|
|
10019
10020
|
const chains = computeChains(defs, uses);
|
|
10021
|
+
const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
|
|
10022
|
+
for (const ch of extraChains) {
|
|
10023
|
+
const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
|
|
10024
|
+
if (!seen.has(key)) {
|
|
10025
|
+
seen.add(key);
|
|
10026
|
+
chains.push(ch);
|
|
10027
|
+
}
|
|
10028
|
+
}
|
|
10029
|
+
chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
|
|
10020
10030
|
return { defs, uses, chains };
|
|
10021
10031
|
}
|
|
10022
10032
|
function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
@@ -10041,28 +10051,47 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
|
10041
10051
|
}
|
|
10042
10052
|
}
|
|
10043
10053
|
}
|
|
10044
|
-
function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
10054
|
+
function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
|
|
10055
|
+
const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
|
|
10056
|
+
if (right.endPosition.row <= right.startPosition.row) return;
|
|
10057
|
+
for (let ui = useStart; ui < useEnd; ui++) {
|
|
10058
|
+
const u = uses[ui];
|
|
10059
|
+
if (!u || u.def_id === null) continue;
|
|
10060
|
+
for (let di = defStart; di < defEnd; di++) {
|
|
10061
|
+
const d = defs[di];
|
|
10062
|
+
if (d && d.kind === "local" && d.id !== u.def_id) {
|
|
10063
|
+
extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
|
|
10064
|
+
}
|
|
10065
|
+
}
|
|
10066
|
+
}
|
|
10067
|
+
};
|
|
10045
10068
|
walkTree(node, (child) => {
|
|
10046
10069
|
if (child.type === "short_var_declaration") {
|
|
10047
10070
|
const left = child.childForFieldName("left");
|
|
10048
10071
|
const right = child.childForFieldName("right");
|
|
10072
|
+
const usesBefore = uses.length;
|
|
10049
10073
|
if (right) {
|
|
10050
10074
|
extractGoUses(right, uses, scopeStack);
|
|
10051
10075
|
}
|
|
10076
|
+
const defsBefore = defs.length;
|
|
10052
10077
|
if (left) {
|
|
10053
10078
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
10054
10079
|
}
|
|
10080
|
+
if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
10055
10081
|
} else if (child.type === "var_declaration") {
|
|
10056
10082
|
processGoVarDecl(child, defs, scopeStack, counters);
|
|
10057
10083
|
} else if (child.type === "assignment_statement") {
|
|
10058
10084
|
const left = child.childForFieldName("left");
|
|
10059
10085
|
const right = child.childForFieldName("right");
|
|
10086
|
+
const usesBefore = uses.length;
|
|
10060
10087
|
if (right) {
|
|
10061
10088
|
extractGoUses(right, uses, scopeStack);
|
|
10062
10089
|
}
|
|
10090
|
+
const defsBefore = defs.length;
|
|
10063
10091
|
if (left) {
|
|
10064
10092
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
10065
10093
|
}
|
|
10094
|
+
if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
10066
10095
|
} else if (child.type === "for_statement") {
|
|
10067
10096
|
const rangeClause = findChildByTypeGo(child, "range_clause");
|
|
10068
10097
|
if (rangeClause) {
|
|
@@ -9915,6 +9915,7 @@ var GO_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
9915
9915
|
function buildGoDFG(tree) {
|
|
9916
9916
|
const defs = [];
|
|
9917
9917
|
const uses = [];
|
|
9918
|
+
const extraChains = [];
|
|
9918
9919
|
let defIdCounter = 1;
|
|
9919
9920
|
let useIdCounter = 1;
|
|
9920
9921
|
const scopeStack = [/* @__PURE__ */ new Map()];
|
|
@@ -9936,7 +9937,7 @@ function buildGoDFG(tree) {
|
|
|
9936
9937
|
}
|
|
9937
9938
|
const body2 = func2.childForFieldName("body");
|
|
9938
9939
|
if (body2) {
|
|
9939
|
-
processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
|
|
9940
|
+
processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
|
|
9940
9941
|
defIdCounter = defs.length + 1;
|
|
9941
9942
|
useIdCounter = uses.length + 1;
|
|
9942
9943
|
}
|
|
@@ -9951,6 +9952,15 @@ function buildGoDFG(tree) {
|
|
|
9951
9952
|
}
|
|
9952
9953
|
}
|
|
9953
9954
|
const chains = computeChains(defs, uses);
|
|
9955
|
+
const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
|
|
9956
|
+
for (const ch of extraChains) {
|
|
9957
|
+
const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
|
|
9958
|
+
if (!seen.has(key)) {
|
|
9959
|
+
seen.add(key);
|
|
9960
|
+
chains.push(ch);
|
|
9961
|
+
}
|
|
9962
|
+
}
|
|
9963
|
+
chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
|
|
9954
9964
|
return { defs, uses, chains };
|
|
9955
9965
|
}
|
|
9956
9966
|
function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
@@ -9975,28 +9985,47 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
|
|
|
9975
9985
|
}
|
|
9976
9986
|
}
|
|
9977
9987
|
}
|
|
9978
|
-
function processGoBlock(node, defs, uses, scopeStack, counters) {
|
|
9988
|
+
function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
|
|
9989
|
+
const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
|
|
9990
|
+
if (right.endPosition.row <= right.startPosition.row) return;
|
|
9991
|
+
for (let ui = useStart; ui < useEnd; ui++) {
|
|
9992
|
+
const u = uses[ui];
|
|
9993
|
+
if (!u || u.def_id === null) continue;
|
|
9994
|
+
for (let di = defStart; di < defEnd; di++) {
|
|
9995
|
+
const d = defs[di];
|
|
9996
|
+
if (d && d.kind === "local" && d.id !== u.def_id) {
|
|
9997
|
+
extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
|
|
9998
|
+
}
|
|
9999
|
+
}
|
|
10000
|
+
}
|
|
10001
|
+
};
|
|
9979
10002
|
walkTree(node, (child) => {
|
|
9980
10003
|
if (child.type === "short_var_declaration") {
|
|
9981
10004
|
const left = child.childForFieldName("left");
|
|
9982
10005
|
const right = child.childForFieldName("right");
|
|
10006
|
+
const usesBefore = uses.length;
|
|
9983
10007
|
if (right) {
|
|
9984
10008
|
extractGoUses(right, uses, scopeStack);
|
|
9985
10009
|
}
|
|
10010
|
+
const defsBefore = defs.length;
|
|
9986
10011
|
if (left) {
|
|
9987
10012
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
9988
10013
|
}
|
|
10014
|
+
if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
9989
10015
|
} else if (child.type === "var_declaration") {
|
|
9990
10016
|
processGoVarDecl(child, defs, scopeStack, counters);
|
|
9991
10017
|
} else if (child.type === "assignment_statement") {
|
|
9992
10018
|
const left = child.childForFieldName("left");
|
|
9993
10019
|
const right = child.childForFieldName("right");
|
|
10020
|
+
const usesBefore = uses.length;
|
|
9994
10021
|
if (right) {
|
|
9995
10022
|
extractGoUses(right, uses, scopeStack);
|
|
9996
10023
|
}
|
|
10024
|
+
const defsBefore = defs.length;
|
|
9997
10025
|
if (left) {
|
|
9998
10026
|
extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
|
|
9999
10027
|
}
|
|
10028
|
+
if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
|
|
10000
10029
|
} else if (child.type === "for_statement") {
|
|
10001
10030
|
const rangeClause = findChildByTypeGo(child, "range_clause");
|
|
10002
10031
|
if (rangeClause) {
|