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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromptInjectionSafetyGatePass — cognium-dev #267
|
|
3
|
+
*
|
|
4
|
+
* Drops `prompt_injection` (CWE-1427) sinks whose untrusted content is
|
|
5
|
+
* *delimiter-wrapped* — enclosed in matched boundary markers
|
|
6
|
+
* (`<user_question>…</user_question>`, fences) that keep it as data rather
|
|
7
|
+
* than instructions. Removes the false positive on the delimiter-wrapped
|
|
8
|
+
* "safe mirror" of the OWASP-LLM LLM01 probe pairs while leaving genuine
|
|
9
|
+
* prompt-injection flows untouched.
|
|
10
|
+
*
|
|
11
|
+
* The engine already fires `prompt_injection` when tainted input reaches an
|
|
12
|
+
* LLM-client call (#248); the gap this closes is precision on the wrapped
|
|
13
|
+
* safe-mitigation shape.
|
|
14
|
+
*
|
|
15
|
+
* Scope decision — *role separation alone is NOT treated as a sanitizer*
|
|
16
|
+
* here: `{"role":"user","content":untrusted}` continues to fire, matching
|
|
17
|
+
* #248's must-fire contract (untrusted reaching an LLM prompt is a finding
|
|
18
|
+
* even in the user channel). Only affirmative delimiter-wrapping suppresses.
|
|
19
|
+
*
|
|
20
|
+
* Conservative by construction: a sink is dropped ONLY when EVERY dynamic
|
|
21
|
+
* content placement is delimiter-wrapped and none lands in the instruction
|
|
22
|
+
* channel (system/assistant role, or instruction concat/interpolation). Any
|
|
23
|
+
* ambiguity — unparsed value, standalone value, dynamic+dynamic concat —
|
|
24
|
+
* keeps the sink (no recall loss). Runs after SinkFilterPass and before
|
|
25
|
+
* TaintPropagationPass so flow generators never see a dropped sink.
|
|
26
|
+
*
|
|
27
|
+
* Builder-pattern aware: when the request is constructed in a prior
|
|
28
|
+
* statement (Go/Java `req := ChatCompletionRequest{Messages: …}`) and passed
|
|
29
|
+
* as a bare arg, the gate traces the builder var(s) to see the message
|
|
30
|
+
* structure.
|
|
31
|
+
*
|
|
32
|
+
* Pillar I note: `prompt_injection` / CWE-1427 is a deterministic taint
|
|
33
|
+
* category (#248). This gate is pure structural analysis of the call site —
|
|
34
|
+
* no model invocation, no LLM concepts in its API surface.
|
|
35
|
+
*/
|
|
36
|
+
import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
|
|
37
|
+
export interface PromptInjectionSafetyGateResult {
|
|
38
|
+
droppedSafe: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A string literal is a *delimiter* (structural boundary marker) rather
|
|
42
|
+
* than *instruction text* when it is a short tag/fence-like token:
|
|
43
|
+
* `<user_question>`, `</user_question>`, `[data]`, ```` ``` ````, `"""`,
|
|
44
|
+
* `---`, `###`, `<<<`. Instruction literals ("Follow policy.") contain free
|
|
45
|
+
* words and do not match.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isDelimiterLiteral(lit: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Scan a balanced expression starting at `start`, stopping at a top-level
|
|
50
|
+
* `,` or a closing `) ] }` that closes the enclosing structure. Tracks
|
|
51
|
+
* bracket depth and string state so `}`/`)` inside strings (f-string
|
|
52
|
+
* `{user}`, template `${user}`) or nested calls do not truncate the value.
|
|
53
|
+
*/
|
|
54
|
+
export declare function scanValue(code: string, start: number): string;
|
|
55
|
+
/** `+`-concat mixing a dynamic value with a NON-delimiter string literal. */
|
|
56
|
+
export declare function isInstructionConcat(valueExpr: string): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Classify a prompt-injection call site.
|
|
59
|
+
* 'unsafe' — untrusted in the instruction channel, or mixed into instructions
|
|
60
|
+
* 'safe' — every dynamic content is standalone role-separated or delimiter-wrapped
|
|
61
|
+
* 'unknown' — no confident classification (kept)
|
|
62
|
+
*/
|
|
63
|
+
export declare function classifyPromptCall(callCode: string, codeLines: string[]): 'unsafe' | 'safe' | 'unknown';
|
|
64
|
+
/**
|
|
65
|
+
* Classify a prompt-injection sink, first on the call line (Python/JS inline
|
|
66
|
+
* message shape) and — when that is inconclusive — over the builder region
|
|
67
|
+
* for the call's arg vars (Go/Java builder shape).
|
|
68
|
+
*/
|
|
69
|
+
export declare function classifyPromptSink(callCode: string, codeLines: string[]): 'unsafe' | 'safe' | 'unknown';
|
|
70
|
+
export declare class PromptInjectionSafetyGatePass implements AnalysisPass<PromptInjectionSafetyGateResult> {
|
|
71
|
+
readonly name = "prompt-injection-safety-gate";
|
|
72
|
+
readonly category: "security";
|
|
73
|
+
run(ctx: PassContext): PromptInjectionSafetyGateResult;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=prompt-injection-safety-gate-pass.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-injection-safety-gate-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/prompt-injection-safety-gate-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAI9E,MAAM,WAAW,+BAA+B;IAC9C,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAYvD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CA2B7D;AAiBD,6EAA6E;AAC7E,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAM9D;AAkFD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EAAE,GAClB,QAAQ,GAAG,MAAM,GAAG,SAAS,CAwB/B;AA8DD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EAAE,GAClB,QAAQ,GAAG,MAAM,GAAG,SAAS,CAM/B;AAED,qBAAa,6BACX,YAAW,YAAY,CAAC,+BAA+B,CAAC;IAExD,QAAQ,CAAC,IAAI,kCAAkC;IAC/C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,+BAA+B;CA4BvD"}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromptInjectionSafetyGatePass — cognium-dev #267
|
|
3
|
+
*
|
|
4
|
+
* Drops `prompt_injection` (CWE-1427) sinks whose untrusted content is
|
|
5
|
+
* *delimiter-wrapped* — enclosed in matched boundary markers
|
|
6
|
+
* (`<user_question>…</user_question>`, fences) that keep it as data rather
|
|
7
|
+
* than instructions. Removes the false positive on the delimiter-wrapped
|
|
8
|
+
* "safe mirror" of the OWASP-LLM LLM01 probe pairs while leaving genuine
|
|
9
|
+
* prompt-injection flows untouched.
|
|
10
|
+
*
|
|
11
|
+
* The engine already fires `prompt_injection` when tainted input reaches an
|
|
12
|
+
* LLM-client call (#248); the gap this closes is precision on the wrapped
|
|
13
|
+
* safe-mitigation shape.
|
|
14
|
+
*
|
|
15
|
+
* Scope decision — *role separation alone is NOT treated as a sanitizer*
|
|
16
|
+
* here: `{"role":"user","content":untrusted}` continues to fire, matching
|
|
17
|
+
* #248's must-fire contract (untrusted reaching an LLM prompt is a finding
|
|
18
|
+
* even in the user channel). Only affirmative delimiter-wrapping suppresses.
|
|
19
|
+
*
|
|
20
|
+
* Conservative by construction: a sink is dropped ONLY when EVERY dynamic
|
|
21
|
+
* content placement is delimiter-wrapped and none lands in the instruction
|
|
22
|
+
* channel (system/assistant role, or instruction concat/interpolation). Any
|
|
23
|
+
* ambiguity — unparsed value, standalone value, dynamic+dynamic concat —
|
|
24
|
+
* keeps the sink (no recall loss). Runs after SinkFilterPass and before
|
|
25
|
+
* TaintPropagationPass so flow generators never see a dropped sink.
|
|
26
|
+
*
|
|
27
|
+
* Builder-pattern aware: when the request is constructed in a prior
|
|
28
|
+
* statement (Go/Java `req := ChatCompletionRequest{Messages: …}`) and passed
|
|
29
|
+
* as a bare arg, the gate traces the builder var(s) to see the message
|
|
30
|
+
* structure.
|
|
31
|
+
*
|
|
32
|
+
* Pillar I note: `prompt_injection` / CWE-1427 is a deterministic taint
|
|
33
|
+
* category (#248). This gate is pure structural analysis of the call site —
|
|
34
|
+
* no model invocation, no LLM concepts in its API surface.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* A string literal is a *delimiter* (structural boundary marker) rather
|
|
38
|
+
* than *instruction text* when it is a short tag/fence-like token:
|
|
39
|
+
* `<user_question>`, `</user_question>`, `[data]`, ```` ``` ````, `"""`,
|
|
40
|
+
* `---`, `###`, `<<<`. Instruction literals ("Follow policy.") contain free
|
|
41
|
+
* words and do not match.
|
|
42
|
+
*/
|
|
43
|
+
export function isDelimiterLiteral(lit) {
|
|
44
|
+
const s = lit.trim();
|
|
45
|
+
if (s.length === 0)
|
|
46
|
+
return true;
|
|
47
|
+
return (/^<\/?[\w.-]+\s*\/?>$/.test(s) ||
|
|
48
|
+
/^\[\/?[\w.-]+\]$/.test(s) ||
|
|
49
|
+
/^`{1,3}[\w-]*$/.test(s) ||
|
|
50
|
+
/^"""[\w-]*$/.test(s) ||
|
|
51
|
+
/^[#*=_~-]{2,}$/.test(s) ||
|
|
52
|
+
/^<{2,}[\w-]*$/.test(s) ||
|
|
53
|
+
/^[\w-]*>{2,}$/.test(s));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Scan a balanced expression starting at `start`, stopping at a top-level
|
|
57
|
+
* `,` or a closing `) ] }` that closes the enclosing structure. Tracks
|
|
58
|
+
* bracket depth and string state so `}`/`)` inside strings (f-string
|
|
59
|
+
* `{user}`, template `${user}`) or nested calls do not truncate the value.
|
|
60
|
+
*/
|
|
61
|
+
export function scanValue(code, start) {
|
|
62
|
+
let depth = 0;
|
|
63
|
+
let str = null;
|
|
64
|
+
let out = '';
|
|
65
|
+
for (let i = start; i < code.length; i++) {
|
|
66
|
+
const c = code[i];
|
|
67
|
+
if (str) {
|
|
68
|
+
out += c;
|
|
69
|
+
if (c === str && code[i - 1] !== '\\')
|
|
70
|
+
str = null;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
74
|
+
str = c;
|
|
75
|
+
out += c;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (c === '(' || c === '[' || c === '{') {
|
|
79
|
+
depth++;
|
|
80
|
+
out += c;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (c === ')' || c === ']' || c === '}') {
|
|
84
|
+
if (depth === 0)
|
|
85
|
+
break;
|
|
86
|
+
depth--;
|
|
87
|
+
out += c;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (c === ',' && depth === 0)
|
|
91
|
+
break;
|
|
92
|
+
out += c;
|
|
93
|
+
}
|
|
94
|
+
return out.trim();
|
|
95
|
+
}
|
|
96
|
+
function stringLiterals(expr) {
|
|
97
|
+
return [...expr.matchAll(/"([^"]*)"|'([^']*)'/g)].map(m => m[1] ?? m[2] ?? '');
|
|
98
|
+
}
|
|
99
|
+
/** Reference to a bare identifier (a dynamic value) once literals removed. */
|
|
100
|
+
function hasIdentifier(expr) {
|
|
101
|
+
return /[A-Za-z_$][\w$]*/.test(expr.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, ' '));
|
|
102
|
+
}
|
|
103
|
+
/** A content value that is entirely a string literal (no dynamic part). */
|
|
104
|
+
function isPureLiteral(valueExpr) {
|
|
105
|
+
const stripped = valueExpr.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, ' ');
|
|
106
|
+
return !/[A-Za-z_$][\w$]*/.test(stripped);
|
|
107
|
+
}
|
|
108
|
+
/** `+`-concat mixing a dynamic value with a NON-delimiter string literal. */
|
|
109
|
+
export function isInstructionConcat(valueExpr) {
|
|
110
|
+
if (!valueExpr.includes('+'))
|
|
111
|
+
return false;
|
|
112
|
+
if (!hasIdentifier(valueExpr))
|
|
113
|
+
return false;
|
|
114
|
+
const lits = stringLiterals(valueExpr);
|
|
115
|
+
if (lits.length === 0)
|
|
116
|
+
return false;
|
|
117
|
+
return !lits.every(isDelimiterLiteral);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* f-string / template literal that mixes untrusted interpolation with free
|
|
121
|
+
* instruction text: `f"Follow policy. {user}"` / `` `Follow policy. ${user}` ``.
|
|
122
|
+
* Pure interpolation (`f"{user}"`) is not instruction-mixing.
|
|
123
|
+
*/
|
|
124
|
+
function isMixedTemplate(v) {
|
|
125
|
+
const fstr = v.match(/\bf(["'])((?:\\.|(?!\1).)*)\1/);
|
|
126
|
+
if (fstr) {
|
|
127
|
+
const body = fstr[2];
|
|
128
|
+
if (/\{[^}]+\}/.test(body) && /\w/.test(body.replace(/\{[^}]*\}/g, '')))
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
const tmpl = v.match(/`([^`]*)`/);
|
|
132
|
+
if (tmpl) {
|
|
133
|
+
const body = tmpl[1];
|
|
134
|
+
if (/\$\{[^}]+\}/.test(body) && /\w/.test(body.replace(/\$\{[^}]*\}/g, '')))
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Assignment-operator matcher for `<var> := …` / `<var> = …`, excluding the
|
|
141
|
+
* comparisons `==` / `<=` / `>=` / `!=`. `:=` is matched directly; a plain
|
|
142
|
+
* `=` must not be preceded by `=<>!` nor followed by `=`.
|
|
143
|
+
*/
|
|
144
|
+
function assignmentRe(varName, flags = '') {
|
|
145
|
+
return new RegExp(`\\b${varName}\\s*(?::=|(?<![=<>!])=(?!=))\\s*`, flags);
|
|
146
|
+
}
|
|
147
|
+
/** Resolve a bare-identifier content value one hop back to its assignment RHS. */
|
|
148
|
+
function resolveAssignmentRHS(varName, codeLines) {
|
|
149
|
+
const re = assignmentRe(varName);
|
|
150
|
+
for (const line of codeLines) {
|
|
151
|
+
const m = line.match(re);
|
|
152
|
+
if (m && m.index !== undefined) {
|
|
153
|
+
return scanValue(line, m.index + m[0].length);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Classify a fully-scanned content expression.
|
|
160
|
+
* 'safe' — standalone dynamic value or delimiter-wrapped concat
|
|
161
|
+
* 'unsafe' — instruction concat / instruction-mixing template
|
|
162
|
+
* 'unknown' — dynamic+dynamic concat / unrecognized — kept
|
|
163
|
+
*/
|
|
164
|
+
function classifyExpr(v) {
|
|
165
|
+
if (isMixedTemplate(v))
|
|
166
|
+
return 'unsafe';
|
|
167
|
+
if (v.includes('+')) {
|
|
168
|
+
if (isInstructionConcat(v))
|
|
169
|
+
return 'unsafe';
|
|
170
|
+
const lits = stringLiterals(v);
|
|
171
|
+
if (lits.length > 0 && lits.every(isDelimiterLiteral) && hasIdentifier(v))
|
|
172
|
+
return 'safe';
|
|
173
|
+
return 'unknown'; // dynamic+dynamic / complex — never claim safe
|
|
174
|
+
}
|
|
175
|
+
// No '+', no instruction-mixing template: a standalone value placed as
|
|
176
|
+
// the whole content. Whether this is "safe" (role-separation as the
|
|
177
|
+
// mitigation) is a security-semantics choice that conflicts with #248's
|
|
178
|
+
// must-fire contract — left as 'unknown' (kept) pending that decision;
|
|
179
|
+
// only delimiter-wrapping is treated as an affirmative sanitizer.
|
|
180
|
+
return 'unknown';
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Classify one dynamic content placement, resolving a bare identifier one
|
|
184
|
+
* hop back so `wrapped = "<q>"+x+"</q>"; content: wrapped` and
|
|
185
|
+
* `bad = "instr "+x; content: bad` are judged on their construction.
|
|
186
|
+
*/
|
|
187
|
+
function classifyContentPlacement(val, codeLines) {
|
|
188
|
+
const v = val.trim();
|
|
189
|
+
if (/^[A-Za-z_$][\w$]*$/.test(v)) {
|
|
190
|
+
const rhs = resolveAssignmentRHS(v, codeLines);
|
|
191
|
+
// Unresolved bare identifier (param, source var, or local whose
|
|
192
|
+
// construction we can't see) — standalone/role-separated placement,
|
|
193
|
+
// which is NOT an affirmative sanitizer (kept; see scope note).
|
|
194
|
+
if (rhs === undefined)
|
|
195
|
+
return 'unknown';
|
|
196
|
+
return classifyExpr(rhs);
|
|
197
|
+
}
|
|
198
|
+
return classifyExpr(v);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Classify a prompt-injection call site.
|
|
202
|
+
* 'unsafe' — untrusted in the instruction channel, or mixed into instructions
|
|
203
|
+
* 'safe' — every dynamic content is standalone role-separated or delimiter-wrapped
|
|
204
|
+
* 'unknown' — no confident classification (kept)
|
|
205
|
+
*/
|
|
206
|
+
export function classifyPromptCall(callCode, codeLines) {
|
|
207
|
+
const contentRe = /["']?[Cc]ontent["']?\s*[:=]\s*/g;
|
|
208
|
+
let m;
|
|
209
|
+
let sawDynamic = false;
|
|
210
|
+
let allSafe = true;
|
|
211
|
+
while ((m = contentRe.exec(callCode)) !== null) {
|
|
212
|
+
const val = scanValue(callCode, m.index + m[0].length);
|
|
213
|
+
if (val === '' || isPureLiteral(val))
|
|
214
|
+
continue;
|
|
215
|
+
sawDynamic = true;
|
|
216
|
+
// Role context: nearest preceding `role: "..."` in the same object.
|
|
217
|
+
const ctx = callCode.slice(Math.max(0, m.index - 80), m.index);
|
|
218
|
+
const roleM = ctx.match(/["']?[Rr]ole["']?\s*[:=]\s*["'](\w+)["'][^"']*$/);
|
|
219
|
+
const role = roleM ? roleM[1].toLowerCase() : 'user';
|
|
220
|
+
if (role === 'system' || role === 'assistant')
|
|
221
|
+
return 'unsafe';
|
|
222
|
+
const placement = classifyContentPlacement(val, codeLines);
|
|
223
|
+
if (placement === 'unsafe')
|
|
224
|
+
return 'unsafe';
|
|
225
|
+
if (placement !== 'safe')
|
|
226
|
+
allSafe = false;
|
|
227
|
+
}
|
|
228
|
+
if (!sawDynamic)
|
|
229
|
+
return 'unknown';
|
|
230
|
+
return allSafe ? 'safe' : 'unknown';
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Scan a statement RHS across lines, stopping at a top-level newline (Go/
|
|
234
|
+
* builder style) or a closing bracket that ends the expression — so a
|
|
235
|
+
* multi-line composite literal `T{ … }` is captured whole. Tracks bracket
|
|
236
|
+
* depth and string state.
|
|
237
|
+
*/
|
|
238
|
+
function scanRhsAcrossLines(text, start) {
|
|
239
|
+
let depth = 0;
|
|
240
|
+
let str = null;
|
|
241
|
+
let out = '';
|
|
242
|
+
for (let i = start; i < text.length; i++) {
|
|
243
|
+
const c = text[i];
|
|
244
|
+
if (str) {
|
|
245
|
+
out += c;
|
|
246
|
+
if (c === str && text[i - 1] !== '\\')
|
|
247
|
+
str = null;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
251
|
+
str = c;
|
|
252
|
+
out += c;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (c === '(' || c === '[' || c === '{') {
|
|
256
|
+
depth++;
|
|
257
|
+
out += c;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (c === ')' || c === ']' || c === '}') {
|
|
261
|
+
if (depth === 0)
|
|
262
|
+
break;
|
|
263
|
+
depth--;
|
|
264
|
+
out += c;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (c === '\n' && depth === 0)
|
|
268
|
+
break;
|
|
269
|
+
out += c;
|
|
270
|
+
}
|
|
271
|
+
return out.trim();
|
|
272
|
+
}
|
|
273
|
+
/** Full multi-line RHS of `var := …` / `var = …`. */
|
|
274
|
+
function resolveBuilderRhs(varName, joined) {
|
|
275
|
+
const re = assignmentRe(varName, 'g');
|
|
276
|
+
const m = re.exec(joined);
|
|
277
|
+
if (m)
|
|
278
|
+
return scanRhsAcrossLines(joined, m.index + m[0].length);
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Builder-pattern support (Go/Java): when the request is built in a prior
|
|
283
|
+
* statement (`req := ChatCompletionRequest{Messages: …}`) and passed as a
|
|
284
|
+
* bare arg to the client call, the role/content structure is not on the
|
|
285
|
+
* sink line. Gather the builder text for each identifier arg of the call,
|
|
286
|
+
* recursively inlining referenced builder vars (bounded), so the classifier
|
|
287
|
+
* can see the message structure. Returns the concatenated builder region.
|
|
288
|
+
*/
|
|
289
|
+
function collectBuilderRegion(callCode, joined) {
|
|
290
|
+
const parts = [];
|
|
291
|
+
const seen = new Set();
|
|
292
|
+
const stack = [...callCode.matchAll(/[A-Za-z_$][\w$]*/g)].map(x => x[0]);
|
|
293
|
+
let budget = 40;
|
|
294
|
+
while (stack.length > 0 && budget-- > 0) {
|
|
295
|
+
const v = stack.pop();
|
|
296
|
+
if (seen.has(v))
|
|
297
|
+
continue;
|
|
298
|
+
seen.add(v);
|
|
299
|
+
const rhs = resolveBuilderRhs(v, joined);
|
|
300
|
+
if (rhs) {
|
|
301
|
+
parts.push(rhs);
|
|
302
|
+
for (const mm of rhs.matchAll(/[A-Za-z_$][\w$]*/g))
|
|
303
|
+
stack.push(mm[0]);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return parts.join('\n');
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Classify a prompt-injection sink, first on the call line (Python/JS inline
|
|
310
|
+
* message shape) and — when that is inconclusive — over the builder region
|
|
311
|
+
* for the call's arg vars (Go/Java builder shape).
|
|
312
|
+
*/
|
|
313
|
+
export function classifyPromptSink(callCode, codeLines) {
|
|
314
|
+
const direct = classifyPromptCall(callCode, codeLines);
|
|
315
|
+
if (direct !== 'unknown')
|
|
316
|
+
return direct;
|
|
317
|
+
const region = collectBuilderRegion(callCode, codeLines.join('\n'));
|
|
318
|
+
if (!region)
|
|
319
|
+
return 'unknown';
|
|
320
|
+
return classifyPromptCall(region, codeLines);
|
|
321
|
+
}
|
|
322
|
+
export class PromptInjectionSafetyGatePass {
|
|
323
|
+
name = 'prompt-injection-safety-gate';
|
|
324
|
+
category = 'security';
|
|
325
|
+
run(ctx) {
|
|
326
|
+
const { graph, code } = ctx;
|
|
327
|
+
const sinks = ctx.hasResult('sink-filter')
|
|
328
|
+
? ctx.getResult('sink-filter').sinks
|
|
329
|
+
: graph.ir.taint.sinks;
|
|
330
|
+
if (sinks.length === 0)
|
|
331
|
+
return { droppedSafe: 0 };
|
|
332
|
+
const codeLines = code.split('\n');
|
|
333
|
+
let droppedSafe = 0;
|
|
334
|
+
const kept = sinks.filter(sink => {
|
|
335
|
+
if (sink.type !== 'prompt_injection')
|
|
336
|
+
return true;
|
|
337
|
+
const callCode = sink.code ?? codeLines[sink.line - 1] ?? '';
|
|
338
|
+
if (!callCode)
|
|
339
|
+
return true;
|
|
340
|
+
if (classifyPromptSink(callCode, codeLines) === 'safe') {
|
|
341
|
+
droppedSafe++;
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
return true;
|
|
345
|
+
});
|
|
346
|
+
if (droppedSafe > 0) {
|
|
347
|
+
sinks.length = 0;
|
|
348
|
+
sinks.push(...kept);
|
|
349
|
+
}
|
|
350
|
+
return { droppedSafe };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
//# sourceMappingURL=prompt-injection-safety-gate-pass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-injection-safety-gate-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/prompt-injection-safety-gate-pass.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpeculativePromptParamSourcePass — cognium-dev #267
|
|
3
|
+
*
|
|
4
|
+
* OPT-IN, OFF-BY-DEFAULT. When `AnalyzerOptions.speculativeParamSources` is
|
|
5
|
+
* set, seeds function parameters as untrusted sources within functions that
|
|
6
|
+
* already contain a `prompt_injection` (CWE-1427) sink. This unblocks
|
|
7
|
+
* library / agent / SDK code whose untrusted entry point is a bare
|
|
8
|
+
* parameter (`func Ask(user string, client *LLM)`) — there is no in-file
|
|
9
|
+
* request source to seed taint, so the param → prompt-sink flow never fires
|
|
10
|
+
* under the deterministic default.
|
|
11
|
+
*
|
|
12
|
+
* This is a deliberately speculative, higher-FP heuristic. It is gated
|
|
13
|
+
* behind an explicit flag that defaults OFF, so the precision-sensitive
|
|
14
|
+
* benchmarks (OWASP / Juliet / SecuriBench) — which never set it — are
|
|
15
|
+
* unaffected and the 0%-FPR moat is preserved. Scope is bounded to
|
|
16
|
+
* parameters of functions containing a prompt-construction sink, not every
|
|
17
|
+
* function parameter.
|
|
18
|
+
*
|
|
19
|
+
* Runs after the sink gates (so the prompt_injection sink set is final) and
|
|
20
|
+
* before TaintPropagationPass, appending to the sink-filter source set so
|
|
21
|
+
* the normal flow generators connect param → sink. Pillar-I clean: no model
|
|
22
|
+
* invocation, no LLM identifiers in its surface (`prompt_injection` /
|
|
23
|
+
* CWE-1427 is the established deterministic taint category, #248).
|
|
24
|
+
*/
|
|
25
|
+
import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
|
|
26
|
+
import type { TaintFlowInfo, TypeInfo } from '../../types/index.js';
|
|
27
|
+
/**
|
|
28
|
+
* Heuristic: does a string literal read like LLM *prompt / instruction*
|
|
29
|
+
* text rather than an incidental separator or short token? Used to keep the
|
|
30
|
+
* client-less construction detector off ordinary string concatenation
|
|
31
|
+
* (`"/" + name`, `"Hello, " + name`) that structurally looks identical but
|
|
32
|
+
* is not a prompt. Fires on instruction phrasing or a full sentence.
|
|
33
|
+
*/
|
|
34
|
+
export declare function looksLikePromptText(lit: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Detect *client-less* prompt-construction flows (cognium-dev #267,
|
|
37
|
+
* speculative). A function that returns or assigns an instruction-literal
|
|
38
|
+
* concatenated with one of its own parameters builds a prompt from
|
|
39
|
+
* untrusted input even without an LLM-client call
|
|
40
|
+
* (`func BuildPrompt(user string) string { return "You are a bot. " + user }`).
|
|
41
|
+
* Emitted directly as `prompt_injection` (CWE-1427) flows — there is no
|
|
42
|
+
* call sink for the flow generators to match. Delimiter-wrapped concats are
|
|
43
|
+
* NOT instruction-concat, so the safe mirror produces no flow.
|
|
44
|
+
*
|
|
45
|
+
* Off-default: only invoked when `speculativeParamSources` is set (the
|
|
46
|
+
* benchmarks never enable it).
|
|
47
|
+
*/
|
|
48
|
+
export declare function detectPromptConstructionFlows(codeLines: string[], types: TypeInfo[]): TaintFlowInfo[];
|
|
49
|
+
export interface SpeculativePromptParamSourceResult {
|
|
50
|
+
added: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A parameter is a plausible untrusted-text entry point when its type is
|
|
54
|
+
* unknown (untyped languages — Python/JS) or string-ish. Handle/dependency
|
|
55
|
+
* params (a client, context, writer, DB, logger — typically pointer or
|
|
56
|
+
* well-known infrastructure types) are skipped to keep the speculative FP
|
|
57
|
+
* surface off obviously-non-data parameters.
|
|
58
|
+
*/
|
|
59
|
+
export declare function looksLikeTextParam(type: string | null | undefined): boolean;
|
|
60
|
+
export declare class SpeculativePromptParamSourcePass implements AnalysisPass<SpeculativePromptParamSourceResult> {
|
|
61
|
+
private readonly enabled;
|
|
62
|
+
readonly name = "speculative-prompt-param-source";
|
|
63
|
+
readonly category: "security";
|
|
64
|
+
constructor(enabled: boolean);
|
|
65
|
+
run(ctx: PassContext): SpeculativePromptParamSourceResult;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=speculative-prompt-param-source-pass.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"speculative-prompt-param-source-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/speculative-prompt-param-source-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAE9E,OAAO,KAAK,EAA0B,aAAa,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAiB5F;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASxD;AAOD;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EAAE,EACnB,KAAK,EAAE,QAAQ,EAAE,GAChB,aAAa,EAAE,CAqCjB;AAED,MAAM,WAAW,kCAAkC;IACjD,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAU3E;AAED,qBAAa,gCACX,YAAW,YAAY,CAAC,kCAAkC,CAAC;IAK/C,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,QAAQ,CAAC,IAAI,qCAAqC;IAClD,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;gBAEX,OAAO,EAAE,OAAO;IAE7C,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,kCAAkC;CAqD1D"}
|