circle-ir 3.80.0 → 3.82.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/xss.yaml +2 -1
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +26 -4
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/_credential-helpers.d.ts +40 -0
- package/dist/analysis/passes/_credential-helpers.d.ts.map +1 -0
- package/dist/analysis/passes/_credential-helpers.js +152 -0
- package/dist/analysis/passes/_credential-helpers.js.map +1 -0
- package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts +42 -0
- package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts.map +1 -0
- package/dist/analysis/passes/cleartext-credential-transport-pass.js +196 -0
- package/dist/analysis/passes/cleartext-credential-transport-pass.js.map +1 -0
- package/dist/analysis/passes/info-disclosure-stacktrace-pass.d.ts +48 -0
- package/dist/analysis/passes/info-disclosure-stacktrace-pass.d.ts.map +1 -0
- package/dist/analysis/passes/info-disclosure-stacktrace-pass.js +222 -0
- package/dist/analysis/passes/info-disclosure-stacktrace-pass.js.map +1 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts +47 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts.map +1 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.js +159 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.js.map +1 -0
- package/dist/analysis/passes/unrestricted-file-upload-pass.d.ts +46 -0
- package/dist/analysis/passes/unrestricted-file-upload-pass.d.ts.map +1 -0
- package/dist/analysis/passes/unrestricted-file-upload-pass.js +193 -0
- package/dist/analysis/passes/unrestricted-file-upload-pass.js.map +1 -0
- package/dist/analysis/passes/weak-password-encoding-pass.d.ts +40 -0
- package/dist/analysis/passes/weak-password-encoding-pass.d.ts.map +1 -0
- package/dist/analysis/passes/weak-password-encoding-pass.js +157 -0
- package/dist/analysis/passes/weak-password-encoding-pass.js.map +1 -0
- package/dist/analysis/passes/weak-password-hash-pass.d.ts +49 -0
- package/dist/analysis/passes/weak-password-hash-pass.d.ts.map +1 -0
- package/dist/analysis/passes/weak-password-hash-pass.js +225 -0
- package/dist/analysis/passes/weak-password-hash-pass.js.map +1 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +18 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +912 -4
- package/dist/core/circle-ir-core.cjs +26 -4
- package/dist/core/circle-ir-core.js +26 -4
- package/package.json +1 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pass: info-disclosure-stacktrace (CWE-209, category: security)
|
|
3
|
+
*
|
|
4
|
+
* Detects exception stack traces / messages being returned to remote clients
|
|
5
|
+
* via an HTTP response handler. This leaks framework internals, file paths,
|
|
6
|
+
* SQL fragments, and class names — useful reconnaissance for an attacker.
|
|
7
|
+
*
|
|
8
|
+
* Detection per language:
|
|
9
|
+
* Java:
|
|
10
|
+
* - `e.printStackTrace(response.getWriter())` / `.printStackTrace(out)`
|
|
11
|
+
* where `out` is a response writer.
|
|
12
|
+
* - `response.getWriter().write(e.toString())`
|
|
13
|
+
* - `response.getWriter().println(e.getMessage())`
|
|
14
|
+
* - `new ResponseEntity<>(e.getStackTrace(), …)`
|
|
15
|
+
*
|
|
16
|
+
* Python:
|
|
17
|
+
* - `return traceback.format_exc()` from a handler-like function
|
|
18
|
+
* - `flask.jsonify(error=traceback.format_exc())`
|
|
19
|
+
* - Bare `return str(e)` / `return {"error": str(e)}` in handler
|
|
20
|
+
*
|
|
21
|
+
* JS/TS:
|
|
22
|
+
* - `res.send(err.stack)` / `res.json({error: err.stack})`
|
|
23
|
+
* - `res.json(err)` (whole error object)
|
|
24
|
+
* - `res.status(N).send(err.message + err.stack)` / similar
|
|
25
|
+
*
|
|
26
|
+
* Go:
|
|
27
|
+
* - `http.Error(w, err.Error()+debug.Stack(), 500)` — narrow
|
|
28
|
+
* - `fmt.Fprintln(w, err)` in an HTTP handler
|
|
29
|
+
*
|
|
30
|
+
* Negative guard: if the consumer is a logger (`console.error`,
|
|
31
|
+
* `logger.error`, `log.Error`), do NOT fire — logging stack traces
|
|
32
|
+
* server-side is not a leak.
|
|
33
|
+
*/
|
|
34
|
+
/** Receiver names that almost always indicate an HTTP response handle. */
|
|
35
|
+
const RESPONSE_RECEIVER_RE = /^(res|response|w|writer|ctx|c)$/i;
|
|
36
|
+
/** Logger receivers (negative guard). */
|
|
37
|
+
const LOGGER_RECEIVER_RE = /^(log|logger|slog|console|pino|winston|sentry)$/i;
|
|
38
|
+
/** Method names on a response that send data to the client. */
|
|
39
|
+
const RESPONSE_SEND_METHODS = new Set([
|
|
40
|
+
'send', 'json', 'write', 'writeHead', 'end', 'sendFile',
|
|
41
|
+
'println', 'print', 'getWriter',
|
|
42
|
+
'Fprintln', 'Fprintf', 'Fprint',
|
|
43
|
+
]);
|
|
44
|
+
/** Expression heuristics for "this is an exception value". */
|
|
45
|
+
function isExceptionExpression(expr) {
|
|
46
|
+
if (!expr)
|
|
47
|
+
return false;
|
|
48
|
+
const e = expr.trim();
|
|
49
|
+
// err.stack | err.message | e.toString() | e.getMessage() | e.getStackTrace()
|
|
50
|
+
// exc.format_exc() | traceback.format_exc() | str(e)
|
|
51
|
+
return (/\b(err|error|exc|exception|e|t|throwable)\.(stack|message|toString\(|getMessage\(|getStackTrace\(|getLocalizedMessage\(|getCause\()/i.test(e) ||
|
|
52
|
+
/\btraceback\.(format_exc|format_exception|print_exc)\b/i.test(e) ||
|
|
53
|
+
/\bdebug\.Stack\(\)/.test(e) ||
|
|
54
|
+
/\bstr\(\s*(err|error|exc|exception|e)\s*\)/i.test(e) ||
|
|
55
|
+
/\bString\(\s*(err|error|exc|exception|e)\s*\)/i.test(e));
|
|
56
|
+
}
|
|
57
|
+
/** True if an argument carries an exception-like value. */
|
|
58
|
+
function argIsException(arg) {
|
|
59
|
+
if (!arg)
|
|
60
|
+
return false;
|
|
61
|
+
if (arg.variable && /^(err|error|exc|exception|e|t|throwable)$/i.test(arg.variable)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return isExceptionExpression(arg.expression);
|
|
65
|
+
}
|
|
66
|
+
/** Detect Java: e.printStackTrace(out) where out is a response writer. */
|
|
67
|
+
function detectJavaPrintStackTrace(call) {
|
|
68
|
+
if (call.method_name !== 'printStackTrace')
|
|
69
|
+
return null;
|
|
70
|
+
// Receiver should look like an exception variable name.
|
|
71
|
+
const rec = call.receiver ?? '';
|
|
72
|
+
if (!/^(e|ex|exc|exception|err|error|t|throwable)$/i.test(rec))
|
|
73
|
+
return null;
|
|
74
|
+
// Arg 0 should be a response writer expression.
|
|
75
|
+
const arg0 = call.arguments.find((a) => a.position === 0);
|
|
76
|
+
if (!arg0)
|
|
77
|
+
return null;
|
|
78
|
+
const expr = (arg0.expression ?? arg0.variable ?? '').trim();
|
|
79
|
+
if (/\bresponse\.getWriter\(\)/.test(expr) ||
|
|
80
|
+
/\bresp\.getWriter\(\)/.test(expr) ||
|
|
81
|
+
/\bout\b/.test(expr) || // common name; conservative
|
|
82
|
+
/\bgetWriter\(\)/.test(expr)) {
|
|
83
|
+
return 'e.printStackTrace(response.getWriter())';
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/** Detect calls of the shape `response.send(err.stack)` / `.json(err)`. */
|
|
88
|
+
function detectResponseLeakCall(call) {
|
|
89
|
+
const method = call.method_name ?? '';
|
|
90
|
+
const receiver = call.receiver ?? '';
|
|
91
|
+
if (!RESPONSE_SEND_METHODS.has(method))
|
|
92
|
+
return null;
|
|
93
|
+
if (LOGGER_RECEIVER_RE.test(receiver))
|
|
94
|
+
return null;
|
|
95
|
+
// Accept either a bare known receiver name, or one whose tail is a known name
|
|
96
|
+
// (e.g. `ctx.response`, `event.res`, `response.status(500)` chained returns).
|
|
97
|
+
const recTail = receiver.split('.').pop() ?? receiver;
|
|
98
|
+
const recHead = receiver.split('.')[0] ?? receiver;
|
|
99
|
+
if (!RESPONSE_RECEIVER_RE.test(recTail) && !RESPONSE_RECEIVER_RE.test(recHead)) {
|
|
100
|
+
// Allow chained: res.status(500).send(...) — receiver text often contains
|
|
101
|
+
// `.status(` substring.
|
|
102
|
+
if (!/(?:^|[.\s])(res|response)\.(?:status|set|header|cookie)\b/i.test(receiver)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Any argument contains an exception expression?
|
|
107
|
+
for (const a of call.arguments) {
|
|
108
|
+
if (argIsException(a)) {
|
|
109
|
+
return `${receiver || ''}${receiver ? '.' : ''}${method}(${(a.expression ?? a.variable ?? '').trim()})`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
/** Detect Python: `return traceback.format_exc()` inside any function. */
|
|
115
|
+
function detectPythonTracebackReturn(ctx) {
|
|
116
|
+
const out = [];
|
|
117
|
+
const lines = ctx.code.split('\n');
|
|
118
|
+
for (let i = 0; i < lines.length; i++) {
|
|
119
|
+
const ln = lines[i] ?? '';
|
|
120
|
+
if (/\breturn\s+traceback\.format_exc\s*\(\s*\)/.test(ln) ||
|
|
121
|
+
/\breturn\s+\{[^}]*traceback\.format_exc\s*\(\s*\)[^}]*\}/.test(ln) ||
|
|
122
|
+
/\bjsonify\s*\([^)]*traceback\.format_exc\s*\(\s*\)/.test(ln)) {
|
|
123
|
+
out.push({ line: i + 1, api: 'return traceback.format_exc()' });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// `return str(e)` in a handler context — conservative: require the
|
|
127
|
+
// surrounding 5-line window to contain a Flask/FastAPI/Django marker.
|
|
128
|
+
if (/\breturn\s+(?:str|repr)\s*\(\s*(?:e|err|error|exc|exception)\s*\)/.test(ln)) {
|
|
129
|
+
const start = Math.max(0, i - 8);
|
|
130
|
+
const end = Math.min(lines.length, i + 2);
|
|
131
|
+
const window = lines.slice(start, end).join('\n');
|
|
132
|
+
if (/@(?:app|router|blueprint)\.(?:route|get|post|put|delete|patch)\b/.test(window)) {
|
|
133
|
+
out.push({ line: i + 1, api: 'return str(e) in handler' });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
export class InfoDisclosureStacktracePass {
|
|
140
|
+
name = 'info-disclosure-stacktrace';
|
|
141
|
+
category = 'security';
|
|
142
|
+
run(ctx) {
|
|
143
|
+
const { graph, language } = ctx;
|
|
144
|
+
const file = graph.ir.meta.file;
|
|
145
|
+
const findings = [];
|
|
146
|
+
if (language === 'python') {
|
|
147
|
+
for (const f of detectPythonTracebackReturn(ctx)) {
|
|
148
|
+
findings.push({ line: f.line, api: f.api, language });
|
|
149
|
+
ctx.addFinding(this.makeFinding(file, f.line, f.api));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const call of graph.ir.calls) {
|
|
153
|
+
let api = null;
|
|
154
|
+
if (language === 'java') {
|
|
155
|
+
api = detectJavaPrintStackTrace(call);
|
|
156
|
+
if (!api)
|
|
157
|
+
api = detectResponseLeakCall(call);
|
|
158
|
+
}
|
|
159
|
+
else if (language === 'javascript' || language === 'typescript') {
|
|
160
|
+
api = detectResponseLeakCall(call);
|
|
161
|
+
}
|
|
162
|
+
else if (language === 'go') {
|
|
163
|
+
// http.Error(w, err.Error()+debug.Stack(), 500)
|
|
164
|
+
// fmt.Fprintln(w, err)
|
|
165
|
+
const method = call.method_name ?? '';
|
|
166
|
+
const rec = call.receiver ?? '';
|
|
167
|
+
if (rec === 'http' && method === 'Error') {
|
|
168
|
+
const arg1 = call.arguments.find((a) => a.position === 1);
|
|
169
|
+
if (argIsException(arg1))
|
|
170
|
+
api = 'http.Error(w, err.Error())';
|
|
171
|
+
}
|
|
172
|
+
else if (rec === 'fmt' && (method === 'Fprintln' || method === 'Fprintf' || method === 'Fprint')) {
|
|
173
|
+
const arg0 = call.arguments.find((a) => a.position === 0);
|
|
174
|
+
if (arg0 && /^(w|writer|resp|response)$/i.test((arg0.variable ?? arg0.expression ?? '').trim())) {
|
|
175
|
+
for (const a of call.arguments) {
|
|
176
|
+
if (a.position === 0)
|
|
177
|
+
continue;
|
|
178
|
+
if (argIsException(a)) {
|
|
179
|
+
api = `fmt.${method}(w, err)`;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
api = detectResponseLeakCall(call);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (language === 'python') {
|
|
190
|
+
// Handle response leak shape too: e.g. `return jsonify(stack=...)`
|
|
191
|
+
api = detectResponseLeakCall(call);
|
|
192
|
+
}
|
|
193
|
+
if (!api)
|
|
194
|
+
continue;
|
|
195
|
+
const line = call.location.line;
|
|
196
|
+
findings.push({ line, api, language });
|
|
197
|
+
ctx.addFinding(this.makeFinding(file, line, api));
|
|
198
|
+
}
|
|
199
|
+
return { findings };
|
|
200
|
+
}
|
|
201
|
+
makeFinding(file, line, api) {
|
|
202
|
+
return {
|
|
203
|
+
id: `${this.name}-${file}-${line}`,
|
|
204
|
+
pass: this.name,
|
|
205
|
+
category: this.category,
|
|
206
|
+
rule_id: this.name,
|
|
207
|
+
cwe: 'CWE-209',
|
|
208
|
+
severity: 'medium',
|
|
209
|
+
level: 'warning',
|
|
210
|
+
message: `Exception detail returned to client via \`${api}\`. ` +
|
|
211
|
+
'Leaking stack traces / exception messages reveals framework internals, ' +
|
|
212
|
+
'file paths, and class names — useful reconnaissance for an attacker.',
|
|
213
|
+
file,
|
|
214
|
+
line,
|
|
215
|
+
fix: 'Return a generic error response to the client (e.g. status 500 + a ' +
|
|
216
|
+
'request id) and log the full exception server-side via your logger ' +
|
|
217
|
+
'(e.g. `logger.error("…", e)` or `console.error(err)`).',
|
|
218
|
+
evidence: { api },
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=info-disclosure-stacktrace-pass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"info-disclosure-stacktrace-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/info-disclosure-stacktrace-pass.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pass: plaintext-password-storage (CWE-256, category: security)
|
|
3
|
+
*
|
|
4
|
+
* Detects writing a credential-named identifier to a persistent store
|
|
5
|
+
* (file, KV store, cookie, database) without first passing it through a
|
|
6
|
+
* cryptographic hash / KDF.
|
|
7
|
+
*
|
|
8
|
+
* Detection per language:
|
|
9
|
+
* Python:
|
|
10
|
+
* - `open(...).write(password)` / `f.write(password)`
|
|
11
|
+
* - `pickle.dump(password, ...)` / `json.dump(...)` / `yaml.dump(...)`
|
|
12
|
+
* - `redis.set(key, password)`
|
|
13
|
+
* JS/TS:
|
|
14
|
+
* - `fs.writeFile|writeFileSync|appendFile(path, password)`
|
|
15
|
+
* - `localStorage.setItem(key, password)` / `sessionStorage.setItem`
|
|
16
|
+
* - `redis.set(key, password)`
|
|
17
|
+
* Java:
|
|
18
|
+
* - `Files.write|writeString(path, password)`
|
|
19
|
+
* - `FileWriter.write(password)`
|
|
20
|
+
* Go:
|
|
21
|
+
* - `os.WriteFile(name, []byte(password), ...)`
|
|
22
|
+
* - `f.WriteString(password)` / `f.Write([]byte(password))`
|
|
23
|
+
*
|
|
24
|
+
* Heuristic for "not hashed": intraprocedural — walk all calls earlier
|
|
25
|
+
* in the same `in_method` scope; if any of them is a known hash/KDF
|
|
26
|
+
* (see _credential-helpers `isHashFunctionCall`) and consumes the
|
|
27
|
+
* credential identifier, suppress.
|
|
28
|
+
*
|
|
29
|
+
* This is intentionally lightweight (no full DFG); FP risk skewed toward
|
|
30
|
+
* recall loss for cross-function hashing (controller → service.hash →
|
|
31
|
+
* repo.store), which is acceptable for v1.
|
|
32
|
+
*/
|
|
33
|
+
import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
|
|
34
|
+
export interface PlaintextPasswordStorageResult {
|
|
35
|
+
findings: Array<{
|
|
36
|
+
line: number;
|
|
37
|
+
language: string;
|
|
38
|
+
api: string;
|
|
39
|
+
identifier: string;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
export declare class PlaintextPasswordStoragePass implements AnalysisPass<PlaintextPasswordStorageResult> {
|
|
43
|
+
readonly name = "plaintext-password-storage";
|
|
44
|
+
readonly category: "security";
|
|
45
|
+
run(ctx: PassContext): PlaintextPasswordStorageResult;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=plaintext-password-storage-pass.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plaintext-password-storage-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/plaintext-password-storage-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAc9E,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,KAAK,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;CACJ;AAsED,qBAAa,4BACX,YAAW,YAAY,CAAC,8BAA8B,CAAC;IAEvD,QAAQ,CAAC,IAAI,gCAAgC;IAC7C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,8BAA8B;CAkEtD"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pass: plaintext-password-storage (CWE-256, category: security)
|
|
3
|
+
*
|
|
4
|
+
* Detects writing a credential-named identifier to a persistent store
|
|
5
|
+
* (file, KV store, cookie, database) without first passing it through a
|
|
6
|
+
* cryptographic hash / KDF.
|
|
7
|
+
*
|
|
8
|
+
* Detection per language:
|
|
9
|
+
* Python:
|
|
10
|
+
* - `open(...).write(password)` / `f.write(password)`
|
|
11
|
+
* - `pickle.dump(password, ...)` / `json.dump(...)` / `yaml.dump(...)`
|
|
12
|
+
* - `redis.set(key, password)`
|
|
13
|
+
* JS/TS:
|
|
14
|
+
* - `fs.writeFile|writeFileSync|appendFile(path, password)`
|
|
15
|
+
* - `localStorage.setItem(key, password)` / `sessionStorage.setItem`
|
|
16
|
+
* - `redis.set(key, password)`
|
|
17
|
+
* Java:
|
|
18
|
+
* - `Files.write|writeString(path, password)`
|
|
19
|
+
* - `FileWriter.write(password)`
|
|
20
|
+
* Go:
|
|
21
|
+
* - `os.WriteFile(name, []byte(password), ...)`
|
|
22
|
+
* - `f.WriteString(password)` / `f.Write([]byte(password))`
|
|
23
|
+
*
|
|
24
|
+
* Heuristic for "not hashed": intraprocedural — walk all calls earlier
|
|
25
|
+
* in the same `in_method` scope; if any of them is a known hash/KDF
|
|
26
|
+
* (see _credential-helpers `isHashFunctionCall`) and consumes the
|
|
27
|
+
* credential identifier, suppress.
|
|
28
|
+
*
|
|
29
|
+
* This is intentionally lightweight (no full DFG); FP risk skewed toward
|
|
30
|
+
* recall loss for cross-function hashing (controller → service.hash →
|
|
31
|
+
* repo.store), which is acceptable for v1.
|
|
32
|
+
*/
|
|
33
|
+
import { argLooksLikeCredential, priorHashOf, } from './_credential-helpers.js';
|
|
34
|
+
function isWriteStorageCall(call, language) {
|
|
35
|
+
const method = call.method_name ?? '';
|
|
36
|
+
const receiver = call.receiver ?? '';
|
|
37
|
+
const recvLower = receiver.toLowerCase();
|
|
38
|
+
if (language === 'python') {
|
|
39
|
+
// open(...).write(pw) — receiver is a file handle; we approximate by
|
|
40
|
+
// method name `write` and check arg credential below.
|
|
41
|
+
if (method === 'write' || method === 'writelines') {
|
|
42
|
+
return { credPos: 0, api: `<file>.${method}` };
|
|
43
|
+
}
|
|
44
|
+
if ((recvLower === 'pickle' || recvLower === 'json' || recvLower === 'yaml') &&
|
|
45
|
+
(method === 'dump' || method === 'dumps')) {
|
|
46
|
+
return { credPos: 0, api: `${receiver}.${method}` };
|
|
47
|
+
}
|
|
48
|
+
if (recvLower === 'redis' && (method === 'set' || method === 'setex' || method === 'hset')) {
|
|
49
|
+
return { credPos: 1, api: `redis.${method}` };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
53
|
+
if ((recvLower === 'fs' || recvLower.endsWith('.fs')) &&
|
|
54
|
+
(method === 'writeFile' || method === 'writeFileSync' ||
|
|
55
|
+
method === 'appendFile' || method === 'appendFileSync')) {
|
|
56
|
+
return { credPos: 1, api: `fs.${method}` };
|
|
57
|
+
}
|
|
58
|
+
if ((recvLower === 'localstorage' || recvLower === 'sessionstorage') &&
|
|
59
|
+
method === 'setItem') {
|
|
60
|
+
return { credPos: 1, api: `${receiver}.setItem` };
|
|
61
|
+
}
|
|
62
|
+
if (recvLower === 'redis' && (method === 'set' || method === 'setex' || method === 'hset')) {
|
|
63
|
+
return { credPos: 1, api: `redis.${method}` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (language === 'java') {
|
|
67
|
+
if ((receiver === 'Files' || receiver.endsWith('.Files')) &&
|
|
68
|
+
(method === 'write' || method === 'writeString')) {
|
|
69
|
+
return { credPos: 1, api: `Files.${method}` };
|
|
70
|
+
}
|
|
71
|
+
// FileWriter.write(pw) — instance call, single arg.
|
|
72
|
+
if (method === 'write') {
|
|
73
|
+
// Heuristic: receiver name contains "writer" / "file" / "stream".
|
|
74
|
+
const lc = (receiver ?? '').toLowerCase();
|
|
75
|
+
if (lc.includes('writer') || lc.includes('file') || lc.includes('stream')) {
|
|
76
|
+
return { credPos: 0, api: `${receiver}.write` };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (language === 'go') {
|
|
81
|
+
if (receiver === 'os' || receiver.endsWith('/os')) {
|
|
82
|
+
if (method === 'WriteFile')
|
|
83
|
+
return { credPos: 1, api: 'os.WriteFile' };
|
|
84
|
+
}
|
|
85
|
+
if (receiver === 'ioutil' || receiver.endsWith('/ioutil')) {
|
|
86
|
+
if (method === 'WriteFile')
|
|
87
|
+
return { credPos: 1, api: 'ioutil.WriteFile' };
|
|
88
|
+
}
|
|
89
|
+
if (method === 'WriteString' || method === 'Write') {
|
|
90
|
+
return { credPos: 0, api: `<file>.${method}` };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
export class PlaintextPasswordStoragePass {
|
|
96
|
+
name = 'plaintext-password-storage';
|
|
97
|
+
category = 'security';
|
|
98
|
+
run(ctx) {
|
|
99
|
+
const { graph, language } = ctx;
|
|
100
|
+
const file = graph.ir.meta.file;
|
|
101
|
+
const findings = [];
|
|
102
|
+
// Group calls by in_method for cheap prior-hash lookup.
|
|
103
|
+
const callsByScope = new Map();
|
|
104
|
+
for (const call of graph.ir.calls) {
|
|
105
|
+
const scope = call.in_method ?? '<top>';
|
|
106
|
+
const arr = callsByScope.get(scope) ?? [];
|
|
107
|
+
arr.push(call);
|
|
108
|
+
callsByScope.set(scope, arr);
|
|
109
|
+
}
|
|
110
|
+
for (const call of graph.ir.calls) {
|
|
111
|
+
const spec = isWriteStorageCall(call, language);
|
|
112
|
+
if (!spec)
|
|
113
|
+
continue;
|
|
114
|
+
const credArg = call.arguments.find((a) => a.position === spec.credPos);
|
|
115
|
+
if (!credArg)
|
|
116
|
+
continue;
|
|
117
|
+
if (!argLooksLikeCredential(credArg))
|
|
118
|
+
continue;
|
|
119
|
+
// Resolve the credential identifier name.
|
|
120
|
+
const identExpr = (credArg.expression ?? '').trim();
|
|
121
|
+
const head = identExpr.split(/[.\s(]/, 1)[0] ?? '';
|
|
122
|
+
const identifier = credArg.variable ?? head;
|
|
123
|
+
if (!identifier)
|
|
124
|
+
continue;
|
|
125
|
+
// Suppress if the identifier was hashed earlier in the same scope.
|
|
126
|
+
const scope = call.in_method ?? '<top>';
|
|
127
|
+
const scopeCalls = callsByScope.get(scope) ?? [];
|
|
128
|
+
const prior = scopeCalls.filter((c) => c.location.line < call.location.line);
|
|
129
|
+
if (priorHashOf(identifier, prior))
|
|
130
|
+
continue;
|
|
131
|
+
// Suppress if the credArg expression itself contains a hash call
|
|
132
|
+
// inline: `f.write(bcrypt.hashpw(pw))`.
|
|
133
|
+
if (/\b(?:hashpw|hash|sha\d+|md5|bcrypt|argon2|pbkdf2|digest)\b/i
|
|
134
|
+
.test(credArg.expression ?? '')) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const line = call.location.line;
|
|
138
|
+
findings.push({ line, language, api: spec.api, identifier });
|
|
139
|
+
ctx.addFinding({
|
|
140
|
+
id: `${this.name}-${file}-${line}`,
|
|
141
|
+
pass: this.name,
|
|
142
|
+
category: this.category,
|
|
143
|
+
rule_id: this.name,
|
|
144
|
+
cwe: 'CWE-256',
|
|
145
|
+
severity: 'high',
|
|
146
|
+
level: 'warning',
|
|
147
|
+
message: `Credential \`${identifier}\` written in plaintext via \`${spec.api}\`. ` +
|
|
148
|
+
'Passwords / secrets must be hashed (Argon2id, bcrypt) before storage.',
|
|
149
|
+
file,
|
|
150
|
+
line,
|
|
151
|
+
fix: 'Hash the credential with Argon2id / bcrypt before writing it to ' +
|
|
152
|
+
'disk, cookie, KV store, or database.',
|
|
153
|
+
evidence: { identifier, api: spec.api, language },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return { findings };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=plaintext-password-storage-pass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plaintext-password-storage-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/plaintext-password-storage-pass.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pass: unrestricted-file-upload (CWE-434, category: security)
|
|
3
|
+
*
|
|
4
|
+
* Detects when an HTTP-uploaded file is saved to disk WITHOUT a filename
|
|
5
|
+
* allow-list (extension check) or `secure_filename` normalization.
|
|
6
|
+
*
|
|
7
|
+
* Detection (per language):
|
|
8
|
+
*
|
|
9
|
+
* Java (Spring MultipartFile / Servlet Part):
|
|
10
|
+
* - `file.transferTo(new File(dir, file.getOriginalFilename()))`
|
|
11
|
+
* - `Files.copy(part.getInputStream(), Path.of(dir, part.getSubmittedFileName()))`
|
|
12
|
+
* - Without preceding `ALLOWED_*.contains(ext)` or
|
|
13
|
+
* `FilenameUtils.getExtension(name)` + check.
|
|
14
|
+
*
|
|
15
|
+
* JS/TS (multer / express-fileupload):
|
|
16
|
+
* - `multer({ dest: '…' })` with NO `fileFilter` option.
|
|
17
|
+
* - `fs.writeFile(path, req.file.buffer)` / `req.files.x.mv(path)`
|
|
18
|
+
* without prior `path.extname` allow-list check.
|
|
19
|
+
*
|
|
20
|
+
* Python (Flask / Django / FastAPI):
|
|
21
|
+
* - `f.save(os.path.join(UPLOAD_DIR, f.filename))` without prior
|
|
22
|
+
* `secure_filename(f.filename)` wrapping.
|
|
23
|
+
*
|
|
24
|
+
* Go:
|
|
25
|
+
* - `io.Copy(dst, file)` where `dst = os.Create(fileHeader.Filename)`
|
|
26
|
+
* without an extension allow-list.
|
|
27
|
+
*
|
|
28
|
+
* The pass is intentionally conservative — it only fires when an upload-name
|
|
29
|
+
* expression flows directly into a save sink in the same function and no
|
|
30
|
+
* known allow-list / canonicalizer call appears earlier in the function.
|
|
31
|
+
*/
|
|
32
|
+
import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
|
|
33
|
+
export interface UnrestrictedFileUploadResult {
|
|
34
|
+
findings: Array<{
|
|
35
|
+
line: number;
|
|
36
|
+
api: string;
|
|
37
|
+
language: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
export declare class UnrestrictedFileUploadPass implements AnalysisPass<UnrestrictedFileUploadResult> {
|
|
41
|
+
readonly name = "unrestricted-file-upload";
|
|
42
|
+
readonly category: "security";
|
|
43
|
+
run(ctx: PassContext): UnrestrictedFileUploadResult;
|
|
44
|
+
private emit;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=unrestricted-file-upload-pass.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unrestricted-file-upload-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/unrestricted-file-upload-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAG9E,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClE;AA2BD,qBAAa,0BAA2B,YAAW,YAAY,CAAC,4BAA4B,CAAC;IAC3F,QAAQ,CAAC,IAAI,8BAA8B;IAC3C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,4BAA4B;IAyHnD,OAAO,CAAC,IAAI;CAgCb"}
|