auditai-scan 0.4.0 → 0.6.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/auditai-scan.mjs +1431 -415
- package/package.json +1 -1
package/dist/auditai-scan.mjs
CHANGED
|
@@ -78,20 +78,20 @@ var IllegalTransitionError = class extends Error {
|
|
|
78
78
|
to;
|
|
79
79
|
name = "IllegalTransitionError";
|
|
80
80
|
};
|
|
81
|
-
function transition(
|
|
82
|
-
if (!canTransition(
|
|
83
|
-
throw new IllegalTransitionError(
|
|
81
|
+
function transition(finding4, to, opts = {}) {
|
|
82
|
+
if (!canTransition(finding4.status, to)) {
|
|
83
|
+
throw new IllegalTransitionError(finding4.id, finding4.status, to);
|
|
84
84
|
}
|
|
85
85
|
const requiresEvidence = to === "confirmed" || to === "verified";
|
|
86
86
|
if (requiresEvidence && !opts.evidence) {
|
|
87
|
-
throw new Error(`Finding ${
|
|
87
|
+
throw new Error(`Finding ${finding4.id}: transition to ${to} requires evidence`);
|
|
88
88
|
}
|
|
89
|
-
if (to === "verified" && !isVerificationPassing(
|
|
90
|
-
throw new Error(`Finding ${
|
|
89
|
+
if (to === "verified" && !isVerificationPassing(finding4.verification)) {
|
|
90
|
+
throw new Error(`Finding ${finding4.id}: cannot mark verified without a passing verification`);
|
|
91
91
|
}
|
|
92
|
-
const evidence = opts.evidence ? [...
|
|
92
|
+
const evidence = opts.evidence ? [...finding4.evidence, opts.evidence] : finding4.evidence;
|
|
93
93
|
return {
|
|
94
|
-
...
|
|
94
|
+
...finding4,
|
|
95
95
|
status: to,
|
|
96
96
|
evidence,
|
|
97
97
|
updatedAt: opts.now ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -100,8 +100,8 @@ function transition(finding3, to, opts = {}) {
|
|
|
100
100
|
function isVerificationPassing(v) {
|
|
101
101
|
return v !== void 0 && v.securityTestBefore === "failed" && v.securityTestAfter === "passed" && (v.existingTests === "passed" || v.existingTests === "skipped") && v.rescan === "passed";
|
|
102
102
|
}
|
|
103
|
-
function isDeterministic(
|
|
104
|
-
return
|
|
103
|
+
function isDeterministic(finding4) {
|
|
104
|
+
return finding4.evidence.some((e) => e.kind === "rule" && e.data?.deterministic === true);
|
|
105
105
|
}
|
|
106
106
|
var DEFAULT_BLOCKING_POLICY = {
|
|
107
107
|
minConfidence: 0.8,
|
|
@@ -109,14 +109,14 @@ var DEFAULT_BLOCKING_POLICY = {
|
|
|
109
109
|
blockDeterministicCritical: true
|
|
110
110
|
};
|
|
111
111
|
var CONFIRMED_OR_LATER = ["confirmed", "fix_proposed", "fix_applied"];
|
|
112
|
-
function isBlocking(
|
|
113
|
-
if (
|
|
114
|
-
if (
|
|
115
|
-
if (
|
|
116
|
-
if (CONFIRMED_OR_LATER.includes(
|
|
112
|
+
function isBlocking(finding4, policy = DEFAULT_BLOCKING_POLICY) {
|
|
113
|
+
if (finding4.status === "suppressed") return false;
|
|
114
|
+
if (finding4.status === "verified") return true;
|
|
115
|
+
if (finding4.confidence < policy.minConfidence) return false;
|
|
116
|
+
if (CONFIRMED_OR_LATER.includes(finding4.status) && policy.blockOnConfirmedSeverities.includes(finding4.severity)) {
|
|
117
117
|
return true;
|
|
118
118
|
}
|
|
119
|
-
if (policy.blockDeterministicCritical &&
|
|
119
|
+
if (policy.blockDeterministicCritical && finding4.severity === "critical" && isDeterministic(finding4) && finding4.status !== "unverified") {
|
|
120
120
|
return true;
|
|
121
121
|
}
|
|
122
122
|
return false;
|
|
@@ -158,9 +158,15 @@ function formatScanText(r) {
|
|
|
158
158
|
const s = r.summary;
|
|
159
159
|
const out = [
|
|
160
160
|
`Audit AI scan ${displayRoot(s.root)}`,
|
|
161
|
-
`Files ${s.files} \xB7 Routes ${s.routes} \xB7 Supabase queries ${s.queries} \xB7 Tables with RLS ${s.tablesWithRls}/${s.tablesKnown} \xB7 Rules ${s.rules}
|
|
162
|
-
""
|
|
161
|
+
`Files ${s.files} \xB7 Routes ${s.routes} \xB7 Supabase queries ${s.queries} \xB7 Tables with RLS ${s.tablesWithRls}/${s.tablesKnown} \xB7 Rules ${s.rules}`
|
|
163
162
|
];
|
|
163
|
+
if (s.publicTables.length > 0) {
|
|
164
|
+
const n = r.findings.filter((f) => f.evidence.some((e) => e.data?.publicTables)).length;
|
|
165
|
+
out.push(
|
|
166
|
+
`Declared public in audit.config.json: ${s.publicTables.join(", ")} (${n} read-only finding${n === 1 ? "" : "s"} suppressed by the declaration; write paths are never covered)`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
out.push("");
|
|
164
170
|
if (r.findings.length === 0) {
|
|
165
171
|
out.push(
|
|
166
172
|
`No findings. ${s.routes} route${s.routes === 1 ? "" : "s"} and ${s.queries} quer${s.queries === 1 ? "y" : "ies"} checked.`
|
|
@@ -181,6 +187,9 @@ function formatScanText(r) {
|
|
|
181
187
|
`;
|
|
182
188
|
}
|
|
183
189
|
|
|
190
|
+
// packages/scanner/src/scan.ts
|
|
191
|
+
import { statSync as statSync2 } from "node:fs";
|
|
192
|
+
|
|
184
193
|
// packages/graph/src/graph.ts
|
|
185
194
|
var SecurityGraph = class {
|
|
186
195
|
nodes = /* @__PURE__ */ new Map();
|
|
@@ -255,7 +264,8 @@ function buildGraph(model) {
|
|
|
255
264
|
method: h.method,
|
|
256
265
|
route: h.route,
|
|
257
266
|
inputs: h.inputs,
|
|
258
|
-
metadataAccesses: h.metadataAccesses
|
|
267
|
+
metadataAccesses: h.metadataAccesses,
|
|
268
|
+
roleChecks: h.roleChecks ?? []
|
|
259
269
|
};
|
|
260
270
|
const handler = g.addNode({
|
|
261
271
|
id: `handler:${h.location.file}:${h.location.line}`,
|
|
@@ -295,7 +305,8 @@ function buildGraph(model) {
|
|
|
295
305
|
table: q.table,
|
|
296
306
|
...q.via && q.via.length > 0 ? { via: q.via } : {},
|
|
297
307
|
...q.storage ? { storage: q.storage } : {},
|
|
298
|
-
...q.guard ? { guard: q.guard } : {}
|
|
308
|
+
...q.guard ? { guard: q.guard } : {},
|
|
309
|
+
...q.ownerChecks ? { ownerChecks: q.ownerChecks } : {}
|
|
299
310
|
};
|
|
300
311
|
const qn = g.addNode({
|
|
301
312
|
// Per handler: the same helper query reached from two entry points carries different
|
|
@@ -865,7 +876,7 @@ function isClientComponentFile(sf) {
|
|
|
865
876
|
// packages/parser/src/parse-project.ts
|
|
866
877
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
867
878
|
import { join as join3, resolve as resolve2 } from "node:path";
|
|
868
|
-
import
|
|
879
|
+
import ts11 from "typescript";
|
|
869
880
|
|
|
870
881
|
// packages/parser/src/auth-evidence.ts
|
|
871
882
|
import ts3 from "typescript";
|
|
@@ -1180,6 +1191,36 @@ function missingRowExit(tail, segments) {
|
|
|
1180
1191
|
}
|
|
1181
1192
|
return null;
|
|
1182
1193
|
}
|
|
1194
|
+
var INEQUALITY = /* @__PURE__ */ new Set([
|
|
1195
|
+
ts4.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
1196
|
+
ts4.SyntaxKind.ExclamationEqualsToken
|
|
1197
|
+
]);
|
|
1198
|
+
function rowComparisons(tail) {
|
|
1199
|
+
const { parent } = outerOf(tail);
|
|
1200
|
+
if (!parent || !ts4.isVariableDeclaration(parent)) return [];
|
|
1201
|
+
const { data } = resultNames(parent.name);
|
|
1202
|
+
const out = [];
|
|
1203
|
+
for (const s of ifsAfter(parent)) {
|
|
1204
|
+
const exit = exitKind(s.thenStatement);
|
|
1205
|
+
if (!exit) continue;
|
|
1206
|
+
walk(s.expression, (n) => {
|
|
1207
|
+
if (!ts4.isBinaryExpression(n) || !INEQUALITY.has(n.operatorToken.kind)) return void 0;
|
|
1208
|
+
for (const [a, b] of [
|
|
1209
|
+
[n.left, n.right],
|
|
1210
|
+
[n.right, n.left]
|
|
1211
|
+
]) {
|
|
1212
|
+
const ua = unwrap(a);
|
|
1213
|
+
if (!ts4.isPropertyAccessExpression(ua)) continue;
|
|
1214
|
+
const r = rootName(ua.expression);
|
|
1215
|
+
if (r === null || !data.has(r)) continue;
|
|
1216
|
+
out.push({ column: ua.name.text, value: b, exit });
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
return void 0;
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
return out;
|
|
1223
|
+
}
|
|
1183
1224
|
function callResultChecked(call) {
|
|
1184
1225
|
const { node, parent } = outerOf(call);
|
|
1185
1226
|
if (!parent) return false;
|
|
@@ -1452,10 +1493,17 @@ var Resolver = class {
|
|
|
1452
1493
|
}
|
|
1453
1494
|
this.packages.sort((a, b) => b.name.length - a.name.length);
|
|
1454
1495
|
for (const rel of tsconfigs) {
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1496
|
+
let config;
|
|
1497
|
+
try {
|
|
1498
|
+
const read = ts6.readConfigFile(join2(root, rel), (p) => readFileSync(p, "utf8"));
|
|
1499
|
+
if (read.error) continue;
|
|
1500
|
+
config = read.config;
|
|
1501
|
+
} catch (e) {
|
|
1502
|
+
warnings.push(`could not read ${rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
if (!config || typeof config !== "object") continue;
|
|
1506
|
+
const co = config.compilerOptions && typeof config.compilerOptions === "object" ? config.compilerOptions : {};
|
|
1459
1507
|
const paths = co.paths;
|
|
1460
1508
|
if (!paths || typeof paths !== "object") continue;
|
|
1461
1509
|
const dir = dirOf(rel);
|
|
@@ -1556,7 +1604,7 @@ function isIdentPart(code) {
|
|
|
1556
1604
|
function isDigit(code) {
|
|
1557
1605
|
return code >= 48 && code <= 57;
|
|
1558
1606
|
}
|
|
1559
|
-
function readQuoted(text, from,
|
|
1607
|
+
function readQuoted(text, from, quote2, backslashEscapes) {
|
|
1560
1608
|
let value = "";
|
|
1561
1609
|
let j = from + 1;
|
|
1562
1610
|
while (j < text.length) {
|
|
@@ -1567,9 +1615,9 @@ function readQuoted(text, from, quote, backslashEscapes) {
|
|
|
1567
1615
|
j += 2;
|
|
1568
1616
|
continue;
|
|
1569
1617
|
}
|
|
1570
|
-
if (c ===
|
|
1571
|
-
if (text.charAt(j + 1) ===
|
|
1572
|
-
value +=
|
|
1618
|
+
if (c === quote2) {
|
|
1619
|
+
if (text.charAt(j + 1) === quote2) {
|
|
1620
|
+
value += quote2;
|
|
1573
1621
|
j += 2;
|
|
1574
1622
|
continue;
|
|
1575
1623
|
}
|
|
@@ -2091,6 +2139,213 @@ function parseTableConstraint(tokens) {
|
|
|
2091
2139
|
return { kind: "other", name };
|
|
2092
2140
|
}
|
|
2093
2141
|
|
|
2142
|
+
// packages/parser/src/sql-do-loops.ts
|
|
2143
|
+
function placeholders(fmt) {
|
|
2144
|
+
const out = [];
|
|
2145
|
+
let next = 0;
|
|
2146
|
+
for (let i = 0; i < fmt.length; i += 1) {
|
|
2147
|
+
if (fmt.charAt(i) !== "%") continue;
|
|
2148
|
+
const rest = fmt.slice(i + 1);
|
|
2149
|
+
if (rest.startsWith("%")) {
|
|
2150
|
+
out.push({ kind: "s", arg: -1, start: i, end: i + 2 });
|
|
2151
|
+
i += 1;
|
|
2152
|
+
continue;
|
|
2153
|
+
}
|
|
2154
|
+
const m = /^(?:(\d+)\$)?([IsL])/.exec(rest);
|
|
2155
|
+
if (!m) return null;
|
|
2156
|
+
const arg = m[1] !== void 0 ? Number(m[1]) - 1 : next;
|
|
2157
|
+
next = arg + 1;
|
|
2158
|
+
out.push({ kind: m[2], arg, start: i, end: i + 1 + m[0].length });
|
|
2159
|
+
i += m[0].length;
|
|
2160
|
+
}
|
|
2161
|
+
return out;
|
|
2162
|
+
}
|
|
2163
|
+
function quoteIdent(v) {
|
|
2164
|
+
return /^[a-z_][a-z0-9_]*$/.test(v) ? v : `"${v.replace(/"/g, '""')}"`;
|
|
2165
|
+
}
|
|
2166
|
+
function render(fmt, args) {
|
|
2167
|
+
const ph = placeholders(fmt);
|
|
2168
|
+
if (ph === null) return null;
|
|
2169
|
+
let out = "";
|
|
2170
|
+
let from = 0;
|
|
2171
|
+
for (const p of ph) {
|
|
2172
|
+
out += fmt.slice(from, p.start);
|
|
2173
|
+
from = p.end;
|
|
2174
|
+
if (p.arg < 0) {
|
|
2175
|
+
out += "%";
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
const v = args[p.arg];
|
|
2179
|
+
if (v === void 0) return null;
|
|
2180
|
+
out += p.kind === "I" ? quoteIdent(v) : p.kind === "L" ? `'${v.replace(/'/g, "''")}'` : v;
|
|
2181
|
+
}
|
|
2182
|
+
return out + fmt.slice(from);
|
|
2183
|
+
}
|
|
2184
|
+
function literalArg(part, loopVar, element) {
|
|
2185
|
+
const first = part[0];
|
|
2186
|
+
if (!first) return null;
|
|
2187
|
+
if (part.length === 1 && first.kind === "string") return first.value;
|
|
2188
|
+
if (loopVar === null || element === null) return null;
|
|
2189
|
+
const name = identOf(first);
|
|
2190
|
+
if (name === null || name.toLowerCase() !== loopVar) return null;
|
|
2191
|
+
if (part.length === 1) return element;
|
|
2192
|
+
if (part.length === 3 && isPunct(part[1], ".") && identOf(part[2]) !== null) return element;
|
|
2193
|
+
return null;
|
|
2194
|
+
}
|
|
2195
|
+
function executedSql(tk, loopVar, element) {
|
|
2196
|
+
if (!isWord(tk[0], "execute")) return null;
|
|
2197
|
+
const a = tk[1];
|
|
2198
|
+
if (a?.kind === "string" && tk.length === 2) return a.value;
|
|
2199
|
+
if (!isWord(a, "format") || !isPunct(tk[2], "(")) return null;
|
|
2200
|
+
const parts = splitTopLevelTokens(groupInner(tk, 2));
|
|
2201
|
+
const fmtTok = parts[0]?.[0];
|
|
2202
|
+
if (!fmtTok || parts[0]?.length !== 1 || fmtTok.kind !== "string") return null;
|
|
2203
|
+
const args = [];
|
|
2204
|
+
for (const part of parts.slice(1)) {
|
|
2205
|
+
const v = literalArg(part, loopVar, element);
|
|
2206
|
+
if (v === null) return null;
|
|
2207
|
+
args.push(v);
|
|
2208
|
+
}
|
|
2209
|
+
return render(fmtTok.value, args);
|
|
2210
|
+
}
|
|
2211
|
+
function literalElements(expr) {
|
|
2212
|
+
const arrayAt = expr.findIndex((t, i) => isWord(t, "array") && isPunct(expr[i + 1], "["));
|
|
2213
|
+
if (arrayAt >= 0) {
|
|
2214
|
+
const out = [];
|
|
2215
|
+
for (const part of splitTopLevelTokens(groupInner(expr, arrayAt + 1))) {
|
|
2216
|
+
const t = part[0];
|
|
2217
|
+
if (!t || part.length !== 1 || t.kind !== "string") return null;
|
|
2218
|
+
out.push(t.value);
|
|
2219
|
+
}
|
|
2220
|
+
return out;
|
|
2221
|
+
}
|
|
2222
|
+
const valuesAt = expr.findIndex((t) => isWord(t, "values"));
|
|
2223
|
+
if (valuesAt >= 0) {
|
|
2224
|
+
const out = [];
|
|
2225
|
+
let rows = expr.slice(valuesAt + 1);
|
|
2226
|
+
const close = rows.findIndex((t, i) => isPunct(t, ")") && depthAt(rows, i) < 0);
|
|
2227
|
+
if (close >= 0) rows = rows.slice(0, close);
|
|
2228
|
+
for (const part of splitTopLevelTokens(rows)) {
|
|
2229
|
+
if (!isPunct(part[0], "(")) return null;
|
|
2230
|
+
const inner = groupInner(part, 0);
|
|
2231
|
+
const t = inner[0];
|
|
2232
|
+
if (!t || inner.length !== 1 || t.kind !== "string") return null;
|
|
2233
|
+
out.push(t.value);
|
|
2234
|
+
}
|
|
2235
|
+
return out;
|
|
2236
|
+
}
|
|
2237
|
+
return null;
|
|
2238
|
+
}
|
|
2239
|
+
function depthAt(tokens, i) {
|
|
2240
|
+
let depth = 0;
|
|
2241
|
+
for (let j = 0; j < i; j += 1) {
|
|
2242
|
+
if (isPunct(tokens[j], "(") || isPunct(tokens[j], "[")) depth += 1;
|
|
2243
|
+
else if (isPunct(tokens[j], ")") || isPunct(tokens[j], "]")) depth -= 1;
|
|
2244
|
+
}
|
|
2245
|
+
return depth;
|
|
2246
|
+
}
|
|
2247
|
+
function loopHeader(tk, declared) {
|
|
2248
|
+
const foreach = isWord(tk[0], "foreach");
|
|
2249
|
+
if (!foreach && !isWord(tk[0], "for")) return null;
|
|
2250
|
+
const variable = identOf(tk[1])?.toLowerCase();
|
|
2251
|
+
if (variable === void 0 || !isWord(tk[2], "in")) return null;
|
|
2252
|
+
const loopAt = findWord(tk, 3, "loop");
|
|
2253
|
+
if (loopAt < 0) return null;
|
|
2254
|
+
let from = 3;
|
|
2255
|
+
if (foreach && isWord(tk[from], "array")) from += 1;
|
|
2256
|
+
if (isWord(tk[from], "reverse")) return { variable, elements: null, rest: [] };
|
|
2257
|
+
const expr = tk.slice(from, loopAt);
|
|
2258
|
+
const rest = tk.slice(loopAt + 1);
|
|
2259
|
+
const name = expr.length === 1 ? identOf(expr[0])?.toLowerCase() : void 0;
|
|
2260
|
+
if (name !== void 0) return { variable, elements: declared.get(name) ?? null, rest };
|
|
2261
|
+
const elements = expr.some((t) => t.kind === "op" && t.raw === "..") ? null : literalElements(expr);
|
|
2262
|
+
return { variable, elements, rest };
|
|
2263
|
+
}
|
|
2264
|
+
function declaredList(tk) {
|
|
2265
|
+
let i = 0;
|
|
2266
|
+
if (isWord(tk[i], "declare")) i += 1;
|
|
2267
|
+
const name = identOf(tk[i])?.toLowerCase();
|
|
2268
|
+
if (name === void 0) return null;
|
|
2269
|
+
const assign = tk.findIndex(
|
|
2270
|
+
(t, j) => j > i && (t.kind === "op" && t.raw === ":" && tk[j + 1]?.raw === "=" || isWord(t, "default"))
|
|
2271
|
+
);
|
|
2272
|
+
if (assign < 0) return null;
|
|
2273
|
+
const elements = literalElements(tk.slice(assign + (isWord(tk[assign], "default") ? 1 : 2)));
|
|
2274
|
+
return elements === null ? null : [name, elements];
|
|
2275
|
+
}
|
|
2276
|
+
function stripBegin(tk) {
|
|
2277
|
+
let i = 0;
|
|
2278
|
+
while (isWord(tk[i], "begin")) i += 1;
|
|
2279
|
+
return tk.slice(i);
|
|
2280
|
+
}
|
|
2281
|
+
function conditionalDelta(tk) {
|
|
2282
|
+
if (isWord(tk[0], "if") && findWord(tk, 1, "then") >= 0 || isWord(tk[0], "case")) return 1;
|
|
2283
|
+
if (isWord(tk[0], "end") && (isWord(tk[1], "if") || isWord(tk[1], "case"))) return -1;
|
|
2284
|
+
return 0;
|
|
2285
|
+
}
|
|
2286
|
+
function expandDoBlock(stmt) {
|
|
2287
|
+
const body = stmt.tokens.find((t) => t.kind === "string");
|
|
2288
|
+
const out = { statements: [], dynamic: false };
|
|
2289
|
+
if (!body || !isWord(stmt.tokens[0], "do")) return out;
|
|
2290
|
+
const inner = splitSqlStatements(body.value).map((s) => stripBegin(s.tokens));
|
|
2291
|
+
const emit = (sql) => {
|
|
2292
|
+
if (sql === null) {
|
|
2293
|
+
out.dynamic = true;
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
for (const s of splitSqlStatements(sql)) out.statements.push({ ...s, line: stmt.line });
|
|
2297
|
+
};
|
|
2298
|
+
const declared = /* @__PURE__ */ new Map();
|
|
2299
|
+
for (const tk of inner) {
|
|
2300
|
+
const d = declaredList(tk);
|
|
2301
|
+
if (d) declared.set(d[0], d[1]);
|
|
2302
|
+
}
|
|
2303
|
+
let conditional = 0;
|
|
2304
|
+
let loop = null;
|
|
2305
|
+
const bodyStatements = [];
|
|
2306
|
+
for (const tk of inner) {
|
|
2307
|
+
if (tk.length === 0) continue;
|
|
2308
|
+
if (loop) {
|
|
2309
|
+
if (isWord(tk[0], "end") && isWord(tk[1], "loop")) {
|
|
2310
|
+
if (loop.elements === null) out.dynamic = true;
|
|
2311
|
+
else runLoop(loop, bodyStatements, emit);
|
|
2312
|
+
loop = null;
|
|
2313
|
+
bodyStatements.length = 0;
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
if (loopHeader(tk, declared)) {
|
|
2317
|
+
loop.elements = null;
|
|
2318
|
+
}
|
|
2319
|
+
bodyStatements.push(tk);
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
const header = loopHeader(tk, declared);
|
|
2323
|
+
if (header) {
|
|
2324
|
+
loop = header;
|
|
2325
|
+
if (conditional > 0) loop.elements = null;
|
|
2326
|
+
if (header.rest.length > 0) bodyStatements.push(header.rest);
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
conditional = Math.max(0, conditional + conditionalDelta(tk));
|
|
2330
|
+
if (isWord(tk[0], "execute")) emit(conditional > 0 ? null : executedSql(tk, null, null));
|
|
2331
|
+
}
|
|
2332
|
+
if (loop) out.dynamic = true;
|
|
2333
|
+
return out;
|
|
2334
|
+
}
|
|
2335
|
+
function runLoop(loop, body, emit) {
|
|
2336
|
+
const elements = loop.elements ?? [];
|
|
2337
|
+
let conditional = 0;
|
|
2338
|
+
for (const tk of body) {
|
|
2339
|
+
conditional = Math.max(0, conditional + conditionalDelta(tk));
|
|
2340
|
+
if (!isWord(tk[0], "execute")) continue;
|
|
2341
|
+
if (conditional > 0) {
|
|
2342
|
+
emit(null);
|
|
2343
|
+
continue;
|
|
2344
|
+
}
|
|
2345
|
+
for (const element of elements) emit(executedSql(tk, loop.variable, element));
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2094
2349
|
// packages/parser/src/sql-functions.ts
|
|
2095
2350
|
function newFunctionRegistry() {
|
|
2096
2351
|
return {
|
|
@@ -2548,7 +2803,8 @@ function schemaStateFor(tables) {
|
|
|
2548
2803
|
uniqueIndexes: /* @__PURE__ */ new Map(),
|
|
2549
2804
|
enums: /* @__PURE__ */ new Map(),
|
|
2550
2805
|
functions: newFunctionRegistry(),
|
|
2551
|
-
buckets: newBucketRegistry()
|
|
2806
|
+
buckets: newBucketRegistry(),
|
|
2807
|
+
warnings: []
|
|
2552
2808
|
};
|
|
2553
2809
|
STATES.set(tables, state);
|
|
2554
2810
|
}
|
|
@@ -3022,7 +3278,8 @@ function finishSchema(state) {
|
|
|
3022
3278
|
// fromEntries defines own properties, so a hostile type name like "__proto__" stays a plain key.
|
|
3023
3279
|
enums: Object.fromEntries([...state.enums].map(([k, v]) => [k, [...v]])),
|
|
3024
3280
|
sqlFunctions: finishFunctions(state.functions),
|
|
3025
|
-
storageBuckets: finishBuckets(state.buckets)
|
|
3281
|
+
storageBuckets: finishBuckets(state.buckets),
|
|
3282
|
+
warnings: [...state.warnings]
|
|
3026
3283
|
};
|
|
3027
3284
|
}
|
|
3028
3285
|
|
|
@@ -3045,6 +3302,10 @@ var CREATE_POLICY = new RegExp(
|
|
|
3045
3302
|
String.raw`^create\s+policy\s+("([^"]+)"|\S+)\s+on\s+${QUALIFIED}`,
|
|
3046
3303
|
"i"
|
|
3047
3304
|
);
|
|
3305
|
+
var DROP_POLICY = new RegExp(
|
|
3306
|
+
String.raw`^drop\s+policy\s+(?:if\s+exists\s+)?("([^"]+)"|\S+)\s+on\s+${QUALIFIED}`,
|
|
3307
|
+
"i"
|
|
3308
|
+
);
|
|
3048
3309
|
function isAppliedSqlFile(rel) {
|
|
3049
3310
|
return !/(?:^|\/)supabase\/migrations\/[^/]+\/.+\.sql$/i.test(rel.split("\\").join("/"));
|
|
3050
3311
|
}
|
|
@@ -3052,48 +3313,166 @@ function parseSqlForRls(rel, text, into) {
|
|
|
3052
3313
|
if (!isAppliedSqlFile(rel)) return;
|
|
3053
3314
|
const state = schemaStateFor(into);
|
|
3054
3315
|
for (const stmt of splitSqlStatements(text)) {
|
|
3055
|
-
|
|
3056
|
-
if (!
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
if (c) check = balanced(rest, c.index + c[0].length - 1)?.inner.trim() ?? null;
|
|
3080
|
-
t.policies.push(name);
|
|
3081
|
-
t.policyDetails.push({
|
|
3082
|
-
name,
|
|
3083
|
-
command,
|
|
3084
|
-
roles,
|
|
3085
|
-
using,
|
|
3086
|
-
check,
|
|
3087
|
-
location: { file: rel, line: stmt.line }
|
|
3088
|
-
});
|
|
3316
|
+
applyStatement(state, stmt, rel);
|
|
3317
|
+
if (!isWord(stmt.tokens[0], "do")) continue;
|
|
3318
|
+
const expanded = expandDoBlock(stmt);
|
|
3319
|
+
for (const s of expanded.statements) applyStatement(state, s, rel);
|
|
3320
|
+
if (expanded.dynamic) warnDynamicSql(state, rel);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
function warnDynamicSql(state, rel) {
|
|
3324
|
+
const w = `${rel}: a DO block runs dynamic SQL (a loop over a query, or an EXECUTE that is conditional or built from expressions); RLS, policies and privileges it sets are not seen`;
|
|
3325
|
+
if (!state.warnings.includes(w)) state.warnings.push(w);
|
|
3326
|
+
}
|
|
3327
|
+
var ROLE_NAME = '(?:"[A-Za-z_][A-Za-z0-9_]*"|[A-Za-z_][A-Za-z0-9_]*)';
|
|
3328
|
+
var ROLES = new RegExp(`\\bto\\s+(${ROLE_NAME}(?:\\s*,\\s*${ROLE_NAME})*)`, "i");
|
|
3329
|
+
function headEnd(rest) {
|
|
3330
|
+
const u = /\busing\s*\(/i.exec(rest);
|
|
3331
|
+
const c = /\bwith\s+check\s*\(/i.exec(rest);
|
|
3332
|
+
const ends = [u?.index, c?.index].filter((i) => i !== void 0);
|
|
3333
|
+
return ends.length === 0 ? rest.length : Math.min(...ends);
|
|
3334
|
+
}
|
|
3335
|
+
function applyStatement(state, stmt, rel) {
|
|
3336
|
+
const cp = CREATE_POLICY.exec(stmt.text);
|
|
3337
|
+
if (!cp?.[1] || !cp[4]) {
|
|
3338
|
+
dropPolicy(state, stmt) || applySchemaStatement(state, stmt, rel);
|
|
3339
|
+
return;
|
|
3089
3340
|
}
|
|
3341
|
+
const t = ensureTable(
|
|
3342
|
+
state,
|
|
3343
|
+
qualifiedKey({ schema: cp[3] ?? null, name: cp[4] }),
|
|
3344
|
+
rel,
|
|
3345
|
+
stmt.line
|
|
3346
|
+
);
|
|
3347
|
+
const name = cp[2] ?? cp[1];
|
|
3348
|
+
const rest = stmt.text.slice(cp[0].length);
|
|
3349
|
+
const cmdMatch = /\bfor\s+(select|insert|update|delete|all)\b/i.exec(rest);
|
|
3350
|
+
const command = cmdMatch?.[1]?.toLowerCase() ?? "all";
|
|
3351
|
+
const head = rest.slice(0, headEnd(rest));
|
|
3352
|
+
const rolesMatch = ROLES.exec(head);
|
|
3353
|
+
const roles = rolesMatch?.[1] ? rolesMatch[1].split(/\s*,\s*/).map((r) => r.replace(/"/g, "").toLowerCase()) : [];
|
|
3354
|
+
let using = null;
|
|
3355
|
+
let check = null;
|
|
3356
|
+
const u = /\busing\s*\(/i.exec(rest);
|
|
3357
|
+
if (u) using = balanced(rest, u.index + u[0].length - 1)?.inner.trim() ?? null;
|
|
3358
|
+
const c = /\bwith\s+check\s*\(/i.exec(rest);
|
|
3359
|
+
if (c) check = balanced(rest, c.index + c[0].length - 1)?.inner.trim() ?? null;
|
|
3360
|
+
t.policies.push(name);
|
|
3361
|
+
t.policyDetails.push({
|
|
3362
|
+
name,
|
|
3363
|
+
command,
|
|
3364
|
+
roles,
|
|
3365
|
+
using,
|
|
3366
|
+
check,
|
|
3367
|
+
location: { file: rel, line: stmt.line }
|
|
3368
|
+
});
|
|
3369
|
+
}
|
|
3370
|
+
function dropPolicy(state, stmt) {
|
|
3371
|
+
const dp = DROP_POLICY.exec(stmt.text);
|
|
3372
|
+
if (!dp?.[1] || !dp[4]) return false;
|
|
3373
|
+
const name = dp[2] ?? dp[1].replace(/^"|"$/g, "");
|
|
3374
|
+
const t = state.tables.get(qualifiedKey({ schema: dp[3] ?? null, name: dp[4] }));
|
|
3375
|
+
if (t) {
|
|
3376
|
+
t.policies = t.policies.filter((p) => p !== name);
|
|
3377
|
+
t.policyDetails = t.policyDetails.filter((p) => p.name !== name);
|
|
3378
|
+
}
|
|
3379
|
+
return true;
|
|
3090
3380
|
}
|
|
3091
3381
|
function sqlSchemaFor(into) {
|
|
3092
3382
|
return finishSchema(schemaStateFor(into));
|
|
3093
3383
|
}
|
|
3094
3384
|
|
|
3095
|
-
// packages/parser/src/
|
|
3385
|
+
// packages/parser/src/role-gates.ts
|
|
3096
3386
|
import ts7 from "typescript";
|
|
3387
|
+
var ROLE_PROPERTY = /^(role|roles|app_metadata|is_?admin|is_?superuser|permissions?|claims?)$/i;
|
|
3388
|
+
var EMAIL_PROPERTY = /^email$/i;
|
|
3389
|
+
function propertyPath(e) {
|
|
3390
|
+
const u = unwrap(e);
|
|
3391
|
+
if (ts7.isIdentifier(u)) return [u.text];
|
|
3392
|
+
if (ts7.isPropertyAccessExpression(u)) {
|
|
3393
|
+
const base = propertyPath(u.expression);
|
|
3394
|
+
return base === null ? null : [...base, u.name.text];
|
|
3395
|
+
}
|
|
3396
|
+
return null;
|
|
3397
|
+
}
|
|
3398
|
+
function isLiteral(e) {
|
|
3399
|
+
const u = unwrap(e);
|
|
3400
|
+
return ts7.isStringLiteralLike(u) || ts7.isNumericLiteral(u) || ts7.isArrayLiteralExpression(u) || u.kind === ts7.SyntaxKind.TrueKeyword || u.kind === ts7.SyntaxKind.FalseKeyword || u.kind === ts7.SyntaxKind.NullKeyword;
|
|
3401
|
+
}
|
|
3402
|
+
function sessionClaim(e, sessionNames) {
|
|
3403
|
+
const path = propertyPath(e);
|
|
3404
|
+
const root = path?.[0];
|
|
3405
|
+
const last = path?.[path.length - 1];
|
|
3406
|
+
if (!path || path.length < 2 || root === void 0 || last === void 0) return null;
|
|
3407
|
+
if (!sessionNames.has(root)) return null;
|
|
3408
|
+
if (path.some((p) => p === "user_metadata")) return null;
|
|
3409
|
+
if (path.some((p) => ROLE_PROPERTY.test(p))) return { source: path.join("."), kind: "role" };
|
|
3410
|
+
if (EMAIL_PROPERTY.test(last)) return { source: path.join("."), kind: "email" };
|
|
3411
|
+
return null;
|
|
3412
|
+
}
|
|
3413
|
+
function claimIn(cond, sessionNames) {
|
|
3414
|
+
let found = null;
|
|
3415
|
+
const visit = (n) => {
|
|
3416
|
+
if (found !== null) return;
|
|
3417
|
+
if (ts7.isCallExpression(n)) {
|
|
3418
|
+
for (const a of n.arguments) {
|
|
3419
|
+
const c = sessionClaim(a, sessionNames);
|
|
3420
|
+
if (c) {
|
|
3421
|
+
found = c.source;
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
} else if (ts7.isBinaryExpression(n)) {
|
|
3426
|
+
for (const [a, b] of [
|
|
3427
|
+
[n.left, n.right],
|
|
3428
|
+
[n.right, n.left]
|
|
3429
|
+
]) {
|
|
3430
|
+
const c = sessionClaim(a, sessionNames);
|
|
3431
|
+
if (!c) continue;
|
|
3432
|
+
if (c.kind === "role" || isLiteral(b) || envNamesIn(b).length > 0) {
|
|
3433
|
+
found = c.source;
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
} else if (ts7.isPrefixUnaryExpression(n) || ts7.isPropertyAccessExpression(n)) {
|
|
3438
|
+
const target = ts7.isPrefixUnaryExpression(n) ? n.operand : n;
|
|
3439
|
+
const c = sessionClaim(target, sessionNames);
|
|
3440
|
+
if (c?.kind === "role" && !ts7.isCallExpression(n.parent)) {
|
|
3441
|
+
found = c.source;
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
n.forEachChild(visit);
|
|
3446
|
+
};
|
|
3447
|
+
visit(cond);
|
|
3448
|
+
return found;
|
|
3449
|
+
}
|
|
3450
|
+
function sessionNamesIn(body, isSessionCall) {
|
|
3451
|
+
const out = /* @__PURE__ */ new Set();
|
|
3452
|
+
walkOwn(body, (n) => {
|
|
3453
|
+
if (!ts7.isVariableDeclaration(n) || !n.initializer) return;
|
|
3454
|
+
const init = unwrap(n.initializer);
|
|
3455
|
+
if (ts7.isCallExpression(init) && isSessionCall(init)) {
|
|
3456
|
+
for (const nm of boundNames(n.name)) out.add(nm);
|
|
3457
|
+
}
|
|
3458
|
+
});
|
|
3459
|
+
return out;
|
|
3460
|
+
}
|
|
3461
|
+
function roleGatesIn(body, sessionNames) {
|
|
3462
|
+
const out = [];
|
|
3463
|
+
if (sessionNames.size === 0) return out;
|
|
3464
|
+
walkOwn(body, (n) => {
|
|
3465
|
+
if (!ts7.isIfStatement(n)) return;
|
|
3466
|
+
const exit = exitKind(n.thenStatement);
|
|
3467
|
+
if (!exit) return;
|
|
3468
|
+
const source = claimIn(n.expression, sessionNames);
|
|
3469
|
+
if (source !== null) out.push({ node: n, source, exit });
|
|
3470
|
+
});
|
|
3471
|
+
return out;
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
// packages/parser/src/storage.ts
|
|
3475
|
+
import ts8 from "typescript";
|
|
3097
3476
|
var STORAGE_OPS = {
|
|
3098
3477
|
download: "select",
|
|
3099
3478
|
list: "select",
|
|
@@ -3109,14 +3488,14 @@ var STORAGE_OPS = {
|
|
|
3109
3488
|
var TWO_PATHS = /* @__PURE__ */ new Set(["move", "copy"]);
|
|
3110
3489
|
function isStorageRoot(e) {
|
|
3111
3490
|
const u = unwrap(e);
|
|
3112
|
-
return
|
|
3491
|
+
return ts8.isPropertyAccessExpression(u) && u.name.text === "storage";
|
|
3113
3492
|
}
|
|
3114
3493
|
function storageBindingsIn(body) {
|
|
3115
3494
|
const out = /* @__PURE__ */ new Map();
|
|
3116
|
-
for (const decl of collect(body,
|
|
3117
|
-
if (!decl.initializer || !
|
|
3495
|
+
for (const decl of collect(body, ts8.isVariableDeclaration)) {
|
|
3496
|
+
if (!decl.initializer || !ts8.isIdentifier(decl.name)) continue;
|
|
3118
3497
|
const init = unwrap(decl.initializer);
|
|
3119
|
-
if (!
|
|
3498
|
+
if (!ts8.isCallExpression(init) || !ts8.isPropertyAccessExpression(init.expression)) continue;
|
|
3120
3499
|
const callee = init.expression;
|
|
3121
3500
|
if (callee.name.text !== "from" || !isStorageRoot(callee.expression)) continue;
|
|
3122
3501
|
const root = unwrap(callee.expression);
|
|
@@ -3132,7 +3511,7 @@ function storageCallOf(chain, bound) {
|
|
|
3132
3511
|
const r = unwrap(root);
|
|
3133
3512
|
return { clientRoot: r.expression, bucketArg: first.args[0], op: known(second) };
|
|
3134
3513
|
}
|
|
3135
|
-
if (
|
|
3514
|
+
if (ts8.isIdentifier(root) && first) {
|
|
3136
3515
|
const b = bound.get(root.text);
|
|
3137
3516
|
if (b) return { clientRoot: b.clientRoot, bucketArg: b.bucketArg, op: known(first) };
|
|
3138
3517
|
}
|
|
@@ -3143,9 +3522,9 @@ function bucketName(arg, sf) {
|
|
|
3143
3522
|
const u = unwrap(arg);
|
|
3144
3523
|
const lit = stringLiteralValue(u);
|
|
3145
3524
|
if (lit !== null) return lit;
|
|
3146
|
-
if (!
|
|
3147
|
-
for (const d of collect(sf,
|
|
3148
|
-
if (!
|
|
3525
|
+
if (!ts8.isIdentifier(u)) return null;
|
|
3526
|
+
for (const d of collect(sf, ts8.isVariableDeclaration)) {
|
|
3527
|
+
if (!ts8.isIdentifier(d.name) || d.name.text !== u.text || !d.initializer) continue;
|
|
3149
3528
|
const v = stringLiteralValue(unwrap(d.initializer));
|
|
3150
3529
|
if (v !== null) return v;
|
|
3151
3530
|
}
|
|
@@ -3158,20 +3537,20 @@ var INPUT_OBJECT = /^(params|searchParams|body|query|headers|cookies|formData)$/
|
|
|
3158
3537
|
var CallerScope = class {
|
|
3159
3538
|
constructor(body, inputNames) {
|
|
3160
3539
|
this.inputNames = inputNames;
|
|
3161
|
-
for (const decl of collect(body,
|
|
3162
|
-
if (!decl.initializer || !
|
|
3540
|
+
for (const decl of collect(body, ts8.isVariableDeclaration)) {
|
|
3541
|
+
if (!decl.initializer || !ts8.isIdentifier(decl.name)) continue;
|
|
3163
3542
|
if (this.refersToCaller(decl.initializer)) this.scopedVars.add(decl.name.text);
|
|
3164
3543
|
}
|
|
3165
|
-
for (const call of collect(body,
|
|
3544
|
+
for (const call of collect(body, ts8.isCallExpression)) {
|
|
3166
3545
|
const callee = call.expression;
|
|
3167
|
-
if (!
|
|
3546
|
+
if (!ts8.isPropertyAccessExpression(callee) || callee.name.text !== "startsWith") continue;
|
|
3168
3547
|
const target = unwrap(callee.expression);
|
|
3169
3548
|
const arg = call.arguments[0];
|
|
3170
|
-
if (
|
|
3549
|
+
if (ts8.isIdentifier(target) && arg && this.refersToCaller(arg)) this.guarded.add(target.text);
|
|
3171
3550
|
}
|
|
3172
|
-
for (const bin of collect(body,
|
|
3551
|
+
for (const bin of collect(body, ts8.isBinaryExpression)) {
|
|
3173
3552
|
const k = bin.operatorToken.kind;
|
|
3174
|
-
if (k !==
|
|
3553
|
+
if (k !== ts8.SyntaxKind.EqualsEqualsEqualsToken && k !== ts8.SyntaxKind.ExclamationEqualsEqualsToken && k !== ts8.SyntaxKind.EqualsEqualsToken && k !== ts8.SyntaxKind.ExclamationEqualsToken) {
|
|
3175
3554
|
continue;
|
|
3176
3555
|
}
|
|
3177
3556
|
for (const [side, other] of [
|
|
@@ -3191,13 +3570,13 @@ var CallerScope = class {
|
|
|
3191
3570
|
let hit = false;
|
|
3192
3571
|
walk(e, (n) => {
|
|
3193
3572
|
if (hit) return false;
|
|
3194
|
-
if (
|
|
3573
|
+
if (ts8.isPropertyAccessExpression(n) && this.isIdentityAccess(n)) {
|
|
3195
3574
|
hit = true;
|
|
3196
3575
|
return false;
|
|
3197
3576
|
}
|
|
3198
|
-
if (
|
|
3577
|
+
if (ts8.isIdentifier(n)) {
|
|
3199
3578
|
const parent = n.parent;
|
|
3200
|
-
if (parent &&
|
|
3579
|
+
if (parent && ts8.isPropertyAccessExpression(parent) && parent.name === n) return void 0;
|
|
3201
3580
|
if (this.scopedVars.has(n.text)) hit = true;
|
|
3202
3581
|
else if (IDENTITY_VAR.test(n.text) && !this.inputNames.has(n.text)) hit = true;
|
|
3203
3582
|
}
|
|
@@ -3208,17 +3587,17 @@ var CallerScope = class {
|
|
|
3208
3587
|
/** A path argument is covered when it embeds the caller's id or is an identifier checked against it. */
|
|
3209
3588
|
covers(e) {
|
|
3210
3589
|
const u = unwrap(e);
|
|
3211
|
-
return this.refersToCaller(u) ||
|
|
3590
|
+
return this.refersToCaller(u) || ts8.isIdentifier(u) && this.guarded.has(u.text);
|
|
3212
3591
|
}
|
|
3213
3592
|
isIdentityAccess(pa) {
|
|
3214
3593
|
const field = pa.name.text;
|
|
3215
3594
|
const names = [];
|
|
3216
3595
|
let base = unwrap(pa.expression);
|
|
3217
|
-
while (
|
|
3596
|
+
while (ts8.isPropertyAccessExpression(base)) {
|
|
3218
3597
|
names.push(base.name.text);
|
|
3219
3598
|
base = unwrap(base.expression);
|
|
3220
3599
|
}
|
|
3221
|
-
if (!
|
|
3600
|
+
if (!ts8.isIdentifier(base) || this.inputNames.has(base.text)) return false;
|
|
3222
3601
|
names.push(base.text);
|
|
3223
3602
|
if (names.some((n) => INPUT_OBJECT.test(n))) return false;
|
|
3224
3603
|
if (OWNER_FIELD.test(field)) return true;
|
|
@@ -3227,20 +3606,20 @@ var CallerScope = class {
|
|
|
3227
3606
|
};
|
|
3228
3607
|
function splitHead(e) {
|
|
3229
3608
|
const u = unwrap(e);
|
|
3230
|
-
if (!
|
|
3609
|
+
if (!ts8.isElementAccessExpression(u)) return null;
|
|
3231
3610
|
const idx = u.argumentExpression;
|
|
3232
|
-
if (!
|
|
3611
|
+
if (!ts8.isNumericLiteral(idx) || idx.text !== "0") return null;
|
|
3233
3612
|
const call = unwrap(u.expression);
|
|
3234
|
-
if (!
|
|
3613
|
+
if (!ts8.isCallExpression(call) || !ts8.isPropertyAccessExpression(call.expression)) return null;
|
|
3235
3614
|
if (call.expression.name.text !== "split") return null;
|
|
3236
3615
|
const target = unwrap(call.expression.expression);
|
|
3237
|
-
return
|
|
3616
|
+
return ts8.isIdentifier(target) ? target.text : null;
|
|
3238
3617
|
}
|
|
3239
3618
|
function storageAccessOf(op, bucket, sf, scope, derived) {
|
|
3240
3619
|
const pathArgs = op.args.slice(0, TWO_PATHS.has(op.name) ? 2 : 1);
|
|
3241
3620
|
const parts = pathArgs.flatMap((a) => {
|
|
3242
3621
|
const u = unwrap(a);
|
|
3243
|
-
return
|
|
3622
|
+
return ts8.isArrayLiteralExpression(u) ? [...u.elements] : [a];
|
|
3244
3623
|
});
|
|
3245
3624
|
const tainted = parts.filter((p) => derived(p));
|
|
3246
3625
|
const judged = tainted.length > 0 ? tainted : parts;
|
|
@@ -3254,22 +3633,22 @@ function storageAccessOf(op, bucket, sf, scope, derived) {
|
|
|
3254
3633
|
}
|
|
3255
3634
|
|
|
3256
3635
|
// packages/parser/src/supabase.ts
|
|
3257
|
-
import
|
|
3636
|
+
import ts9 from "typescript";
|
|
3258
3637
|
var CREATE_CLIENT_CALLEES = /^(createClient|createServerClient|createBrowserClient)$/;
|
|
3259
3638
|
function isCreateClientCall(call, sf) {
|
|
3260
3639
|
const callee = call.expression;
|
|
3261
|
-
if (
|
|
3262
|
-
if (
|
|
3640
|
+
if (ts9.isIdentifier(callee)) return CREATE_CLIENT_CALLEES.test(callee.text);
|
|
3641
|
+
if (ts9.isPropertyAccessExpression(callee)) return CREATE_CLIENT_CALLEES.test(callee.name.text);
|
|
3263
3642
|
return CREATE_CLIENT_CALLEES.test(callee.getText(sf));
|
|
3264
3643
|
}
|
|
3265
3644
|
function resolveArgText(expr, sf) {
|
|
3266
3645
|
const u = unwrap(expr);
|
|
3267
|
-
if (!
|
|
3646
|
+
if (!ts9.isIdentifier(u)) return expr.getText(sf);
|
|
3268
3647
|
let scope = expr.parent;
|
|
3269
3648
|
while (scope) {
|
|
3270
|
-
if (
|
|
3271
|
-
const decl = collect(scope,
|
|
3272
|
-
(d) =>
|
|
3649
|
+
if (ts9.isFunctionLike(scope) || ts9.isBlock(scope) || ts9.isSourceFile(scope)) {
|
|
3650
|
+
const decl = collect(scope, ts9.isVariableDeclaration).find(
|
|
3651
|
+
(d) => ts9.isIdentifier(d.name) && d.name.text === u.text && d.initializer
|
|
3273
3652
|
);
|
|
3274
3653
|
if (decl?.initializer) return `${u.text} = ${decl.initializer.getText(sf)}`;
|
|
3275
3654
|
}
|
|
@@ -3311,7 +3690,7 @@ function classifyCreateClientCall(call, sf) {
|
|
|
3311
3690
|
}
|
|
3312
3691
|
var AUTH_CALL = /\.auth\.(getUser|getSession|getClaims)\s*\(/;
|
|
3313
3692
|
function hasModifier(node, kind) {
|
|
3314
|
-
const mods =
|
|
3693
|
+
const mods = ts9.canHaveModifiers(node) ? ts9.getModifiers(node) : void 0;
|
|
3315
3694
|
return mods?.some((m) => m.kind === kind) ?? false;
|
|
3316
3695
|
}
|
|
3317
3696
|
function analyzeModule(rel, sf) {
|
|
@@ -3320,33 +3699,33 @@ function analyzeModule(rel, sf) {
|
|
|
3320
3699
|
const exportedNames = /* @__PURE__ */ new Set();
|
|
3321
3700
|
let defaultExport = null;
|
|
3322
3701
|
for (const stmt of sf.statements) {
|
|
3323
|
-
if (
|
|
3702
|
+
if (ts9.isImportDeclaration(stmt) && ts9.isStringLiteral(stmt.moduleSpecifier)) {
|
|
3324
3703
|
const spec = stmt.moduleSpecifier.text;
|
|
3325
3704
|
const clause = stmt.importClause;
|
|
3326
3705
|
if (!clause) continue;
|
|
3327
3706
|
if (clause.name) imports.set(clause.name.text, { spec, imported: "default" });
|
|
3328
3707
|
const nb = clause.namedBindings;
|
|
3329
|
-
if (nb &&
|
|
3708
|
+
if (nb && ts9.isNamedImports(nb)) {
|
|
3330
3709
|
for (const el of nb.elements) {
|
|
3331
3710
|
imports.set(el.name.text, { spec, imported: (el.propertyName ?? el.name).text });
|
|
3332
3711
|
}
|
|
3333
3712
|
}
|
|
3334
|
-
if (nb &&
|
|
3335
|
-
} else if (
|
|
3336
|
-
const spec = stmt.moduleSpecifier &&
|
|
3713
|
+
if (nb && ts9.isNamespaceImport(nb)) imports.set(nb.name.text, { spec, imported: "*" });
|
|
3714
|
+
} else if (ts9.isExportDeclaration(stmt)) {
|
|
3715
|
+
const spec = stmt.moduleSpecifier && ts9.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : null;
|
|
3337
3716
|
const clause = stmt.exportClause;
|
|
3338
3717
|
if (spec && !clause) reexports.push({ star: true, spec });
|
|
3339
|
-
else if (clause &&
|
|
3718
|
+
else if (clause && ts9.isNamedExports(clause)) {
|
|
3340
3719
|
for (const el of clause.elements) {
|
|
3341
3720
|
const name = (el.propertyName ?? el.name).text;
|
|
3342
3721
|
if (spec) reexports.push({ star: false, name, alias: el.name.text, spec });
|
|
3343
3722
|
else exportedNames.add(name);
|
|
3344
3723
|
}
|
|
3345
3724
|
}
|
|
3346
|
-
} else if (
|
|
3725
|
+
} else if (ts9.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
|
3347
3726
|
const e = unwrap(stmt.expression);
|
|
3348
|
-
if (
|
|
3349
|
-
} else if (
|
|
3727
|
+
if (ts9.isIdentifier(e)) defaultExport = e.text;
|
|
3728
|
+
} else if (ts9.isFunctionDeclaration(stmt) && stmt.name && hasModifier(stmt, ts9.SyntaxKind.DefaultKeyword)) {
|
|
3350
3729
|
defaultExport = stmt.name.text;
|
|
3351
3730
|
}
|
|
3352
3731
|
}
|
|
@@ -3361,15 +3740,15 @@ function analyzeModule(rel, sf) {
|
|
|
3361
3740
|
const moduleVars = /* @__PURE__ */ new Map();
|
|
3362
3741
|
const drizzleTables = /* @__PURE__ */ new Map();
|
|
3363
3742
|
for (const stmt of sf.statements) {
|
|
3364
|
-
if (!
|
|
3365
|
-
const exported = hasModifier(stmt,
|
|
3743
|
+
if (!ts9.isVariableStatement(stmt)) continue;
|
|
3744
|
+
const exported = hasModifier(stmt, ts9.SyntaxKind.ExportKeyword);
|
|
3366
3745
|
for (const d of stmt.declarationList.declarations) {
|
|
3367
|
-
if (!
|
|
3746
|
+
if (!ts9.isIdentifier(d.name) || !d.initializer || functions.has(d.name.text)) continue;
|
|
3368
3747
|
const init = clientCreatingOperand(d.initializer);
|
|
3369
|
-
if (!
|
|
3370
|
-
if (
|
|
3748
|
+
if (!ts9.isCallExpression(init) && !ts9.isNewExpression(init)) continue;
|
|
3749
|
+
if (ts9.isCallExpression(init)) {
|
|
3371
3750
|
const callee = init.expression;
|
|
3372
|
-
const fn =
|
|
3751
|
+
const fn = ts9.isIdentifier(callee) ? callee.text : ts9.isPropertyAccessExpression(callee) ? callee.name.text : "";
|
|
3373
3752
|
const tableName = stringLiteralValue(init.arguments[0]);
|
|
3374
3753
|
if ((DRIZZLE_TABLE_FNS.has(fn) || fn === "table") && tableName !== null) {
|
|
3375
3754
|
drizzleTables.set(d.name.text, tableName);
|
|
@@ -3392,8 +3771,8 @@ function analyzeModule(rel, sf) {
|
|
|
3392
3771
|
);
|
|
3393
3772
|
if (nextAuthLocal.size > 0) {
|
|
3394
3773
|
for (const stmt of sf.statements) {
|
|
3395
|
-
if (!
|
|
3396
|
-
const exported = hasModifier(stmt,
|
|
3774
|
+
if (!ts9.isVariableStatement(stmt)) continue;
|
|
3775
|
+
const exported = hasModifier(stmt, ts9.SyntaxKind.ExportKeyword);
|
|
3397
3776
|
for (const name of nextAuthSessionNames(stmt, nextAuthLocal)) {
|
|
3398
3777
|
const helper = {
|
|
3399
3778
|
name,
|
|
@@ -3416,7 +3795,7 @@ function analyzeModule(rel, sf) {
|
|
|
3416
3795
|
});
|
|
3417
3796
|
continue;
|
|
3418
3797
|
}
|
|
3419
|
-
const creates = collect(f.fn,
|
|
3798
|
+
const creates = collect(f.fn, ts9.isCallExpression).filter((c) => isCreateClientCall(c, sf));
|
|
3420
3799
|
const first = creates[0];
|
|
3421
3800
|
if (first) {
|
|
3422
3801
|
const { kind, evidence } = classifyCreateClientCall(first, sf);
|
|
@@ -3439,7 +3818,7 @@ function analyzeModule(rel, sf) {
|
|
|
3439
3818
|
}
|
|
3440
3819
|
|
|
3441
3820
|
// packages/parser/src/whole-input.ts
|
|
3442
|
-
import
|
|
3821
|
+
import ts10 from "typescript";
|
|
3443
3822
|
var ELEMENT_PRESERVING = /* @__PURE__ */ new Set([
|
|
3444
3823
|
"filter",
|
|
3445
3824
|
"slice",
|
|
@@ -3455,35 +3834,35 @@ var ELEMENT_PRESERVING = /* @__PURE__ */ new Set([
|
|
|
3455
3834
|
]);
|
|
3456
3835
|
var RESHAPING = /* @__PURE__ */ new Set(["map", "flatMap"]);
|
|
3457
3836
|
var LOGICAL = /* @__PURE__ */ new Set([
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3837
|
+
ts10.SyntaxKind.QuestionQuestionToken,
|
|
3838
|
+
ts10.SyntaxKind.BarBarToken,
|
|
3839
|
+
ts10.SyntaxKind.AmpersandAmpersandToken
|
|
3461
3840
|
]);
|
|
3462
3841
|
function isWholeInput(e, cx) {
|
|
3463
3842
|
const u = unwrap(e);
|
|
3464
|
-
if (
|
|
3465
|
-
if (
|
|
3843
|
+
if (ts10.isIdentifier(u)) return cx.wholeName(u.text);
|
|
3844
|
+
if (ts10.isPropertyAccessExpression(u) || ts10.isElementAccessExpression(u)) {
|
|
3466
3845
|
return isWholeInput(u.expression, cx);
|
|
3467
3846
|
}
|
|
3468
|
-
if (
|
|
3469
|
-
return u.properties.some((pr) =>
|
|
3847
|
+
if (ts10.isObjectLiteralExpression(u)) {
|
|
3848
|
+
return u.properties.some((pr) => ts10.isSpreadAssignment(pr) && isWholeInput(pr.expression, cx));
|
|
3470
3849
|
}
|
|
3471
|
-
if (
|
|
3472
|
-
return u.elements.some((el) => isWholeInput(
|
|
3850
|
+
if (ts10.isArrayLiteralExpression(u)) {
|
|
3851
|
+
return u.elements.some((el) => isWholeInput(ts10.isSpreadElement(el) ? el.expression : el, cx));
|
|
3473
3852
|
}
|
|
3474
|
-
if (
|
|
3853
|
+
if (ts10.isConditionalExpression(u)) {
|
|
3475
3854
|
return isWholeInput(u.whenTrue, cx) || isWholeInput(u.whenFalse, cx);
|
|
3476
3855
|
}
|
|
3477
|
-
if (
|
|
3856
|
+
if (ts10.isBinaryExpression(u) && LOGICAL.has(u.operatorToken.kind)) {
|
|
3478
3857
|
return isWholeInput(u.left, cx) || isWholeInput(u.right, cx);
|
|
3479
3858
|
}
|
|
3480
|
-
if (
|
|
3859
|
+
if (ts10.isCallExpression(u)) return isWholeCall(u, cx);
|
|
3481
3860
|
return false;
|
|
3482
3861
|
}
|
|
3483
3862
|
function isWholeCall(call, cx) {
|
|
3484
3863
|
if (cx.requestBody(call)) return true;
|
|
3485
3864
|
const callee = call.expression;
|
|
3486
|
-
if (
|
|
3865
|
+
if (ts10.isPropertyAccessExpression(callee)) {
|
|
3487
3866
|
const method = callee.name.text;
|
|
3488
3867
|
const receiverWhole = isWholeInput(callee.expression, cx);
|
|
3489
3868
|
if (RESHAPING.has(method)) return callbackKeepsWhole(call.arguments[0], receiverWhole, cx);
|
|
@@ -3491,13 +3870,13 @@ function isWholeCall(call, cx) {
|
|
|
3491
3870
|
}
|
|
3492
3871
|
return call.arguments.some((a) => {
|
|
3493
3872
|
const ua = unwrap(a);
|
|
3494
|
-
return isWholeInput(a, cx) ||
|
|
3873
|
+
return isWholeInput(a, cx) || ts10.isIdentifier(ua) && cx.requestName(ua.text);
|
|
3495
3874
|
});
|
|
3496
3875
|
}
|
|
3497
3876
|
function callbackKeepsWhole(cb, receiverWhole, cx) {
|
|
3498
3877
|
if (!cb) return false;
|
|
3499
3878
|
const f = unwrap(cb);
|
|
3500
|
-
if (!
|
|
3879
|
+
if (!ts10.isArrowFunction(f) && !ts10.isFunctionExpression(f)) return receiverWhole;
|
|
3501
3880
|
const element = f.parameters[0];
|
|
3502
3881
|
const elementNames = new Set(receiverWhole && element ? boundNames(element.name) : []);
|
|
3503
3882
|
const inner = {
|
|
@@ -3508,19 +3887,19 @@ function callbackKeepsWhole(cb, receiverWhole, cx) {
|
|
|
3508
3887
|
return ownReturns(f).some((r) => isWholeInput(r, inner));
|
|
3509
3888
|
}
|
|
3510
3889
|
function propertyKey(name) {
|
|
3511
|
-
if (
|
|
3890
|
+
if (ts10.isIdentifier(name) || ts10.isStringLiteral(name)) return name.text;
|
|
3512
3891
|
return null;
|
|
3513
3892
|
}
|
|
3514
3893
|
function wholeParamNames(param, arg, cx) {
|
|
3515
3894
|
if (!arg) return [];
|
|
3516
3895
|
const u = unwrap(arg);
|
|
3517
|
-
if (
|
|
3896
|
+
if (ts10.isObjectBindingPattern(param) && ts10.isObjectLiteralExpression(u) && !u.properties.some(ts10.isSpreadAssignment)) {
|
|
3518
3897
|
const out = [];
|
|
3519
3898
|
for (const el of param.elements) {
|
|
3520
3899
|
if (el.dotDotDotToken) continue;
|
|
3521
3900
|
const key = propertyKey(el.propertyName ?? el.name);
|
|
3522
3901
|
const prop = u.properties.find((pr) => pr.name && propertyKey(pr.name) === key);
|
|
3523
|
-
const value = prop &&
|
|
3902
|
+
const value = prop && ts10.isPropertyAssignment(prop) ? prop.initializer : prop && ts10.isShorthandPropertyAssignment(prop) ? prop.name : void 0;
|
|
3524
3903
|
if (value && isWholeInput(value, cx)) out.push(...boundNames(el.name));
|
|
3525
3904
|
}
|
|
3526
3905
|
return out;
|
|
@@ -3654,8 +4033,8 @@ function scopeOf(p, facts) {
|
|
|
3654
4033
|
return scope;
|
|
3655
4034
|
}
|
|
3656
4035
|
function symOfCallee(p, callee, scope) {
|
|
3657
|
-
if (
|
|
3658
|
-
if (
|
|
4036
|
+
if (ts11.isIdentifier(callee)) return scope.get(callee.text);
|
|
4037
|
+
if (ts11.isPropertyAccessExpression(callee) && ts11.isIdentifier(callee.expression)) {
|
|
3659
4038
|
const ns = scope.get(callee.expression.text);
|
|
3660
4039
|
if (ns?.kind === "namespace") return exportedSym(p, ns.facts, callee.name.text, 0) ?? void 0;
|
|
3661
4040
|
}
|
|
@@ -3663,8 +4042,8 @@ function symOfCallee(p, callee, scope) {
|
|
|
3663
4042
|
}
|
|
3664
4043
|
function returnedExpressions(fn) {
|
|
3665
4044
|
if (!fn.body) return [];
|
|
3666
|
-
if (!
|
|
3667
|
-
return collect(fn.body,
|
|
4045
|
+
if (!ts11.isBlock(fn.body)) return [fn.body];
|
|
4046
|
+
return collect(fn.body, ts11.isReturnStatement).map((r) => r.expression).filter((e) => e !== void 0);
|
|
3668
4047
|
}
|
|
3669
4048
|
function factoryOfFunction(p, sym, depth) {
|
|
3670
4049
|
const key = `${sym.facts.file}#${sym.name}`;
|
|
@@ -3676,7 +4055,7 @@ function factoryOfFunction(p, sym, depth) {
|
|
|
3676
4055
|
let found = null;
|
|
3677
4056
|
for (const ret of returnedExpressions(sym.fn)) {
|
|
3678
4057
|
const u = unwrap(ret);
|
|
3679
|
-
if (!
|
|
4058
|
+
if (!ts11.isCallExpression(u)) continue;
|
|
3680
4059
|
found = classifyCall(p, u, sf, scope, null, depth + 1);
|
|
3681
4060
|
if (found) break;
|
|
3682
4061
|
}
|
|
@@ -3713,7 +4092,7 @@ function classifyCall(p, call, sf, scope, frame, depth) {
|
|
|
3713
4092
|
}
|
|
3714
4093
|
function prismaExtensionBase(p, call, sf, scope, frame, depth) {
|
|
3715
4094
|
const callee = call.expression;
|
|
3716
|
-
if (!
|
|
4095
|
+
if (!ts11.isPropertyAccessExpression(callee) || callee.name.text !== "$extends") return null;
|
|
3717
4096
|
if (depth > 5) return null;
|
|
3718
4097
|
const base = unwrap(callee.expression);
|
|
3719
4098
|
if (isPrismaNew(base)) {
|
|
@@ -3723,8 +4102,8 @@ function prismaExtensionBase(p, call, sf, scope, frame, depth) {
|
|
|
3723
4102
|
location: { file: sf.fileName, line: lineOf(sf, call) }
|
|
3724
4103
|
};
|
|
3725
4104
|
}
|
|
3726
|
-
if (
|
|
3727
|
-
if (!
|
|
4105
|
+
if (ts11.isCallExpression(base)) return classifyCall(p, base, sf, scope, frame, depth + 1);
|
|
4106
|
+
if (!ts11.isIdentifier(base)) return null;
|
|
3728
4107
|
const bound = frame?.clients.get(base.text);
|
|
3729
4108
|
if (bound) return bound;
|
|
3730
4109
|
const sym = scope.get(base.text);
|
|
@@ -3734,15 +4113,15 @@ function instanceOfCall(p, call, frame, scope) {
|
|
|
3734
4113
|
const sym = symOfCallee(p, call.expression, scope);
|
|
3735
4114
|
if (sym?.kind !== "function") return null;
|
|
3736
4115
|
const fscope = scopeOf(p, sym.facts);
|
|
3737
|
-
const params = sym.fn.parameters.map((pp) =>
|
|
4116
|
+
const params = sym.fn.parameters.map((pp) => ts11.isIdentifier(pp.name) ? pp.name.text : null);
|
|
3738
4117
|
for (const ret of returnedExpressions(sym.fn)) {
|
|
3739
4118
|
const u = unwrap(ret);
|
|
3740
|
-
if (!
|
|
4119
|
+
if (!ts11.isNewExpression(u) || !ts11.isIdentifier(u.expression)) continue;
|
|
3741
4120
|
const cs = fscope.get(u.expression.text);
|
|
3742
4121
|
if (cs?.kind !== "class") continue;
|
|
3743
4122
|
const ctorArgs = (u.arguments ?? []).map((a) => {
|
|
3744
4123
|
const ua = unwrap(a);
|
|
3745
|
-
if (
|
|
4124
|
+
if (ts11.isIdentifier(ua)) {
|
|
3746
4125
|
const j = params.indexOf(ua.text);
|
|
3747
4126
|
if (j >= 0 && frame) return argBinding(p, call.arguments[j], frame);
|
|
3748
4127
|
}
|
|
@@ -3774,7 +4153,7 @@ function varBinding(p, sym) {
|
|
|
3774
4153
|
whole: false,
|
|
3775
4154
|
isRequest: false
|
|
3776
4155
|
};
|
|
3777
|
-
} else if (
|
|
4156
|
+
} else if (ts11.isCallExpression(init)) {
|
|
3778
4157
|
const found = classifyCall(p, init, sf, scope, null, 0);
|
|
3779
4158
|
const client = found ? {
|
|
3780
4159
|
kind: found.kind,
|
|
@@ -3783,7 +4162,7 @@ function varBinding(p, sym) {
|
|
|
3783
4162
|
} : null;
|
|
3784
4163
|
const instance = client ? null : instanceOfCall(p, init, null, scope);
|
|
3785
4164
|
out = { client, instance, tainted: false, whole: false, isRequest: false };
|
|
3786
|
-
} else if (
|
|
4165
|
+
} else if (ts11.isNewExpression(init) && ts11.isIdentifier(init.expression)) {
|
|
3787
4166
|
const cs = scope.get(init.expression.text);
|
|
3788
4167
|
if (cs?.kind === "class") {
|
|
3789
4168
|
out = {
|
|
@@ -3809,15 +4188,15 @@ function usesInput(frame, e) {
|
|
|
3809
4188
|
let hit = false;
|
|
3810
4189
|
walk(e, (n) => {
|
|
3811
4190
|
if (hit) return false;
|
|
3812
|
-
if (
|
|
4191
|
+
if (ts11.isCallExpression(n) && handsOverRequest(frame, n)) {
|
|
3813
4192
|
hit = true;
|
|
3814
4193
|
return false;
|
|
3815
4194
|
}
|
|
3816
|
-
if (!
|
|
4195
|
+
if (!ts11.isIdentifier(n)) return void 0;
|
|
3817
4196
|
const parent = n.parent;
|
|
3818
|
-
if (parent &&
|
|
3819
|
-
if (parent &&
|
|
3820
|
-
const member = parent &&
|
|
4197
|
+
if (parent && ts11.isPropertyAccessExpression(parent) && parent.name === n) return void 0;
|
|
4198
|
+
if (parent && ts11.isPropertyAssignment(parent) && parent.name === n) return void 0;
|
|
4199
|
+
const member = parent && ts11.isPropertyAccessExpression(parent) && parent.expression === n;
|
|
3821
4200
|
if (frame.inputNames.has(n.text)) hit = true;
|
|
3822
4201
|
else if (member && frame.reqNames.has(n.text) && REQUEST_MEMBER.test(parent.name.text)) {
|
|
3823
4202
|
hit = true;
|
|
@@ -3836,46 +4215,46 @@ function handsOverRequest(frame, call) {
|
|
|
3836
4215
|
if (IDENTITY_CALLEE.test(callee) || REQUEST_CLIENT_CALLEE.test(callee)) return false;
|
|
3837
4216
|
return call.arguments.some((a) => {
|
|
3838
4217
|
const u = unwrap(a);
|
|
3839
|
-
return
|
|
4218
|
+
return ts11.isIdentifier(u) && frame.reqNames.has(u.text);
|
|
3840
4219
|
});
|
|
3841
4220
|
}
|
|
3842
4221
|
function propertyKeyOf(name) {
|
|
3843
|
-
if (name && (
|
|
4222
|
+
if (name && (ts11.isIdentifier(name) || ts11.isStringLiteral(name))) return name.text;
|
|
3844
4223
|
return null;
|
|
3845
4224
|
}
|
|
3846
4225
|
function taintedProperties(frame, lit) {
|
|
3847
4226
|
const out = /* @__PURE__ */ new Set();
|
|
3848
4227
|
for (const pr of lit.properties) {
|
|
3849
|
-
if (
|
|
4228
|
+
if (ts11.isSpreadAssignment(pr)) {
|
|
3850
4229
|
if (derivedIn(frame, pr.expression)) return null;
|
|
3851
4230
|
continue;
|
|
3852
4231
|
}
|
|
3853
4232
|
const key = propertyKeyOf(pr.name);
|
|
3854
4233
|
if (key === null) continue;
|
|
3855
|
-
if (
|
|
3856
|
-
else if (
|
|
4234
|
+
if (ts11.isPropertyAssignment(pr) && derivedIn(frame, pr.initializer)) out.add(key);
|
|
4235
|
+
else if (ts11.isShorthandPropertyAssignment(pr) && derivedIn(frame, pr.name)) out.add(key);
|
|
3857
4236
|
}
|
|
3858
4237
|
return out;
|
|
3859
4238
|
}
|
|
3860
4239
|
function bindParamTaint(child, param, arg, frame) {
|
|
3861
4240
|
const u = arg ? unwrap(arg) : void 0;
|
|
3862
4241
|
let props = null;
|
|
3863
|
-
if (u &&
|
|
3864
|
-
else if (u &&
|
|
4242
|
+
if (u && ts11.isObjectLiteralExpression(u)) props = taintedProperties(frame, u);
|
|
4243
|
+
else if (u && ts11.isIdentifier(u) && !frame.inputNames.has(u.text)) {
|
|
3865
4244
|
props = frame.partialInputs.get(u.text) ?? null;
|
|
3866
4245
|
}
|
|
3867
4246
|
if (props === null) {
|
|
3868
4247
|
for (const nm of boundNames(param)) child.inputNames.add(nm);
|
|
3869
4248
|
return;
|
|
3870
4249
|
}
|
|
3871
|
-
if (
|
|
4250
|
+
if (ts11.isIdentifier(param)) {
|
|
3872
4251
|
child.partialInputs.set(param.text, new Set(props));
|
|
3873
4252
|
return;
|
|
3874
4253
|
}
|
|
3875
4254
|
bindPatternFrom(child, param, props);
|
|
3876
4255
|
}
|
|
3877
4256
|
function bindPatternFrom(child, pattern, props) {
|
|
3878
|
-
if (!
|
|
4257
|
+
if (!ts11.isObjectBindingPattern(pattern)) {
|
|
3879
4258
|
if (props.size > 0) for (const nm of boundNames(pattern)) child.inputNames.add(nm);
|
|
3880
4259
|
return;
|
|
3881
4260
|
}
|
|
@@ -3888,11 +4267,11 @@ function bindPatternFrom(child, pattern, props) {
|
|
|
3888
4267
|
}
|
|
3889
4268
|
function isRequestBodyCall(frame, call) {
|
|
3890
4269
|
const callee = call.expression;
|
|
3891
|
-
if (!
|
|
4270
|
+
if (!ts11.isPropertyAccessExpression(callee) || !/^(json|formData|text)$/.test(callee.name.text)) {
|
|
3892
4271
|
return false;
|
|
3893
4272
|
}
|
|
3894
4273
|
const recv = unwrap(callee.expression);
|
|
3895
|
-
if (!
|
|
4274
|
+
if (!ts11.isIdentifier(recv)) return false;
|
|
3896
4275
|
if (frame.reqNames.has(recv.text)) return true;
|
|
3897
4276
|
return frame.depth === 0 && frame.reqNames.size === 0 && REQUEST_NAME.test(recv.text);
|
|
3898
4277
|
}
|
|
@@ -3905,7 +4284,7 @@ function wholeContext(frame) {
|
|
|
3905
4284
|
}
|
|
3906
4285
|
function receiverTainted(frame, call) {
|
|
3907
4286
|
const callee = call.expression;
|
|
3908
|
-
if (!
|
|
4287
|
+
if (!ts11.isPropertyAccessExpression(callee) && !ts11.isElementAccessExpression(callee)) return false;
|
|
3909
4288
|
return derivedIn(frame, callee);
|
|
3910
4289
|
}
|
|
3911
4290
|
function argBinding(p, arg, frame) {
|
|
@@ -3915,7 +4294,7 @@ function argBinding(p, arg, frame) {
|
|
|
3915
4294
|
let client = null;
|
|
3916
4295
|
let instance = null;
|
|
3917
4296
|
let isRequest = false;
|
|
3918
|
-
if (
|
|
4297
|
+
if (ts11.isIdentifier(u)) {
|
|
3919
4298
|
client = frame.clients.get(u.text) ?? null;
|
|
3920
4299
|
instance = frame.instances.get(u.text) ?? null;
|
|
3921
4300
|
isRequest = frame.reqNames.has(u.text);
|
|
@@ -3927,14 +4306,14 @@ function argBinding(p, arg, frame) {
|
|
|
3927
4306
|
instance = vb.instance;
|
|
3928
4307
|
}
|
|
3929
4308
|
}
|
|
3930
|
-
} else if (
|
|
4309
|
+
} else if (ts11.isPropertyAccessExpression(u) && u.expression.kind === ts11.SyntaxKind.ThisKeyword) {
|
|
3931
4310
|
const tp = frame.thisProps.get(u.name.text);
|
|
3932
4311
|
if (tp) {
|
|
3933
4312
|
client = tp.client;
|
|
3934
4313
|
instance = tp.instance;
|
|
3935
4314
|
isRequest = tp.isRequest;
|
|
3936
4315
|
}
|
|
3937
|
-
} else if (
|
|
4316
|
+
} else if (ts11.isCallExpression(u)) {
|
|
3938
4317
|
client = classifyCall(p, u, frame.sf, scope, frame, 0);
|
|
3939
4318
|
if (!client) instance = instanceOfCall(p, u, frame, scope);
|
|
3940
4319
|
} else if (isPrismaNew(u)) {
|
|
@@ -3943,7 +4322,7 @@ function argBinding(p, arg, frame) {
|
|
|
3943
4322
|
name: "PrismaClient",
|
|
3944
4323
|
location: { file: frame.rel, line: lineOf(frame.sf, u) }
|
|
3945
4324
|
};
|
|
3946
|
-
} else if (
|
|
4325
|
+
} else if (ts11.isNewExpression(u) && ts11.isIdentifier(u.expression)) {
|
|
3947
4326
|
const cs = scope.get(u.expression.text);
|
|
3948
4327
|
if (cs?.kind === "class") {
|
|
3949
4328
|
instance = {
|
|
@@ -3971,7 +4350,7 @@ function methodTarget(inst, method) {
|
|
|
3971
4350
|
}
|
|
3972
4351
|
function callTarget(p, call, frame, scope) {
|
|
3973
4352
|
const callee = call.expression;
|
|
3974
|
-
if (
|
|
4353
|
+
if (ts11.isIdentifier(callee)) {
|
|
3975
4354
|
const sym = scope.get(callee.text);
|
|
3976
4355
|
if (sym?.kind === "function") {
|
|
3977
4356
|
return { fn: sym.fn, facts: sym.facts, name: sym.name, cls: null, thisProps: /* @__PURE__ */ new Map() };
|
|
@@ -3987,10 +4366,10 @@ function callTarget(p, call, frame, scope) {
|
|
|
3987
4366
|
}
|
|
3988
4367
|
return null;
|
|
3989
4368
|
}
|
|
3990
|
-
if (!
|
|
4369
|
+
if (!ts11.isPropertyAccessExpression(callee)) return null;
|
|
3991
4370
|
const obj = callee.expression;
|
|
3992
4371
|
const method = callee.name.text;
|
|
3993
|
-
if (
|
|
4372
|
+
if (ts11.isIdentifier(obj)) {
|
|
3994
4373
|
const inst = frame.instances.get(obj.text);
|
|
3995
4374
|
if (inst) return methodTarget(inst, method);
|
|
3996
4375
|
const sym = scope.get(obj.text);
|
|
@@ -4008,7 +4387,7 @@ function callTarget(p, call, frame, scope) {
|
|
|
4008
4387
|
}
|
|
4009
4388
|
return null;
|
|
4010
4389
|
}
|
|
4011
|
-
if (obj.kind ===
|
|
4390
|
+
if (obj.kind === ts11.SyntaxKind.ThisKeyword && frame.cls) {
|
|
4012
4391
|
const m = frame.cls.methods.get(method);
|
|
4013
4392
|
if (!m) return null;
|
|
4014
4393
|
return {
|
|
@@ -4019,13 +4398,13 @@ function callTarget(p, call, frame, scope) {
|
|
|
4019
4398
|
thisProps: frame.thisProps
|
|
4020
4399
|
};
|
|
4021
4400
|
}
|
|
4022
|
-
if (
|
|
4401
|
+
if (ts11.isPropertyAccessExpression(obj) && obj.expression.kind === ts11.SyntaxKind.ThisKeyword) {
|
|
4023
4402
|
const tp = frame.thisProps.get(obj.name.text);
|
|
4024
4403
|
if (tp?.instance) return methodTarget(tp.instance, method);
|
|
4025
4404
|
}
|
|
4026
4405
|
return null;
|
|
4027
4406
|
}
|
|
4028
|
-
function
|
|
4407
|
+
function bindDeclarations(p, frame, acc) {
|
|
4029
4408
|
const { rel, sf, fn } = frame;
|
|
4030
4409
|
const scope = scopeOf(p, frame.facts);
|
|
4031
4410
|
const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
|
|
@@ -4057,24 +4436,24 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4057
4436
|
for (const nm of names) addInput(kind, nm, decl, true);
|
|
4058
4437
|
bindInput(names, true);
|
|
4059
4438
|
};
|
|
4060
|
-
for (const decl of collect(body,
|
|
4439
|
+
for (const decl of collect(body, ts11.isVariableDeclaration)) {
|
|
4061
4440
|
if (!decl.initializer) continue;
|
|
4062
4441
|
const init = clientCreatingOperand(decl.initializer);
|
|
4063
4442
|
const names = boundNames(decl.name);
|
|
4064
4443
|
const text = init.getText(sf);
|
|
4065
4444
|
aliasLocal(frame, decl.name, init);
|
|
4066
|
-
const client =
|
|
4067
|
-
if (client &&
|
|
4445
|
+
const client = ts11.isCallExpression(init) ? classifyCall(p, init, sf, scope, frame, 0) : null;
|
|
4446
|
+
if (client && ts11.isIdentifier(decl.name)) {
|
|
4068
4447
|
frame.clients.set(decl.name.text, client);
|
|
4069
4448
|
continue;
|
|
4070
4449
|
}
|
|
4071
|
-
const rowsOfQuery =
|
|
4450
|
+
const rowsOfQuery = ts11.isCallExpression(init) && (isQueryChain(init) || isDbChain(init, frame));
|
|
4072
4451
|
if (frame.depth === 0 && !rowsOfQuery) {
|
|
4073
4452
|
if (/^(params|context\.params|ctx\.params|props\.params)$/.test(text)) {
|
|
4074
4453
|
handlerInput("route_param", names, decl);
|
|
4075
4454
|
continue;
|
|
4076
4455
|
}
|
|
4077
|
-
if (
|
|
4456
|
+
if (ts11.isCallExpression(init) && isRequestCall(text)) {
|
|
4078
4457
|
handlerInput("body", names, decl);
|
|
4079
4458
|
continue;
|
|
4080
4459
|
}
|
|
@@ -4087,9 +4466,9 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4087
4466
|
continue;
|
|
4088
4467
|
}
|
|
4089
4468
|
}
|
|
4090
|
-
if (
|
|
4469
|
+
if (ts11.isCallExpression(init)) {
|
|
4091
4470
|
const inst = instanceOfCall(p, init, frame, scope);
|
|
4092
|
-
if (inst &&
|
|
4471
|
+
if (inst && ts11.isIdentifier(decl.name)) {
|
|
4093
4472
|
frame.instances.set(decl.name.text, inst);
|
|
4094
4473
|
continue;
|
|
4095
4474
|
}
|
|
@@ -4097,34 +4476,41 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4097
4476
|
if (returnsIdentity(p, init, scope)) continue;
|
|
4098
4477
|
const args = init.arguments.map((a) => argBinding(p, a, frame));
|
|
4099
4478
|
if (args.some((a) => a.tainted || a.isRequest) || receiverTainted(frame, init)) {
|
|
4100
|
-
|
|
4479
|
+
const rt = returnTaint(p, init, frame, scope, 0);
|
|
4480
|
+
if (rt === null || rt.tainted && rt.props === null) {
|
|
4481
|
+
bindInput(names, isWholeInput(init, wholeContext(frame)));
|
|
4482
|
+
} else if (rt.tainted && rt.props !== null) {
|
|
4483
|
+
if (ts11.isIdentifier(decl.name))
|
|
4484
|
+
frame.partialInputs.set(decl.name.text, new Set(rt.props));
|
|
4485
|
+
else bindPatternFrom(frame, decl.name, rt.props);
|
|
4486
|
+
}
|
|
4101
4487
|
}
|
|
4102
|
-
} else if (isPrismaNew(init) &&
|
|
4488
|
+
} else if (isPrismaNew(init) && ts11.isIdentifier(decl.name)) {
|
|
4103
4489
|
frame.clients.set(decl.name.text, {
|
|
4104
4490
|
kind: "direct_db",
|
|
4105
4491
|
name: decl.name.text,
|
|
4106
4492
|
location: loc2(init)
|
|
4107
4493
|
});
|
|
4108
|
-
} else if (
|
|
4494
|
+
} else if (ts11.isNewExpression(init) && ts11.isIdentifier(init.expression) && scope.get(init.expression.text)?.kind === "class") {
|
|
4109
4495
|
const cs = scope.get(init.expression.text);
|
|
4110
|
-
if (cs?.kind === "class" &&
|
|
4496
|
+
if (cs?.kind === "class" && ts11.isIdentifier(decl.name)) {
|
|
4111
4497
|
frame.instances.set(decl.name.text, {
|
|
4112
4498
|
cls: cs.cls,
|
|
4113
4499
|
facts: cs.facts,
|
|
4114
4500
|
ctorArgs: (init.arguments ?? []).map((a) => argBinding(p, a, frame))
|
|
4115
4501
|
});
|
|
4116
4502
|
}
|
|
4117
|
-
} else if (
|
|
4503
|
+
} else if (ts11.isIdentifier(init)) {
|
|
4118
4504
|
const c = frame.clients.get(init.text);
|
|
4119
|
-
if (c &&
|
|
4505
|
+
if (c && ts11.isIdentifier(decl.name)) frame.clients.set(decl.name.text, c);
|
|
4120
4506
|
const i = frame.instances.get(init.text);
|
|
4121
|
-
if (i &&
|
|
4507
|
+
if (i && ts11.isIdentifier(decl.name)) frame.instances.set(decl.name.text, i);
|
|
4122
4508
|
if (frame.inputNames.has(init.text)) {
|
|
4123
4509
|
if (frame.depth === 0) for (const nm of names) addInput("body", nm, decl, true);
|
|
4124
4510
|
bindInput(names, frame.wholeNames.has(init.text));
|
|
4125
4511
|
} else {
|
|
4126
4512
|
const partial = frame.partialInputs.get(init.text);
|
|
4127
|
-
if (partial &&
|
|
4513
|
+
if (partial && ts11.isIdentifier(decl.name)) {
|
|
4128
4514
|
frame.partialInputs.set(decl.name.text, new Set(partial));
|
|
4129
4515
|
} else if (partial) bindPatternFrom(frame, decl.name, partial);
|
|
4130
4516
|
}
|
|
@@ -4134,34 +4520,47 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4134
4520
|
}
|
|
4135
4521
|
if (frame.depth === 0) {
|
|
4136
4522
|
walk(body, (n) => {
|
|
4137
|
-
if (
|
|
4523
|
+
if (ts11.isPropertyAccessExpression(n) && ts11.isIdentifier(n.expression) && n.expression.text === "params") {
|
|
4138
4524
|
addInput("route_param", n.name.text, n, false);
|
|
4139
4525
|
}
|
|
4140
4526
|
return void 0;
|
|
4141
4527
|
});
|
|
4142
4528
|
}
|
|
4143
|
-
|
|
4529
|
+
}
|
|
4530
|
+
function analyzeFrame(p, frame, acc) {
|
|
4531
|
+
const { rel, sf, fn } = frame;
|
|
4532
|
+
const scope = scopeOf(p, frame.facts);
|
|
4533
|
+
const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
|
|
4534
|
+
const body = fn.body ?? fn;
|
|
4535
|
+
bindDeclarations(p, frame, acc);
|
|
4536
|
+
for (const call of collect(body, ts11.isCallExpression)) {
|
|
4144
4537
|
const callee = call.expression;
|
|
4145
|
-
if (!
|
|
4538
|
+
if (!ts11.isPropertyAccessExpression(callee) || !/^\$?transaction$/.test(callee.name.text))
|
|
4146
4539
|
continue;
|
|
4147
|
-
if (!
|
|
4540
|
+
if (!ts11.isIdentifier(callee.expression)) continue;
|
|
4148
4541
|
const outer = frame.clients.get(callee.expression.text);
|
|
4149
|
-
const fn2 = call.arguments.map((a) => unwrap(a)).find((a) =>
|
|
4150
|
-
const param = fn2 && (
|
|
4151
|
-
if (outer && param &&
|
|
4152
|
-
}
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
if (
|
|
4156
|
-
|
|
4157
|
-
|
|
4542
|
+
const fn2 = call.arguments.map((a) => unwrap(a)).find((a) => ts11.isArrowFunction(a) || ts11.isFunctionExpression(a));
|
|
4543
|
+
const param = fn2 && (ts11.isArrowFunction(fn2) || ts11.isFunctionExpression(fn2)) ? fn2.parameters[0] : void 0;
|
|
4544
|
+
if (outer && param && ts11.isIdentifier(param.name)) frame.clients.set(param.name.text, outer);
|
|
4545
|
+
}
|
|
4546
|
+
const isSessionCall = (call) => /\.auth\.(getUser|getSession|getClaims)$/.test(call.expression.getText(sf)) || symOfCallee(p, call.expression, scope)?.kind === "auth";
|
|
4547
|
+
for (const call of collect(body, ts11.isCallExpression)) {
|
|
4548
|
+
if (isSessionCall(call)) acc.authChecks.push({ ...loc2(call), kind: "session" });
|
|
4549
|
+
}
|
|
4550
|
+
for (const gate of roleGatesIn(body, sessionNamesIn(body, isSessionCall))) {
|
|
4551
|
+
if (gate.exit === "return" && !frame.exitPropagates) continue;
|
|
4552
|
+
acc.roleChecks.push({
|
|
4553
|
+
...loc2(gate.node),
|
|
4554
|
+
source: gate.source,
|
|
4555
|
+
text: gate.node.expression.getText(sf).replace(/\s+/g, " ").slice(0, 160)
|
|
4556
|
+
});
|
|
4158
4557
|
}
|
|
4159
4558
|
for (const check of secretChecksIn(fn, sf)) {
|
|
4160
4559
|
acc.authChecks.push({ ...loc2(check.node), kind: "secret" });
|
|
4161
4560
|
}
|
|
4162
|
-
for (const pa of collect(body,
|
|
4561
|
+
for (const pa of collect(body, ts11.isPropertyAccessExpression)) {
|
|
4163
4562
|
const inner = pa.expression;
|
|
4164
|
-
if (
|
|
4563
|
+
if (ts11.isPropertyAccessExpression(inner) && (inner.name.text === "user_metadata" || inner.name.text === "app_metadata")) {
|
|
4165
4564
|
acc.metadataAccesses.push({
|
|
4166
4565
|
path: pa.getText(sf),
|
|
4167
4566
|
bucket: inner.name.text,
|
|
@@ -4173,15 +4572,15 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4173
4572
|
const seen = /* @__PURE__ */ new Set();
|
|
4174
4573
|
const clientOf = (root) => {
|
|
4175
4574
|
const u = unwrap(root);
|
|
4176
|
-
if (
|
|
4575
|
+
if (ts11.isIdentifier(u)) {
|
|
4177
4576
|
const b = frame.clients.get(u.text) ?? null;
|
|
4178
4577
|
return { binding: b, name: b ? null : u.text };
|
|
4179
4578
|
}
|
|
4180
|
-
if (
|
|
4579
|
+
if (ts11.isCallExpression(u)) {
|
|
4181
4580
|
const b = classifyCall(p, u, sf, scope, frame, 0);
|
|
4182
4581
|
return { binding: b, name: b ? null : u.expression.getText(sf) };
|
|
4183
4582
|
}
|
|
4184
|
-
if (
|
|
4583
|
+
if (ts11.isPropertyAccessExpression(u) && u.expression.kind === ts11.SyntaxKind.ThisKeyword) {
|
|
4185
4584
|
const b = frame.thisProps.get(u.name.text)?.client ?? null;
|
|
4186
4585
|
return { binding: b, name: b ? null : u.getText(sf) };
|
|
4187
4586
|
}
|
|
@@ -4190,11 +4589,11 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4190
4589
|
const tableSym = (e) => {
|
|
4191
4590
|
if (!e) return null;
|
|
4192
4591
|
const u = unwrap(e);
|
|
4193
|
-
if (
|
|
4592
|
+
if (ts11.isIdentifier(u)) {
|
|
4194
4593
|
const sym = scope.get(u.text);
|
|
4195
4594
|
return sym?.kind === "table" ? sym.table : null;
|
|
4196
4595
|
}
|
|
4197
|
-
if (
|
|
4596
|
+
if (ts11.isPropertyAccessExpression(u) && ts11.isIdentifier(u.expression)) {
|
|
4198
4597
|
const ns = scope.get(u.expression.text);
|
|
4199
4598
|
if (ns?.kind === "namespace") {
|
|
4200
4599
|
const sym = exportedSym(p, ns.facts, u.name.text, 0);
|
|
@@ -4251,7 +4650,46 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4251
4650
|
if (frame.via.length > 0) query.via = frame.via;
|
|
4252
4651
|
acc.queries.push(query);
|
|
4253
4652
|
};
|
|
4254
|
-
|
|
4653
|
+
const supabaseFilters = (segments) => {
|
|
4654
|
+
const filters = [];
|
|
4655
|
+
for (const s of segments) {
|
|
4656
|
+
if (!FILTER_METHODS.has(s.name)) continue;
|
|
4657
|
+
const firstArg = s.args[0];
|
|
4658
|
+
if (s.name === "match" && firstArg && ts11.isObjectLiteralExpression(firstArg)) {
|
|
4659
|
+
for (const pr of firstArg.properties) {
|
|
4660
|
+
if (ts11.isPropertyAssignment(pr)) {
|
|
4661
|
+
const f2 = {
|
|
4662
|
+
method: "match",
|
|
4663
|
+
column: pr.name.getText(sf).replace(/['"]/g, ""),
|
|
4664
|
+
valueText: pr.initializer.getText(sf),
|
|
4665
|
+
inputDerived: derivedIn(frame, pr.initializer)
|
|
4666
|
+
};
|
|
4667
|
+
filters.push(valued(f2, pr.initializer));
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
continue;
|
|
4671
|
+
}
|
|
4672
|
+
const val = s.args[1];
|
|
4673
|
+
const f = {
|
|
4674
|
+
method: s.name,
|
|
4675
|
+
column: stringLiteralValue(firstArg),
|
|
4676
|
+
valueText: val ? val.getText(sf) : "",
|
|
4677
|
+
inputDerived: val ? derivedIn(frame, val) : false
|
|
4678
|
+
};
|
|
4679
|
+
filters.push(valued(f, val));
|
|
4680
|
+
}
|
|
4681
|
+
return filters;
|
|
4682
|
+
};
|
|
4683
|
+
const builders = /* @__PURE__ */ new Map();
|
|
4684
|
+
const bindBuilder = (tail, query) => {
|
|
4685
|
+
const { node, parent } = outerOf(tail);
|
|
4686
|
+
if (parent && ts11.isVariableDeclaration(parent) && ts11.isIdentifier(parent.name)) {
|
|
4687
|
+
builders.set(parent.name.text, query);
|
|
4688
|
+
} else if (parent && ts11.isBinaryExpression(parent) && parent.operatorToken.kind === ts11.SyntaxKind.EqualsToken && parent.right === node && ts11.isIdentifier(parent.left)) {
|
|
4689
|
+
builders.set(parent.left.text, query);
|
|
4690
|
+
}
|
|
4691
|
+
};
|
|
4692
|
+
for (const call of collect(body, ts11.isCallExpression)) {
|
|
4255
4693
|
if (!isChainTail(call)) continue;
|
|
4256
4694
|
const chain = flattenChain(call);
|
|
4257
4695
|
const storageCall = storageCallOf(chain, storageHandles);
|
|
@@ -4260,6 +4698,18 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4260
4698
|
continue;
|
|
4261
4699
|
}
|
|
4262
4700
|
const root = unwrap(chain.root);
|
|
4701
|
+
const builder = ts11.isIdentifier(root) ? builders.get(root.text) : void 0;
|
|
4702
|
+
if (builder) {
|
|
4703
|
+
const condition = enclosingCondition(call, body);
|
|
4704
|
+
if (!condition || !derivedIn(frame, condition)) {
|
|
4705
|
+
const note = condition ? ` (when ${condition.getText(sf).replace(/\s+/g, " ")})` : "";
|
|
4706
|
+
for (const f of supabaseFilters(chain.segments)) {
|
|
4707
|
+
builder.filters.push(note ? { ...f, valueText: `${f.valueText}${note}` } : f);
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
bindBuilder(call, builder);
|
|
4711
|
+
continue;
|
|
4712
|
+
}
|
|
4263
4713
|
const first = chain.segments[0];
|
|
4264
4714
|
const fromIdx = chain.segments.findIndex((s) => s.name === "from");
|
|
4265
4715
|
const rpcIdx = chain.segments.findIndex((s) => s.name === "rpc");
|
|
@@ -4282,33 +4732,7 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4282
4732
|
break;
|
|
4283
4733
|
}
|
|
4284
4734
|
}
|
|
4285
|
-
const filters =
|
|
4286
|
-
for (const s of after) {
|
|
4287
|
-
if (!FILTER_METHODS.has(s.name)) continue;
|
|
4288
|
-
const firstArg = s.args[0];
|
|
4289
|
-
if (s.name === "match" && firstArg && ts10.isObjectLiteralExpression(firstArg)) {
|
|
4290
|
-
for (const pr of firstArg.properties) {
|
|
4291
|
-
if (ts10.isPropertyAssignment(pr)) {
|
|
4292
|
-
const f2 = {
|
|
4293
|
-
method: "match",
|
|
4294
|
-
column: pr.name.getText(sf).replace(/['"]/g, ""),
|
|
4295
|
-
valueText: pr.initializer.getText(sf),
|
|
4296
|
-
inputDerived: derivedIn(frame, pr.initializer)
|
|
4297
|
-
};
|
|
4298
|
-
filters.push(valued(f2, pr.initializer));
|
|
4299
|
-
}
|
|
4300
|
-
}
|
|
4301
|
-
continue;
|
|
4302
|
-
}
|
|
4303
|
-
const val = s.args[1];
|
|
4304
|
-
const f = {
|
|
4305
|
-
method: s.name,
|
|
4306
|
-
column: stringLiteralValue(firstArg),
|
|
4307
|
-
valueText: val ? val.getText(sf) : "",
|
|
4308
|
-
inputDerived: val ? derivedIn(frame, val) : false
|
|
4309
|
-
};
|
|
4310
|
-
filters.push(valued(f, val));
|
|
4311
|
-
}
|
|
4735
|
+
const filters = supabaseFilters(after);
|
|
4312
4736
|
parsed = {
|
|
4313
4737
|
anchor: anchor.node,
|
|
4314
4738
|
table: fromTable ?? stringLiteralValue(anchor.args[0]) ?? "(dynamic)",
|
|
@@ -4337,7 +4761,7 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4337
4761
|
payload: payloadOf(payloadSeg?.args[0]),
|
|
4338
4762
|
clientRoot: chain.root
|
|
4339
4763
|
};
|
|
4340
|
-
} else if (first && DRIZZLE_QUERY_API.has(first.name) &&
|
|
4764
|
+
} else if (first && DRIZZLE_QUERY_API.has(first.name) && ts11.isPropertyAccessExpression(root) && ts11.isPropertyAccessExpression(root.expression) && root.expression.name.text === "query") {
|
|
4341
4765
|
const key = root.name.text;
|
|
4342
4766
|
parsed = {
|
|
4343
4767
|
anchor: first.node,
|
|
@@ -4349,7 +4773,7 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4349
4773
|
payload: null,
|
|
4350
4774
|
clientRoot: root.expression.expression
|
|
4351
4775
|
};
|
|
4352
|
-
} else if (first && PRISMA_OPS[first.name] &&
|
|
4776
|
+
} else if (first && PRISMA_OPS[first.name] && ts11.isPropertyAccessExpression(root) && ts11.isIdentifier(root.expression) && clientOf(root.expression).binding?.kind === "direct_db") {
|
|
4353
4777
|
const accessor = root.name.text;
|
|
4354
4778
|
const arg = first.args[0];
|
|
4355
4779
|
parsed = {
|
|
@@ -4382,6 +4806,13 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4382
4806
|
call,
|
|
4383
4807
|
chain.segments.map((s) => s.name)
|
|
4384
4808
|
);
|
|
4809
|
+
const checks = rowComparisons(call).filter((c) => c.exit === "throw" || frame.exitPropagates).map((c) => ({
|
|
4810
|
+
method: "compare",
|
|
4811
|
+
column: c.column,
|
|
4812
|
+
valueText: c.value.getText(sf).replace(/\s+/g, " "),
|
|
4813
|
+
inputDerived: derivedIn(frame, c.value)
|
|
4814
|
+
}));
|
|
4815
|
+
if (checks.length > 0) query.ownerChecks = checks;
|
|
4385
4816
|
acc.reads.push({
|
|
4386
4817
|
query,
|
|
4387
4818
|
keys: query.filters.map((f) => {
|
|
@@ -4389,76 +4820,151 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4389
4820
|
return v ? valueKey(frame, v) : null;
|
|
4390
4821
|
}),
|
|
4391
4822
|
order: [...frame.pathPos, parsed.anchor.getStart(sf)],
|
|
4392
|
-
exits: rowExit === "throw" || rowExit === "return" && frame.exitPropagates
|
|
4823
|
+
exits: rowExit === "throw" || rowExit === "return" && frame.exitPropagates,
|
|
4824
|
+
checks
|
|
4393
4825
|
});
|
|
4826
|
+
bindBuilder(call, query);
|
|
4394
4827
|
}
|
|
4395
4828
|
if (query.filters.some((f) => f.inputDerived && isCredentialColumn(f.column))) {
|
|
4396
4829
|
acc.authChecks.push({ ...query.location, kind: "credential" });
|
|
4397
4830
|
}
|
|
4398
4831
|
}
|
|
4399
4832
|
if (frame.depth >= MAX_DEPTH) return;
|
|
4400
|
-
for (const call of collect(body,
|
|
4833
|
+
for (const call of collect(body, ts11.isCallExpression)) {
|
|
4401
4834
|
const target = callTarget(p, call, frame, scope);
|
|
4402
4835
|
if (!target) continue;
|
|
4403
|
-
const
|
|
4404
|
-
if (!
|
|
4405
|
-
const
|
|
4406
|
-
rel: target.facts.file,
|
|
4407
|
-
sf: tsf,
|
|
4408
|
-
facts: target.facts,
|
|
4409
|
-
fn: target.fn,
|
|
4410
|
-
depth: frame.depth + 1,
|
|
4411
|
-
via: [...frame.via, `${target.name} (${target.facts.file}:${lineOf(tsf, target.fn)})`],
|
|
4412
|
-
inputNames: /* @__PURE__ */ new Set(),
|
|
4413
|
-
wholeNames: /* @__PURE__ */ new Set(),
|
|
4414
|
-
partialInputs: /* @__PURE__ */ new Map(),
|
|
4415
|
-
reqNames: /* @__PURE__ */ new Set(),
|
|
4416
|
-
clients: /* @__PURE__ */ new Map(),
|
|
4417
|
-
instances: /* @__PURE__ */ new Map(),
|
|
4418
|
-
cls: target.cls,
|
|
4419
|
-
thisProps: target.thisProps,
|
|
4420
|
-
key: `${target.facts.file}#${target.name}@${call.getStart(frame.sf)}`,
|
|
4421
|
-
aliases: /* @__PURE__ */ new Map(),
|
|
4422
|
-
pathPos: [...frame.pathPos, call.getStart(frame.sf)],
|
|
4423
|
-
exitPropagates: frame.exitPropagates && callResultChecked(call)
|
|
4424
|
-
};
|
|
4425
|
-
const cx = wholeContext(frame);
|
|
4426
|
-
target.fn.parameters.forEach((param, i) => {
|
|
4427
|
-
const arg = call.arguments[i];
|
|
4428
|
-
const ab = argBinding(p, arg, frame);
|
|
4429
|
-
const names = boundNames(param.name);
|
|
4430
|
-
const head = names[0];
|
|
4431
|
-
if (ab.client && head !== void 0 && ts10.isIdentifier(param.name)) {
|
|
4432
|
-
child.clients.set(head, ab.client);
|
|
4433
|
-
}
|
|
4434
|
-
if (ab.instance && head !== void 0 && ts10.isIdentifier(param.name)) {
|
|
4435
|
-
child.instances.set(head, ab.instance);
|
|
4436
|
-
}
|
|
4437
|
-
for (const nm of names) if (ab.isRequest) child.reqNames.add(nm);
|
|
4438
|
-
if (ab.tainted) {
|
|
4439
|
-
bindParamTaint(child, param.name, arg, frame);
|
|
4440
|
-
for (const nm of wholeParamNames(param.name, arg, cx)) {
|
|
4441
|
-
child.inputNames.add(nm);
|
|
4442
|
-
child.wholeNames.add(nm);
|
|
4443
|
-
}
|
|
4444
|
-
aliasParam(child, param.name, arg, frame);
|
|
4445
|
-
}
|
|
4446
|
-
});
|
|
4447
|
-
const signature = [
|
|
4448
|
-
...[...child.clients].map(([n, c]) => `${n}=${c.kind}`),
|
|
4449
|
-
...[...child.inputNames].map((n) => `${n}!${child.wholeNames.has(n) ? "*" : ""}`),
|
|
4450
|
-
...[...child.partialInputs].map(([n, s]) => `${n}.{${[...s].sort().join("|")}}`),
|
|
4451
|
-
...[...child.reqNames].map((n) => `${n}?`),
|
|
4452
|
-
...[...child.thisProps].map(([n, b]) => `this.${n}=${b.client?.kind ?? "-"}`),
|
|
4453
|
-
...[...child.aliases].map(([n, k]) => `${n}~${k}`),
|
|
4454
|
-
`exit=${child.exitPropagates}`
|
|
4455
|
-
].sort();
|
|
4456
|
-
const key = `${target.facts.file}#${target.name}#${signature.join(",")}`;
|
|
4836
|
+
const child = childFrame(p, call, target, frame);
|
|
4837
|
+
if (!child) continue;
|
|
4838
|
+
const key = `${target.facts.file}#${target.name}#${frameSignature(child)}`;
|
|
4457
4839
|
if (acc.visited.has(key)) continue;
|
|
4458
4840
|
acc.visited.add(key);
|
|
4459
4841
|
analyzeFrame(p, child, acc);
|
|
4460
4842
|
}
|
|
4461
4843
|
}
|
|
4844
|
+
function childFrame(p, call, target, frame) {
|
|
4845
|
+
const tsf = p.sources.get(target.facts.file);
|
|
4846
|
+
if (!tsf) return null;
|
|
4847
|
+
const child = {
|
|
4848
|
+
rel: target.facts.file,
|
|
4849
|
+
sf: tsf,
|
|
4850
|
+
facts: target.facts,
|
|
4851
|
+
fn: target.fn,
|
|
4852
|
+
depth: frame.depth + 1,
|
|
4853
|
+
via: [...frame.via, `${target.name} (${target.facts.file}:${lineOf(tsf, target.fn)})`],
|
|
4854
|
+
inputNames: /* @__PURE__ */ new Set(),
|
|
4855
|
+
wholeNames: /* @__PURE__ */ new Set(),
|
|
4856
|
+
partialInputs: /* @__PURE__ */ new Map(),
|
|
4857
|
+
reqNames: /* @__PURE__ */ new Set(),
|
|
4858
|
+
clients: /* @__PURE__ */ new Map(),
|
|
4859
|
+
instances: /* @__PURE__ */ new Map(),
|
|
4860
|
+
cls: target.cls,
|
|
4861
|
+
thisProps: target.thisProps,
|
|
4862
|
+
key: `${target.facts.file}#${target.name}@${call.getStart(frame.sf)}`,
|
|
4863
|
+
aliases: /* @__PURE__ */ new Map(),
|
|
4864
|
+
pathPos: [...frame.pathPos, call.getStart(frame.sf)],
|
|
4865
|
+
exitPropagates: frame.exitPropagates && callResultChecked(call)
|
|
4866
|
+
};
|
|
4867
|
+
const cx = wholeContext(frame);
|
|
4868
|
+
target.fn.parameters.forEach((param, i) => {
|
|
4869
|
+
const arg = call.arguments[i];
|
|
4870
|
+
const ab = argBinding(p, arg, frame);
|
|
4871
|
+
const names = boundNames(param.name);
|
|
4872
|
+
const head = names[0];
|
|
4873
|
+
if (ab.client && head !== void 0 && ts11.isIdentifier(param.name)) {
|
|
4874
|
+
child.clients.set(head, ab.client);
|
|
4875
|
+
}
|
|
4876
|
+
if (ab.instance && head !== void 0 && ts11.isIdentifier(param.name)) {
|
|
4877
|
+
child.instances.set(head, ab.instance);
|
|
4878
|
+
}
|
|
4879
|
+
for (const nm of names) if (ab.isRequest) child.reqNames.add(nm);
|
|
4880
|
+
if (ab.tainted) {
|
|
4881
|
+
bindParamTaint(child, param.name, arg, frame);
|
|
4882
|
+
for (const nm of wholeParamNames(param.name, arg, cx)) {
|
|
4883
|
+
child.inputNames.add(nm);
|
|
4884
|
+
child.wholeNames.add(nm);
|
|
4885
|
+
}
|
|
4886
|
+
aliasParam(child, param.name, arg, frame);
|
|
4887
|
+
}
|
|
4888
|
+
});
|
|
4889
|
+
return child;
|
|
4890
|
+
}
|
|
4891
|
+
function frameSignature(child) {
|
|
4892
|
+
return [
|
|
4893
|
+
...[...child.clients].map(([n, c]) => `${n}=${c.kind}`),
|
|
4894
|
+
...[...child.inputNames].map((n) => `${n}!${child.wholeNames.has(n) ? "*" : ""}`),
|
|
4895
|
+
...[...child.partialInputs].map(([n, s]) => `${n}.{${[...s].sort().join("|")}}`),
|
|
4896
|
+
...[...child.reqNames].map((n) => `${n}?`),
|
|
4897
|
+
...[...child.thisProps].map(([n, b]) => `this.${n}=${b.client?.kind ?? "-"}`),
|
|
4898
|
+
...[...child.aliases].map(([n, k]) => `${n}~${k}`),
|
|
4899
|
+
`exit=${child.exitPropagates}`
|
|
4900
|
+
].sort().join(",");
|
|
4901
|
+
}
|
|
4902
|
+
var CLEAN = { tainted: false, props: null };
|
|
4903
|
+
var TAINTED = { tainted: true, props: null };
|
|
4904
|
+
var MAX_RETURN_DEPTH = 2;
|
|
4905
|
+
function branchesOf(e) {
|
|
4906
|
+
const u = unwrap(e);
|
|
4907
|
+
if (ts11.isConditionalExpression(u)) return [...branchesOf(u.whenTrue), ...branchesOf(u.whenFalse)];
|
|
4908
|
+
if (ts11.isBinaryExpression(u) && (u.operatorToken.kind === ts11.SyntaxKind.QuestionQuestionToken || u.operatorToken.kind === ts11.SyntaxKind.BarBarToken || u.operatorToken.kind === ts11.SyntaxKind.AmpersandAmpersandToken)) {
|
|
4909
|
+
return [...branchesOf(u.left), ...branchesOf(u.right)];
|
|
4910
|
+
}
|
|
4911
|
+
return [u];
|
|
4912
|
+
}
|
|
4913
|
+
function isLiteralValue(e) {
|
|
4914
|
+
return ts11.isStringLiteralLike(e) || ts11.isNumericLiteral(e) || e.kind === ts11.SyntaxKind.NullKeyword || e.kind === ts11.SyntaxKind.TrueKeyword || e.kind === ts11.SyntaxKind.FalseKeyword || ts11.isIdentifier(e) && e.text === "undefined";
|
|
4915
|
+
}
|
|
4916
|
+
function mergeTaint(a, b) {
|
|
4917
|
+
if (!a.tainted) return b;
|
|
4918
|
+
if (!b.tainted) return a;
|
|
4919
|
+
if (a.props === null || b.props === null) return TAINTED;
|
|
4920
|
+
return { tainted: true, props: /* @__PURE__ */ new Set([...a.props, ...b.props]) };
|
|
4921
|
+
}
|
|
4922
|
+
function leafTaint(p, leaf, child, scope, depth) {
|
|
4923
|
+
if (isLiteralValue(leaf)) return CLEAN;
|
|
4924
|
+
if (ts11.isObjectLiteralExpression(leaf)) {
|
|
4925
|
+
const props = taintedProperties(child, leaf);
|
|
4926
|
+
if (props === null) return TAINTED;
|
|
4927
|
+
return props.size === 0 ? CLEAN : { tainted: true, props };
|
|
4928
|
+
}
|
|
4929
|
+
if (ts11.isCallExpression(leaf)) {
|
|
4930
|
+
if (isChainWithQuery(leaf) || isDbChain(leaf, child)) return CLEAN;
|
|
4931
|
+
if (returnsIdentity(p, leaf, scope)) return CLEAN;
|
|
4932
|
+
const nested = returnTaint(p, leaf, child, scope, depth + 1);
|
|
4933
|
+
if (nested !== null) return nested;
|
|
4934
|
+
}
|
|
4935
|
+
return derivedIn(child, leaf) ? TAINTED : CLEAN;
|
|
4936
|
+
}
|
|
4937
|
+
function returnTaint(p, call, frame, scope, depth) {
|
|
4938
|
+
if (depth > MAX_RETURN_DEPTH) return null;
|
|
4939
|
+
const target = callTarget(p, call, frame, scope);
|
|
4940
|
+
if (!target) return null;
|
|
4941
|
+
const child = childFrame(p, call, target, frame);
|
|
4942
|
+
if (!child) return null;
|
|
4943
|
+
const key = `${target.facts.file}#${target.name}#${frameSignature(child)}`;
|
|
4944
|
+
const cached = p.returnTaints.get(key);
|
|
4945
|
+
if (cached !== void 0) return cached;
|
|
4946
|
+
p.returnTaints.set(key, null);
|
|
4947
|
+
const scratch = {
|
|
4948
|
+
inputs: [],
|
|
4949
|
+
authChecks: [],
|
|
4950
|
+
roleChecks: [],
|
|
4951
|
+
queries: [],
|
|
4952
|
+
metadataAccesses: [],
|
|
4953
|
+
visited: /* @__PURE__ */ new Set(),
|
|
4954
|
+
reads: []
|
|
4955
|
+
};
|
|
4956
|
+
bindDeclarations(p, child, scratch);
|
|
4957
|
+
const childScope = scopeOf(p, child.facts);
|
|
4958
|
+
let out = CLEAN;
|
|
4959
|
+
for (const ret of ownReturns(target.fn)) {
|
|
4960
|
+
for (const leaf of branchesOf(ret)) {
|
|
4961
|
+
out = mergeTaint(out, leafTaint(p, leaf, child, childScope, depth));
|
|
4962
|
+
if (out.tainted && out.props === null) break;
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
p.returnTaints.set(key, out);
|
|
4966
|
+
return out;
|
|
4967
|
+
}
|
|
4462
4968
|
function pushQuery(acc, q) {
|
|
4463
4969
|
const same = acc.queries.find(
|
|
4464
4970
|
(x) => x.location.file === q.location.file && x.location.line === q.location.line && x.text === q.text && x.client === q.client && x.clientName === q.clientName && x.filters.length === q.filters.length
|
|
@@ -4479,8 +4985,8 @@ function pushQuery(acc, q) {
|
|
|
4479
4985
|
}
|
|
4480
4986
|
function dottedPath(e) {
|
|
4481
4987
|
const u = unwrap(e);
|
|
4482
|
-
if (
|
|
4483
|
-
if (
|
|
4988
|
+
if (ts11.isIdentifier(u)) return u.text;
|
|
4989
|
+
if (ts11.isPropertyAccessExpression(u)) {
|
|
4484
4990
|
const base = dottedPath(u.expression);
|
|
4485
4991
|
return base === null ? null : `${base}.${u.name.text}`;
|
|
4486
4992
|
}
|
|
@@ -4499,14 +5005,14 @@ function valueKey(frame, e) {
|
|
|
4499
5005
|
function aliasLocal(frame, name, init) {
|
|
4500
5006
|
const key = valueKey(frame, init);
|
|
4501
5007
|
if (key === null) return;
|
|
4502
|
-
if (
|
|
5008
|
+
if (ts11.isIdentifier(name)) {
|
|
4503
5009
|
frame.aliases.set(name.text, key);
|
|
4504
5010
|
return;
|
|
4505
5011
|
}
|
|
4506
|
-
if (!
|
|
5012
|
+
if (!ts11.isObjectBindingPattern(name)) return;
|
|
4507
5013
|
for (const el of name.elements) {
|
|
4508
5014
|
const k = propertyKeyOf(el.propertyName ?? el.name);
|
|
4509
|
-
if (k !== null && !el.dotDotDotToken &&
|
|
5015
|
+
if (k !== null && !el.dotDotDotToken && ts11.isIdentifier(el.name)) {
|
|
4510
5016
|
frame.aliases.set(el.name.text, `${key}.${k}`);
|
|
4511
5017
|
}
|
|
4512
5018
|
}
|
|
@@ -4514,51 +5020,102 @@ function aliasLocal(frame, name, init) {
|
|
|
4514
5020
|
function aliasParam(child, param, arg, frame) {
|
|
4515
5021
|
if (!arg) return;
|
|
4516
5022
|
const u = unwrap(arg);
|
|
4517
|
-
if (
|
|
5023
|
+
if (ts11.isIdentifier(param)) {
|
|
4518
5024
|
const key = valueKey(frame, arg);
|
|
4519
5025
|
if (key !== null) child.aliases.set(param.text, key);
|
|
4520
|
-
else if (
|
|
5026
|
+
else if (ts11.isObjectLiteralExpression(u)) {
|
|
4521
5027
|
for (const pr of u.properties) {
|
|
4522
5028
|
const k = propertyKeyOf(pr.name);
|
|
4523
|
-
const v =
|
|
5029
|
+
const v = ts11.isPropertyAssignment(pr) ? pr.initializer : ts11.isShorthandPropertyAssignment(pr) ? pr.name : void 0;
|
|
4524
5030
|
const vk = k !== null && v ? valueKey(frame, v) : null;
|
|
4525
5031
|
if (k !== null && vk !== null) child.aliases.set(`${param.text}.${k}`, vk);
|
|
4526
5032
|
}
|
|
4527
5033
|
}
|
|
4528
5034
|
return;
|
|
4529
5035
|
}
|
|
4530
|
-
if (!
|
|
5036
|
+
if (!ts11.isObjectBindingPattern(param)) return;
|
|
4531
5037
|
const base = valueKey(frame, arg);
|
|
4532
5038
|
for (const el of param.elements) {
|
|
4533
5039
|
const k = propertyKeyOf(el.propertyName ?? el.name);
|
|
4534
|
-
if (k === null || el.dotDotDotToken || !
|
|
5040
|
+
if (k === null || el.dotDotDotToken || !ts11.isIdentifier(el.name)) continue;
|
|
4535
5041
|
if (base !== null) {
|
|
4536
5042
|
child.aliases.set(el.name.text, `${base}.${k}`);
|
|
4537
5043
|
continue;
|
|
4538
5044
|
}
|
|
4539
|
-
if (!
|
|
5045
|
+
if (!ts11.isObjectLiteralExpression(u)) continue;
|
|
4540
5046
|
const pr = u.properties.find((x) => propertyKeyOf(x.name) === k);
|
|
4541
|
-
const v = pr &&
|
|
5047
|
+
const v = pr && ts11.isPropertyAssignment(pr) ? pr.initializer : pr && ts11.isShorthandPropertyAssignment(pr) ? pr.name : void 0;
|
|
4542
5048
|
const vk = v ? valueKey(frame, v) : null;
|
|
4543
5049
|
if (vk !== null) child.aliases.set(el.name.text, vk);
|
|
4544
5050
|
}
|
|
4545
5051
|
}
|
|
4546
|
-
function
|
|
4547
|
-
|
|
5052
|
+
function enclosingCondition(node, body) {
|
|
5053
|
+
let child = node;
|
|
5054
|
+
let cur = node.parent;
|
|
5055
|
+
while (cur && cur !== body && !isFunctionLikeNode(cur)) {
|
|
5056
|
+
if (ts11.isIfStatement(cur) && cur.expression !== child) return cur.expression;
|
|
5057
|
+
child = cur;
|
|
5058
|
+
cur = cur.parent;
|
|
5059
|
+
}
|
|
5060
|
+
return null;
|
|
5061
|
+
}
|
|
5062
|
+
var normalizeColumn = (c) => (c ?? "").toLowerCase().replace(/_/g, "");
|
|
5063
|
+
function singular(table) {
|
|
5064
|
+
if (table.endsWith("ies")) return `${table.slice(0, -3)}y`;
|
|
5065
|
+
if (table.endsWith("ses") || table.endsWith("xes")) return table.slice(0, -2);
|
|
5066
|
+
return table.endsWith("s") ? table.slice(0, -1) : table;
|
|
5067
|
+
}
|
|
5068
|
+
function columnNamesTable(column, table) {
|
|
5069
|
+
const c = normalizeColumn(column);
|
|
5070
|
+
if (!c.endsWith("id") || c === "id") return false;
|
|
5071
|
+
const stem = c.slice(0, -2);
|
|
5072
|
+
const t = normalizeColumn(table);
|
|
5073
|
+
return stem === t || stem === singular(t);
|
|
5074
|
+
}
|
|
5075
|
+
function guardMatch(q, g, tables) {
|
|
5076
|
+
const qTable = q.query.table.toLowerCase();
|
|
5077
|
+
const gTable = g.query.table.toLowerCase();
|
|
5078
|
+
const info = tables.get(qTable);
|
|
5079
|
+
for (let i = 0; i < q.query.filters.length; i += 1) {
|
|
5080
|
+
const f = q.query.filters[i];
|
|
5081
|
+
const k = q.keys[i];
|
|
5082
|
+
if (!f?.inputDerived || !f.column || k === null || k === void 0) continue;
|
|
5083
|
+
const j = g.keys.indexOf(k);
|
|
5084
|
+
const gf = j >= 0 ? g.query.filters[j] : void 0;
|
|
5085
|
+
if (!gf) continue;
|
|
5086
|
+
if (qTable === gTable) {
|
|
5087
|
+
if (normalizeColumn(gf.column) === normalizeColumn(f.column))
|
|
5088
|
+
return { read: g, column: f.column };
|
|
5089
|
+
continue;
|
|
5090
|
+
}
|
|
5091
|
+
const ref = info?.columnInfo?.find((c) => c.name === f.column?.toLowerCase())?.references;
|
|
5092
|
+
if (ref && ref.table === gTable && normalizeColumn(ref.column) === normalizeColumn(gf.column)) {
|
|
5093
|
+
return {
|
|
5094
|
+
read: g,
|
|
5095
|
+
column: f.column,
|
|
5096
|
+
parent: { table: g.query.table, column: gf.column ?? "id", how: "foreign key" }
|
|
5097
|
+
};
|
|
5098
|
+
}
|
|
5099
|
+
if (normalizeColumn(gf.column) === "id" && columnNamesTable(f.column, gTable)) {
|
|
5100
|
+
return {
|
|
5101
|
+
read: g,
|
|
5102
|
+
column: f.column,
|
|
5103
|
+
parent: { table: g.query.table, column: gf.column ?? "id", how: "column name" }
|
|
5104
|
+
};
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
5107
|
+
return null;
|
|
5108
|
+
}
|
|
5109
|
+
function linkGuards(reads, tables) {
|
|
4548
5110
|
for (const q of reads) {
|
|
4549
5111
|
let best = null;
|
|
4550
5112
|
for (const g2 of reads) {
|
|
4551
5113
|
if (g2 === q || !["select", "unknown"].includes(g2.query.operation)) continue;
|
|
4552
|
-
if (g2.query.table.toLowerCase() !== q.query.table.toLowerCase()) continue;
|
|
4553
5114
|
if (compareOrder(g2.order, q.order) >= 0) continue;
|
|
4554
|
-
const
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
});
|
|
4559
|
-
const column = q.query.filters[shared]?.column;
|
|
4560
|
-
if (shared < 0 || !column) continue;
|
|
4561
|
-
if (!best || g2.exits && !best.read.exits) best = { read: g2, column };
|
|
5115
|
+
const m = guardMatch(q, g2, tables);
|
|
5116
|
+
if (!m) continue;
|
|
5117
|
+
const stops = (r) => r.exits || r.checks.length > 0;
|
|
5118
|
+
if (!best || stops(m.read) && !stops(best.read)) best = m;
|
|
4562
5119
|
}
|
|
4563
5120
|
if (!best) continue;
|
|
4564
5121
|
const g = best.read.query;
|
|
@@ -4569,20 +5126,23 @@ function linkGuards(reads) {
|
|
|
4569
5126
|
clientName: g.clientName,
|
|
4570
5127
|
filters: g.filters,
|
|
4571
5128
|
column: best.column,
|
|
4572
|
-
|
|
5129
|
+
// A row that fails the comparison stops the entry point too; a missing one fails it as well.
|
|
5130
|
+
exitsWhenMissing: best.read.exits || best.read.checks.length > 0,
|
|
4573
5131
|
text: g.text,
|
|
4574
|
-
...g.via ? { via: g.via } : {}
|
|
5132
|
+
...g.via ? { via: g.via } : {},
|
|
5133
|
+
...best.read.checks.length > 0 ? { checks: best.read.checks } : {},
|
|
5134
|
+
...best.parent ? { parent: best.parent } : {}
|
|
4575
5135
|
};
|
|
4576
5136
|
}
|
|
4577
5137
|
}
|
|
4578
5138
|
function isChainWithQuery(e) {
|
|
4579
|
-
return
|
|
5139
|
+
return ts11.isCallExpression(e) && isQueryChain(e);
|
|
4580
5140
|
}
|
|
4581
5141
|
function isDbChain(call, frame) {
|
|
4582
5142
|
const root = unwrap(flattenChain(call).root);
|
|
4583
5143
|
let base = root;
|
|
4584
|
-
while (
|
|
4585
|
-
return
|
|
5144
|
+
while (ts11.isPropertyAccessExpression(base)) base = base.expression;
|
|
5145
|
+
return ts11.isIdentifier(base) && frame.clients.has(base.text);
|
|
4586
5146
|
}
|
|
4587
5147
|
var IDENTITY_CALLEE = /\.auth\.|user|session|claims|auth|principal|viewer/i;
|
|
4588
5148
|
function returnsIdentity(p, call, scope) {
|
|
@@ -4591,10 +5151,10 @@ function returnsIdentity(p, call, scope) {
|
|
|
4591
5151
|
}
|
|
4592
5152
|
function calleePath(e) {
|
|
4593
5153
|
const u = unwrap(e);
|
|
4594
|
-
if (
|
|
4595
|
-
if (
|
|
4596
|
-
if (
|
|
4597
|
-
if (
|
|
5154
|
+
if (ts11.isIdentifier(u)) return u.text;
|
|
5155
|
+
if (ts11.isPropertyAccessExpression(u)) return `${calleePath(u.expression)}.${u.name.text}`;
|
|
5156
|
+
if (ts11.isCallExpression(u)) return `${calleePath(u.expression)}()`;
|
|
5157
|
+
if (ts11.isElementAccessExpression(u)) return `${calleePath(u.expression)}[]`;
|
|
4598
5158
|
return "";
|
|
4599
5159
|
}
|
|
4600
5160
|
function analyzeHandler(p, h) {
|
|
@@ -4603,6 +5163,7 @@ function analyzeHandler(p, h) {
|
|
|
4603
5163
|
const acc = {
|
|
4604
5164
|
inputs: [],
|
|
4605
5165
|
authChecks: [],
|
|
5166
|
+
roleChecks: [],
|
|
4606
5167
|
queries: [],
|
|
4607
5168
|
metadataAccesses: [],
|
|
4608
5169
|
visited: /* @__PURE__ */ new Set(),
|
|
@@ -4632,7 +5193,7 @@ function analyzeHandler(p, h) {
|
|
|
4632
5193
|
};
|
|
4633
5194
|
const first = fn.parameters[0];
|
|
4634
5195
|
if (h.kind === "route" && first) {
|
|
4635
|
-
if (
|
|
5196
|
+
if (ts11.isIdentifier(first.name)) frame.reqNames.add(first.name.text);
|
|
4636
5197
|
else for (const nm of boundNames(first.name)) if (REQUEST_NAME.test(nm)) frame.reqNames.add(nm);
|
|
4637
5198
|
}
|
|
4638
5199
|
if (h.kind === "server_action") {
|
|
@@ -4651,7 +5212,7 @@ function analyzeHandler(p, h) {
|
|
|
4651
5212
|
acc.authChecks.push({ ...loc2(h.node), kind: "session" });
|
|
4652
5213
|
}
|
|
4653
5214
|
analyzeFrame(p, frame, acc);
|
|
4654
|
-
linkGuards(acc.reads);
|
|
5215
|
+
linkGuards(acc.reads, p.tables);
|
|
4655
5216
|
const entry = h.kind === "route" ? `${h.method} ${h.route}` : h.kind === "page" ? `PAGE ${h.route}` : `server action ${h.route}`;
|
|
4656
5217
|
const stmt = enclosingStatement(h.node);
|
|
4657
5218
|
const ignores = parseIgnoreDirectives(sf, stmt.getFullStart()).map((d) => ({
|
|
@@ -4669,19 +5230,20 @@ function analyzeHandler(p, h) {
|
|
|
4669
5230
|
authChecks: acc.authChecks,
|
|
4670
5231
|
queries: acc.queries,
|
|
4671
5232
|
metadataAccesses: acc.metadataAccesses,
|
|
4672
|
-
ignores
|
|
5233
|
+
ignores,
|
|
5234
|
+
roleChecks: acc.roleChecks
|
|
4673
5235
|
};
|
|
4674
5236
|
}
|
|
4675
5237
|
function publicSecretEnvReads(sf) {
|
|
4676
|
-
const isProcessEnv = (e) =>
|
|
5238
|
+
const isProcessEnv = (e) => ts11.isPropertyAccessExpression(e) && e.name.text === "env" && ts11.isIdentifier(e.expression) && e.expression.text === "process";
|
|
4677
5239
|
const out = [];
|
|
4678
5240
|
const visit = (n) => {
|
|
4679
|
-
if (
|
|
5241
|
+
if (ts11.isPropertyAccessExpression(n) && isProcessEnv(n.expression)) {
|
|
4680
5242
|
if (PUBLIC_SECRET_ENV.test(n.name.text)) out.push({ name: n.name.text, node: n });
|
|
4681
|
-
} else if (
|
|
5243
|
+
} else if (ts11.isElementAccessExpression(n) && isProcessEnv(n.expression) && ts11.isStringLiteralLike(n.argumentExpression) && PUBLIC_SECRET_ENV.test(n.argumentExpression.text)) {
|
|
4682
5244
|
out.push({ name: n.argumentExpression.text, node: n });
|
|
4683
5245
|
}
|
|
4684
|
-
|
|
5246
|
+
ts11.forEachChild(n, visit);
|
|
4685
5247
|
};
|
|
4686
5248
|
visit(sf);
|
|
4687
5249
|
return out;
|
|
@@ -4696,7 +5258,7 @@ function findExposures(rel, sf) {
|
|
|
4696
5258
|
});
|
|
4697
5259
|
}
|
|
4698
5260
|
if (isClientComponentFile(sf)) {
|
|
4699
|
-
for (const call of collect(sf,
|
|
5261
|
+
for (const call of collect(sf, ts11.isCallExpression)) {
|
|
4700
5262
|
if (!isCreateClientCall(call, sf)) continue;
|
|
4701
5263
|
const c = classifyCreateClientCall(call, sf);
|
|
4702
5264
|
if (c.kind === "service_role") {
|
|
@@ -4727,7 +5289,14 @@ function parseProject(rootInput, opts = {}) {
|
|
|
4727
5289
|
}
|
|
4728
5290
|
}
|
|
4729
5291
|
const registry = /* @__PURE__ */ new Map();
|
|
4730
|
-
for (const [rel, sf] of sources)
|
|
5292
|
+
for (const [rel, sf] of sources) {
|
|
5293
|
+
try {
|
|
5294
|
+
registry.set(rel, analyzeModule(rel, sf));
|
|
5295
|
+
} catch (e) {
|
|
5296
|
+
sources.delete(rel);
|
|
5297
|
+
warnings.push(`could not analyse ${rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
4731
5300
|
const tables = /* @__PURE__ */ new Map();
|
|
4732
5301
|
for (const rel of sql) {
|
|
4733
5302
|
try {
|
|
@@ -4759,12 +5328,46 @@ function parseProject(rootInput, opts = {}) {
|
|
|
4759
5328
|
varBindings: /* @__PURE__ */ new Map(),
|
|
4760
5329
|
drizzleTablesByExport,
|
|
4761
5330
|
prismaModels,
|
|
5331
|
+
returnTaints: /* @__PURE__ */ new Map(),
|
|
5332
|
+
tables,
|
|
4762
5333
|
warnings
|
|
4763
5334
|
};
|
|
4764
5335
|
const routes = [];
|
|
4765
5336
|
const exposures = [];
|
|
4766
5337
|
const fileIgnores = {};
|
|
4767
5338
|
for (const [rel, sf] of sources) {
|
|
5339
|
+
try {
|
|
5340
|
+
analyzeFile(project, rel, sf, registry.get(rel) ?? analyzeModule(rel, sf), {
|
|
5341
|
+
routes,
|
|
5342
|
+
exposures,
|
|
5343
|
+
fileIgnores
|
|
5344
|
+
});
|
|
5345
|
+
} catch (e) {
|
|
5346
|
+
warnings.push(`could not analyse ${rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
routes.sort((a, b) => a.entry.localeCompare(b.entry));
|
|
5350
|
+
const all = [...registry.values()];
|
|
5351
|
+
const schema = sqlSchemaFor(tables);
|
|
5352
|
+
warnings.push(...schema.warnings);
|
|
5353
|
+
return {
|
|
5354
|
+
root,
|
|
5355
|
+
files: [...source, ...sql],
|
|
5356
|
+
routes,
|
|
5357
|
+
clientFactories: all.flatMap((f) => f.clientFactories),
|
|
5358
|
+
authHelpers: all.flatMap((f) => f.authHelpers),
|
|
5359
|
+
tables: [...tables.values()],
|
|
5360
|
+
exposures,
|
|
5361
|
+
fileIgnores,
|
|
5362
|
+
warnings,
|
|
5363
|
+
enums: schema.enums,
|
|
5364
|
+
sqlFunctions: schema.sqlFunctions,
|
|
5365
|
+
storageBuckets: schema.storageBuckets
|
|
5366
|
+
};
|
|
5367
|
+
}
|
|
5368
|
+
function analyzeFile(project, rel, sf, facts, out) {
|
|
5369
|
+
const { routes, exposures, fileIgnores } = out;
|
|
5370
|
+
{
|
|
4768
5371
|
const top = parseIgnoreDirectives(sf, 0).map((d) => ({
|
|
4769
5372
|
ruleId: d.ruleId,
|
|
4770
5373
|
reason: d.reason,
|
|
@@ -4772,7 +5375,6 @@ function parseProject(rootInput, opts = {}) {
|
|
|
4772
5375
|
}));
|
|
4773
5376
|
if (top.length > 0) fileIgnores[rel] = top;
|
|
4774
5377
|
exposures.push(...findExposures(rel, sf));
|
|
4775
|
-
const facts = registry.get(rel) ?? analyzeModule(rel, sf);
|
|
4776
5378
|
const route = routeFromFile(rel);
|
|
4777
5379
|
if (route) {
|
|
4778
5380
|
for (const { method, exported } of routeHandlersIn(sf)) {
|
|
@@ -4825,23 +5427,6 @@ function parseProject(rootInput, opts = {}) {
|
|
|
4825
5427
|
}
|
|
4826
5428
|
}
|
|
4827
5429
|
}
|
|
4828
|
-
routes.sort((a, b) => a.entry.localeCompare(b.entry));
|
|
4829
|
-
const all = [...registry.values()];
|
|
4830
|
-
const schema = sqlSchemaFor(tables);
|
|
4831
|
-
return {
|
|
4832
|
-
root,
|
|
4833
|
-
files: [...source, ...sql],
|
|
4834
|
-
routes,
|
|
4835
|
-
clientFactories: all.flatMap((f) => f.clientFactories),
|
|
4836
|
-
authHelpers: all.flatMap((f) => f.authHelpers),
|
|
4837
|
-
tables: [...tables.values()],
|
|
4838
|
-
exposures,
|
|
4839
|
-
fileIgnores,
|
|
4840
|
-
warnings,
|
|
4841
|
-
enums: schema.enums,
|
|
4842
|
-
sqlFunctions: schema.sqlFunctions,
|
|
4843
|
-
storageBuckets: schema.storageBuckets
|
|
4844
|
-
};
|
|
4845
5430
|
}
|
|
4846
5431
|
|
|
4847
5432
|
// packages/rules/src/packs/sql-functions.ts
|
|
@@ -4885,16 +5470,16 @@ var SCOPE_COLUMNS = /* @__PURE__ */ new Set([
|
|
|
4885
5470
|
"author_id",
|
|
4886
5471
|
"profile_id"
|
|
4887
5472
|
]);
|
|
4888
|
-
function
|
|
5473
|
+
function normalizeColumn2(column) {
|
|
4889
5474
|
return column.toLowerCase().replace(/_/g, "");
|
|
4890
5475
|
}
|
|
4891
|
-
var SCOPE_KEYS = new Set([...SCOPE_COLUMNS].map(
|
|
5476
|
+
var SCOPE_KEYS = new Set([...SCOPE_COLUMNS].map(normalizeColumn2));
|
|
4892
5477
|
function isScopeColumn(column) {
|
|
4893
|
-
return column !== null && SCOPE_KEYS.has(
|
|
5478
|
+
return column !== null && SCOPE_KEYS.has(normalizeColumn2(column));
|
|
4894
5479
|
}
|
|
4895
5480
|
function isObjectIdColumn(column) {
|
|
4896
5481
|
if (column === null || isScopeColumn(column)) return false;
|
|
4897
|
-
const c =
|
|
5482
|
+
const c = normalizeColumn2(column);
|
|
4898
5483
|
return c === "id" || c === "uuid" || c === "slug" || c.endsWith("id");
|
|
4899
5484
|
}
|
|
4900
5485
|
function bypassesRls(kind) {
|
|
@@ -4925,7 +5510,8 @@ function handlerViews(ctx) {
|
|
|
4925
5510
|
data,
|
|
4926
5511
|
inputs: data.inputs ?? [],
|
|
4927
5512
|
authenticated: kinds.length > 0,
|
|
4928
|
-
operatorOnly: kinds.length > 0 && kinds.every((k) => k === "secret")
|
|
5513
|
+
operatorOnly: kinds.length > 0 && kinds.every((k) => k === "secret"),
|
|
5514
|
+
roleChecks: data.roleChecks ?? []
|
|
4929
5515
|
};
|
|
4930
5516
|
});
|
|
4931
5517
|
}
|
|
@@ -4955,12 +5541,12 @@ function rlsNote(t, tableName, kind) {
|
|
|
4955
5541
|
const bypass = kind === "direct_db" ? "a direct database connection (Drizzle/Prisma) does not go through PostgREST, so it does not apply" : "the service role bypasses it";
|
|
4956
5542
|
return `RLS is enabled on public.${tableName} with ${t.policies.length} polic${t.policies.length === 1 ? "y" : "ies"}, but ${bypass}.`;
|
|
4957
5543
|
}
|
|
4958
|
-
function finding(ctx, rule, partial) {
|
|
5544
|
+
function finding(ctx, rule, partial, severity) {
|
|
4959
5545
|
return {
|
|
4960
5546
|
id: ctx.nextId(),
|
|
4961
5547
|
ruleId: rule.id,
|
|
4962
5548
|
status: "likely",
|
|
4963
|
-
severity: rule.severity,
|
|
5549
|
+
severity: severity ?? rule.severity,
|
|
4964
5550
|
confidence: rule.confidence,
|
|
4965
5551
|
cwe: rule.cwe,
|
|
4966
5552
|
createdAt: ctx.now,
|
|
@@ -4968,6 +5554,39 @@ function finding(ctx, rule, partial) {
|
|
|
4968
5554
|
...partial
|
|
4969
5555
|
};
|
|
4970
5556
|
}
|
|
5557
|
+
function anonReadPolicy(t) {
|
|
5558
|
+
if (!t?.known || !t.rlsEnabled) return void 0;
|
|
5559
|
+
return t.policyDetails.find(
|
|
5560
|
+
(p) => (p.command === "select" || p.command === "all") && (p.using ?? "").replace(/[\s()]/g, "").toLowerCase() === "true" && (p.roles.length === 0 || p.roles.some((r) => r === "anon" || r === "public"))
|
|
5561
|
+
);
|
|
5562
|
+
}
|
|
5563
|
+
function publicReadNote(p, table) {
|
|
5564
|
+
return ` public.${table} is readable by the anon role through RLS policy "${p.name}" (${p.location.file}:${p.location.line}), so this read leaks nothing beyond what the anon key already returns; the repository may not have intended that policy, so the finding stays at medium.`;
|
|
5565
|
+
}
|
|
5566
|
+
function adminOnly(ctx, h, t) {
|
|
5567
|
+
const check = h.roleChecks[0];
|
|
5568
|
+
if (!check || !t?.known || !singleTenantTable(ctx, t.table)) return void 0;
|
|
5569
|
+
return check;
|
|
5570
|
+
}
|
|
5571
|
+
function singleTenantTable(ctx, table) {
|
|
5572
|
+
const info = ctx.model.tables.find((x) => x.table === table.toLowerCase());
|
|
5573
|
+
if (!info || info.columns.some((c) => isScopeColumn(c))) return false;
|
|
5574
|
+
for (const col of info.columnInfo ?? []) {
|
|
5575
|
+
if (!col.references || col.nullable) continue;
|
|
5576
|
+
const parent = ctx.model.tables.find((x) => x.table === col.references?.table);
|
|
5577
|
+
if (parent?.columns.some((c) => isScopeColumn(c))) return false;
|
|
5578
|
+
}
|
|
5579
|
+
return true;
|
|
5580
|
+
}
|
|
5581
|
+
function adminOnlyNote(check, table) {
|
|
5582
|
+
return ` Admin-only: the handler stops unless ${check.source} passes the role check at ${check.file}:${check.line} (${check.text}), and public.${table} has no tenant or owner column, so the row is shared site content rather than a tenant's; verify the admin check cannot be self-granted.`;
|
|
5583
|
+
}
|
|
5584
|
+
function tableDataOf(ctx, table) {
|
|
5585
|
+
return ctx.graph.nodes.get(`table:${table}`)?.data;
|
|
5586
|
+
}
|
|
5587
|
+
function callerCheck(checks) {
|
|
5588
|
+
return checks?.find((c) => isScopeColumn(c.column) && !c.inputDerived);
|
|
5589
|
+
}
|
|
4971
5590
|
function guardTiesRowToCaller(guard, table, callerFns) {
|
|
4972
5591
|
const callerFilter = guard.filters.find((f) => isScopeColumn(f.column) && !f.inputDerived);
|
|
4973
5592
|
if (callerFilter) {
|
|
@@ -4977,6 +5596,14 @@ function guardTiesRowToCaller(guard, table, callerFns) {
|
|
|
4977
5596
|
why: ""
|
|
4978
5597
|
};
|
|
4979
5598
|
}
|
|
5599
|
+
const compared = callerCheck(guard.checks);
|
|
5600
|
+
if (compared) {
|
|
5601
|
+
return {
|
|
5602
|
+
tied: true,
|
|
5603
|
+
how: `its ${compared.column} compared with ${compared.valueText} in code, stopping otherwise`,
|
|
5604
|
+
why: ""
|
|
5605
|
+
};
|
|
5606
|
+
}
|
|
4980
5607
|
if (guard.client !== "user_scoped") {
|
|
4981
5608
|
return {
|
|
4982
5609
|
tied: false,
|
|
@@ -5017,12 +5644,19 @@ var serviceRoleObjectAccessWithoutTenantScope = {
|
|
|
5017
5644
|
const filters = q.filters;
|
|
5018
5645
|
const idFilter = filters.find((f) => f.inputDerived && isObjectIdColumn(f.column));
|
|
5019
5646
|
if (!idFilter || filters.some((f) => isScopeColumn(f.column))) continue;
|
|
5647
|
+
if (q.operation === "select" && callerCheck(q.ownerChecks)) continue;
|
|
5020
5648
|
const guard = q.guard;
|
|
5021
|
-
const
|
|
5649
|
+
const guardTable = guard?.parent ? tableDataOf(ctx, guard.table) : v.tableData;
|
|
5650
|
+
const tied = guard ? guardTiesRowToCaller(guard, guardTable, callerFns) : null;
|
|
5022
5651
|
if (guard && tied?.tied && guard.exitsWhenMissing) continue;
|
|
5023
|
-
const
|
|
5652
|
+
const guardWhat = guard?.parent ? `the parent row ${guard.parent.table}.${guard.parent.column} (${q.table}.${guard.column} refers to it by ${guard.parent.how})` : `the same "${guard?.column}"`;
|
|
5653
|
+
const guardNote = guard && tied && (tied.tied || guard.client === "user_scoped") ? ` An earlier read of ${guardWhat} at ${guard.location.file}:${guard.location.line} (${tied.how}) could be an ownership check, but ${tied.tied ? "the entry point does not stop when it finds no row" : tied.why}.` : "";
|
|
5024
5654
|
const tableName = v.tableData?.table ?? q.table;
|
|
5025
5655
|
const authNote = h.authenticated ? "The handler authenticates the caller but never checks that the row belongs to them." : "The handler does not authenticate the caller at all.";
|
|
5656
|
+
const admin = adminOnly(ctx, h, v.tableData);
|
|
5657
|
+
const anonPolicy = q.operation === "select" ? anonReadPolicy(v.tableData) : void 0;
|
|
5658
|
+
const downgrade = admin || anonPolicy ? "medium" : void 0;
|
|
5659
|
+
const downgradeNote = (admin ? adminOnlyNote(admin, tableName) : "") + (anonPolicy ? publicReadNote(anonPolicy, tableName) : "");
|
|
5026
5660
|
const path = [
|
|
5027
5661
|
h.data.kind === "server_action" ? "Server action call" : "HTTP request",
|
|
5028
5662
|
h.data.entry,
|
|
@@ -5034,26 +5668,40 @@ var serviceRoleObjectAccessWithoutTenantScope = {
|
|
|
5034
5668
|
const evidence = [
|
|
5035
5669
|
{
|
|
5036
5670
|
kind: "rule",
|
|
5037
|
-
summary: `${q.operation} on public.${tableName} filtered by user-controlled "${idFilter.column}" through a ${clientNoun(v.clientData)}, with no tenant/owner scoping. ${authNote} ${rlsNote(v.tableData, tableName, v.clientData?.kind)}${viaNote(q)}${guardNote}`,
|
|
5038
|
-
locations: locations(
|
|
5671
|
+
summary: `${q.operation} on public.${tableName} filtered by user-controlled "${idFilter.column}" through a ${clientNoun(v.clientData)}, with no tenant/owner scoping. ${authNote} ${rlsNote(v.tableData, tableName, v.clientData?.kind)}${viaNote(q)}${guardNote}${downgradeNote}`,
|
|
5672
|
+
locations: locations(
|
|
5673
|
+
h.handler.location,
|
|
5674
|
+
v.query.location,
|
|
5675
|
+
v.client?.location,
|
|
5676
|
+
admin ? { file: admin.file, line: admin.line } : void 0,
|
|
5677
|
+
anonPolicy?.location
|
|
5678
|
+
),
|
|
5039
5679
|
data: {
|
|
5040
5680
|
deterministic: false,
|
|
5041
5681
|
ruleId: this.id,
|
|
5042
5682
|
authenticated: h.authenticated,
|
|
5043
|
-
query: q.text
|
|
5683
|
+
query: q.text,
|
|
5684
|
+
...admin ? { adminOnly: true, roleCheck: admin.source } : {},
|
|
5685
|
+
...anonPolicy ? { anonReadPolicy: anonPolicy.name } : {}
|
|
5044
5686
|
}
|
|
5045
5687
|
},
|
|
5046
5688
|
{ kind: "trace", summary: path.join(" -> ") }
|
|
5047
5689
|
];
|
|
5690
|
+
const who = admin ? "Admin-only" : h.authenticated ? "Cross-tenant" : "Unauthenticated";
|
|
5048
5691
|
out.push(
|
|
5049
|
-
finding(
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5692
|
+
finding(
|
|
5693
|
+
ctx,
|
|
5694
|
+
this,
|
|
5695
|
+
{
|
|
5696
|
+
title: `${who} ${q.operation} on "${tableName}" via ${clientNoun(v.clientData)}${admin ? " (verify the admin check)" : ""}`,
|
|
5697
|
+
entrypoints: [h.data.entry],
|
|
5698
|
+
sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
|
|
5699
|
+
sinks: [`supabase.${q.operation}:public.${tableName}`],
|
|
5700
|
+
path,
|
|
5701
|
+
evidence
|
|
5702
|
+
},
|
|
5703
|
+
downgrade
|
|
5704
|
+
)
|
|
5057
5705
|
);
|
|
5058
5706
|
}
|
|
5059
5707
|
}
|
|
@@ -5170,7 +5818,13 @@ var rlsPolicyWithoutCallerPredicate = {
|
|
|
5170
5818
|
],
|
|
5171
5819
|
summary: `Policy "${p.name}" for ${p.command} on public.${t.table} uses (${expr}). The table has a scope column (${t.columns.filter(isScopeColumn).join(", ")}) but the policy never compares it to auth.uid() or the caller's tenant, so RLS lets every ${p.roles.join("/") || "authenticated"} user through.`,
|
|
5172
5820
|
title: `RLS policy "${p.name}" on "${t.table}" does not scope rows to the caller`,
|
|
5173
|
-
data: {
|
|
5821
|
+
data: {
|
|
5822
|
+
deterministic: false,
|
|
5823
|
+
ruleId: this.id,
|
|
5824
|
+
policy: p.name,
|
|
5825
|
+
command: p.command,
|
|
5826
|
+
table: t.table
|
|
5827
|
+
},
|
|
5174
5828
|
tail: locations(p.location)
|
|
5175
5829
|
}));
|
|
5176
5830
|
addReach(g, h, v, `supabase.${op}:public.${t.table}`);
|
|
@@ -5331,6 +5985,14 @@ var serviceRoleQueryWithoutAuthentication = {
|
|
|
5331
5985
|
const v = views[0];
|
|
5332
5986
|
if (!v) continue;
|
|
5333
5987
|
const tables = [...new Set(views.map((x) => x.tableData?.table ?? x.data.table))];
|
|
5988
|
+
const anonPolicies = views.map(
|
|
5989
|
+
(x) => x.data.operation === "select" ? anonReadPolicy(x.tableData) : void 0
|
|
5990
|
+
);
|
|
5991
|
+
const allPublicReads = anonPolicies.every((x) => x !== void 0);
|
|
5992
|
+
const publicNote = allPublicReads ? views.map((x, i) => {
|
|
5993
|
+
const pol = anonPolicies[i];
|
|
5994
|
+
return pol ? publicReadNote(pol, x.tableData?.table ?? x.data.table) : "";
|
|
5995
|
+
}).join("") : "";
|
|
5334
5996
|
const path = [
|
|
5335
5997
|
h.data.kind === "server_action" ? "Server action call" : "HTTP request",
|
|
5336
5998
|
h.data.entry,
|
|
@@ -5339,24 +6001,37 @@ var serviceRoleQueryWithoutAuthentication = {
|
|
|
5339
6001
|
`public.${tables.join(", public.")}`
|
|
5340
6002
|
];
|
|
5341
6003
|
out.push(
|
|
5342
|
-
finding(
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
6004
|
+
finding(
|
|
6005
|
+
ctx,
|
|
6006
|
+
this,
|
|
6007
|
+
{
|
|
6008
|
+
title: `Unauthenticated ${views.some((v2) => v2.clientData?.kind === "direct_db") ? "database" : "service-role"} access to "${tables.join('", "')}" in ${h.data.entry}`,
|
|
6009
|
+
entrypoints: [h.data.entry],
|
|
6010
|
+
sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
|
|
6011
|
+
sinks: views.map(
|
|
6012
|
+
(x) => `supabase.${x.data.operation}:public.${x.tableData?.table ?? x.data.table}`
|
|
6013
|
+
),
|
|
6014
|
+
path,
|
|
6015
|
+
evidence: [
|
|
6016
|
+
{
|
|
6017
|
+
kind: "rule",
|
|
6018
|
+
summary: `${h.data.entry} runs ${views.length} privileged quer${views.length === 1 ? "y" : "ies"} (${tables.join(", ")}; RLS does not protect them) and establishes no caller: no auth.getUser/getSession/getClaims call, no session from an auth library, no auth helper, no comparison of a request credential with a server secret (cron secret, API key, signature) and no API-key lookup was found, directly or in the helpers it calls.${publicNote}`,
|
|
6019
|
+
locations: locations(
|
|
6020
|
+
h.handler.location,
|
|
6021
|
+
...views.map((x) => x.query.location),
|
|
6022
|
+
...anonPolicies.map((x) => x?.location)
|
|
6023
|
+
),
|
|
6024
|
+
data: {
|
|
6025
|
+
deterministic: false,
|
|
6026
|
+
ruleId: this.id,
|
|
6027
|
+
...allPublicReads ? { anonReadPolicies: anonPolicies.map((x) => x?.name ?? "") } : {}
|
|
6028
|
+
}
|
|
6029
|
+
},
|
|
6030
|
+
{ kind: "trace", summary: path.join(" -> ") }
|
|
6031
|
+
]
|
|
6032
|
+
},
|
|
6033
|
+
allPublicReads ? "medium" : void 0
|
|
6034
|
+
)
|
|
5360
6035
|
);
|
|
5361
6036
|
}
|
|
5362
6037
|
return out;
|
|
@@ -5421,6 +6096,253 @@ var supabaseAuthorizationPack = [
|
|
|
5421
6096
|
rlsPolicyWithoutCallerPredicate
|
|
5422
6097
|
];
|
|
5423
6098
|
|
|
6099
|
+
// packages/rules/src/packs/supabase-sql-policies.ts
|
|
6100
|
+
var DATA_API = "Supabase Data API (PostgREST)";
|
|
6101
|
+
function locations2(...refs) {
|
|
6102
|
+
const out = [];
|
|
6103
|
+
for (const r of refs) {
|
|
6104
|
+
if (r && !out.some((o) => o.file === r.file && o.line === r.line)) out.push(r);
|
|
6105
|
+
}
|
|
6106
|
+
return out;
|
|
6107
|
+
}
|
|
6108
|
+
function finding2(ctx, rule, body, severity) {
|
|
6109
|
+
return {
|
|
6110
|
+
id: ctx.nextId(),
|
|
6111
|
+
ruleId: rule.id,
|
|
6112
|
+
status: "likely",
|
|
6113
|
+
severity: severity ?? rule.severity,
|
|
6114
|
+
confidence: rule.confidence,
|
|
6115
|
+
cwe: rule.cwe,
|
|
6116
|
+
createdAt: ctx.now,
|
|
6117
|
+
updatedAt: ctx.now,
|
|
6118
|
+
...body
|
|
6119
|
+
};
|
|
6120
|
+
}
|
|
6121
|
+
function publicTables(ctx) {
|
|
6122
|
+
return ctx.model.tables.filter((t) => !t.table.includes("."));
|
|
6123
|
+
}
|
|
6124
|
+
function commandLabel(p) {
|
|
6125
|
+
return p.command === "all" ? "for all commands" : `for ${p.command}`;
|
|
6126
|
+
}
|
|
6127
|
+
function roleLabel(p) {
|
|
6128
|
+
return p.roles.length === 0 ? "public (no TO clause)" : p.roles.join(", ");
|
|
6129
|
+
}
|
|
6130
|
+
function quote(expr, max = 200) {
|
|
6131
|
+
if (expr === null) return "(none)";
|
|
6132
|
+
const one = expr.replace(/\s+/g, " ").trim();
|
|
6133
|
+
return one.length > max ? `${one.slice(0, max - 1)}\u2026` : one;
|
|
6134
|
+
}
|
|
6135
|
+
var USER_METADATA = /\buser_metadata\b/i;
|
|
6136
|
+
var RAW_USER_META = /\braw_user_meta_data\b/i;
|
|
6137
|
+
var JWT_SOURCE = /auth\s*\.\s*jwt\s*\(|request\.jwt\.claim/i;
|
|
6138
|
+
var AUTH_USERS = /\bauth\s*\.\s*users\b/i;
|
|
6139
|
+
function readsSelfWrittenMetadata(expr) {
|
|
6140
|
+
if (USER_METADATA.test(expr) && JWT_SOURCE.test(expr)) return true;
|
|
6141
|
+
return RAW_USER_META.test(expr) && AUTH_USERS.test(expr);
|
|
6142
|
+
}
|
|
6143
|
+
var rlsPolicyTrustsUserMetadata = {
|
|
6144
|
+
id: "supabase.rls-policy-trusts-user-metadata",
|
|
6145
|
+
title: "RLS policy reads user_metadata from the JWT",
|
|
6146
|
+
description: "A policy decides access with a claim under user_metadata (or raw_user_meta_data). Any signed-in user can write user_metadata with updateUser({ data }), and the claim lands in their next JWT without validation, so the policy grants itself. Move the claim to app_metadata, which only the service role writes, or read a roles table joined on auth.uid().",
|
|
6147
|
+
severity: "critical",
|
|
6148
|
+
confidence: 0.9,
|
|
6149
|
+
cwe: ["CWE-602", "CWE-863"],
|
|
6150
|
+
evaluate(ctx) {
|
|
6151
|
+
const out = [];
|
|
6152
|
+
for (const t of publicTables(ctx)) {
|
|
6153
|
+
for (const p of t.policyDetails) {
|
|
6154
|
+
const where2 = [];
|
|
6155
|
+
if (p.using !== null && readsSelfWrittenMetadata(p.using)) where2.push(["USING", p.using]);
|
|
6156
|
+
if (p.check !== null && readsSelfWrittenMetadata(p.check))
|
|
6157
|
+
where2.push(["WITH CHECK", p.check]);
|
|
6158
|
+
if (where2.length === 0) continue;
|
|
6159
|
+
const clause = where2.map(([kind, expr]) => `${kind} ${quote(expr)}`).join(" / ");
|
|
6160
|
+
const evidence = [
|
|
6161
|
+
{
|
|
6162
|
+
kind: "rule",
|
|
6163
|
+
summary: `Policy "${p.name}" on public.${t.table} (${commandLabel(p)}, to ${roleLabel(p)}) decides access from user_metadata: ${clause}. A signed-in user sets user_metadata themselves with supabase.auth.updateUser({ data: { ... } }); the value is copied into their next access token without any check, so the caller can grant themselves whatever this predicate asks for. app_metadata cannot be written this way, and a roles table joined on auth.uid() cannot either.`,
|
|
6164
|
+
locations: locations2(p.location, t.location),
|
|
6165
|
+
// No `deterministic: true`: a critical deterministic finding blocks the GitHub
|
|
6166
|
+
// check, and this rule has not been seen on a real repository yet (zero hits on the
|
|
6167
|
+
// 26-repository corpus of 13 Sept 2026). It blocks once measured, not before.
|
|
6168
|
+
data: {
|
|
6169
|
+
ruleId: this.id,
|
|
6170
|
+
table: t.table,
|
|
6171
|
+
policy: p.name,
|
|
6172
|
+
command: p.command
|
|
6173
|
+
}
|
|
6174
|
+
},
|
|
6175
|
+
{
|
|
6176
|
+
kind: "trace",
|
|
6177
|
+
summary: [
|
|
6178
|
+
"Any signed-in user",
|
|
6179
|
+
'auth.updateUser({ data: { role: "admin" } })',
|
|
6180
|
+
"the claim is signed into the next JWT",
|
|
6181
|
+
`policy "${p.name}" reads it from user_metadata`,
|
|
6182
|
+
`public.${t.table} (${commandLabel(p)})`
|
|
6183
|
+
].join(" -> ")
|
|
6184
|
+
}
|
|
6185
|
+
];
|
|
6186
|
+
out.push(
|
|
6187
|
+
finding2(ctx, this, {
|
|
6188
|
+
title: `Policy "${p.name}" on "${t.table}" trusts user_metadata`,
|
|
6189
|
+
entrypoints: [DATA_API],
|
|
6190
|
+
sources: ["jwt:user_metadata"],
|
|
6191
|
+
sinks: [`supabase.policy:public.${t.table}`],
|
|
6192
|
+
path: [
|
|
6193
|
+
"Any signed-in user",
|
|
6194
|
+
"user_metadata written by the user",
|
|
6195
|
+
`policy "${p.name}"`,
|
|
6196
|
+
`public.${t.table}`
|
|
6197
|
+
],
|
|
6198
|
+
evidence
|
|
6199
|
+
})
|
|
6200
|
+
);
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
return out;
|
|
6204
|
+
}
|
|
6205
|
+
};
|
|
6206
|
+
var policiesWithoutRlsEnabled = {
|
|
6207
|
+
id: "supabase.policies-without-rls-enabled",
|
|
6208
|
+
title: "Policies exist but RLS is never enabled on the table",
|
|
6209
|
+
description: "A migration writes policies for a table but never runs `alter table ... enable row level security`, so the policies have no effect and the table stays fully readable and writable through the Data API. Every reviewer who sees the policies assumes the table is protected. Supabase's own lint (0007) reports the same shape.",
|
|
6210
|
+
severity: "high",
|
|
6211
|
+
confidence: 0.9,
|
|
6212
|
+
cwe: ["CWE-284"],
|
|
6213
|
+
evaluate(ctx) {
|
|
6214
|
+
const out = [];
|
|
6215
|
+
for (const t of publicTables(ctx)) {
|
|
6216
|
+
if (t.rlsEnabled || t.policyDetails.length === 0) continue;
|
|
6217
|
+
const names = t.policyDetails.map((p) => `"${p.name}" (${commandLabel(p)})`).join(", ");
|
|
6218
|
+
const evidence = [
|
|
6219
|
+
{
|
|
6220
|
+
kind: "rule",
|
|
6221
|
+
summary: `public.${t.table} has ${t.policyDetails.length} ${t.policyDetails.length === 1 ? "policy" : "policies"} \u2014 ${names} \u2014 and no "alter table public.${t.table} enable row level security" anywhere in the migrations. Policies only apply to a table with RLS on, so every one of them is dead and the table is fully readable and writable by anyone holding the public anon key. The policies say what the intent was, which is what makes this a defect rather than a choice.`,
|
|
6222
|
+
locations: locations2(t.location, t.policyDetails[0]?.location),
|
|
6223
|
+
data: {
|
|
6224
|
+
deterministic: true,
|
|
6225
|
+
ruleId: this.id,
|
|
6226
|
+
table: t.table,
|
|
6227
|
+
policies: t.policyDetails.map((p) => p.name)
|
|
6228
|
+
}
|
|
6229
|
+
},
|
|
6230
|
+
{
|
|
6231
|
+
kind: "trace",
|
|
6232
|
+
summary: [
|
|
6233
|
+
"Anyone with the public anon key",
|
|
6234
|
+
"PostgREST",
|
|
6235
|
+
`public.${t.table} (RLS never enabled)`,
|
|
6236
|
+
`${t.policyDetails.length} policies that never run`
|
|
6237
|
+
].join(" -> ")
|
|
6238
|
+
}
|
|
6239
|
+
];
|
|
6240
|
+
out.push(
|
|
6241
|
+
finding2(ctx, this, {
|
|
6242
|
+
title: `Table "${t.table}" has policies but RLS is off`,
|
|
6243
|
+
entrypoints: [DATA_API],
|
|
6244
|
+
sources: ["anon-key"],
|
|
6245
|
+
sinks: [`supabase.select:public.${t.table}`],
|
|
6246
|
+
path: [
|
|
6247
|
+
"Anyone with the public anon key",
|
|
6248
|
+
"PostgREST",
|
|
6249
|
+
`public.${t.table} (RLS off, policies inert)`
|
|
6250
|
+
],
|
|
6251
|
+
evidence
|
|
6252
|
+
})
|
|
6253
|
+
);
|
|
6254
|
+
}
|
|
6255
|
+
return out;
|
|
6256
|
+
}
|
|
6257
|
+
};
|
|
6258
|
+
function isTautology(expr) {
|
|
6259
|
+
if (expr === null) return false;
|
|
6260
|
+
return /^\(*\s*true\s*\)*$/i.test(expr.trim());
|
|
6261
|
+
}
|
|
6262
|
+
var ANON_ROLES = /* @__PURE__ */ new Set(["anon", "public"]);
|
|
6263
|
+
function reachableByAnon(p) {
|
|
6264
|
+
if (p.roles.length === 0) return true;
|
|
6265
|
+
return p.roles.some((r) => ANON_ROLES.has(r.toLowerCase().replace(/^"|"$/g, "")));
|
|
6266
|
+
}
|
|
6267
|
+
var WRITE_COMMANDS = /* @__PURE__ */ new Set(["insert", "update", "delete", "all"]);
|
|
6268
|
+
var anonWritePolicy = {
|
|
6269
|
+
id: "supabase.anon-write-policy",
|
|
6270
|
+
title: "Write policy open to anon or every role",
|
|
6271
|
+
description: "An insert, update or delete policy targets anon (or omits the TO clause, which means PUBLIC) and decides with `true`. Anyone holding the public anon key writes the table directly through PostgREST, including rows that belong to signed-in users, whether or not the application has a form for it.",
|
|
6272
|
+
severity: "high",
|
|
6273
|
+
confidence: 0.85,
|
|
6274
|
+
cwe: ["CWE-284", "CWE-862"],
|
|
6275
|
+
evaluate(ctx) {
|
|
6276
|
+
const out = [];
|
|
6277
|
+
for (const t of publicTables(ctx)) {
|
|
6278
|
+
if (!t.rlsEnabled) continue;
|
|
6279
|
+
for (const p of t.policyDetails) {
|
|
6280
|
+
if (!WRITE_COMMANDS.has(p.command) || !reachableByAnon(p)) continue;
|
|
6281
|
+
const decides = p.command === "insert" ? [["WITH CHECK", p.check]] : p.command === "all" ? [
|
|
6282
|
+
["USING", p.using],
|
|
6283
|
+
["WITH CHECK", p.check]
|
|
6284
|
+
] : [["USING", p.using]];
|
|
6285
|
+
const open = decides.filter(([, expr]) => isTautology(expr));
|
|
6286
|
+
if (open.length === 0) continue;
|
|
6287
|
+
const insertOnly = p.command === "insert";
|
|
6288
|
+
const severity = insertOnly ? "medium" : "high";
|
|
6289
|
+
const clause = open.map(([kind, expr]) => `${kind} ${quote(expr)}`).join(" / ");
|
|
6290
|
+
const verbs = p.command === "all" ? "insert, update and delete" : `${p.command} rows in`;
|
|
6291
|
+
const evidence = [
|
|
6292
|
+
{
|
|
6293
|
+
kind: "rule",
|
|
6294
|
+
summary: `Policy "${p.name}" on public.${t.table} is ${commandLabel(p)} to ${roleLabel(p)} and decides with a tautology: ${clause}. Anyone holding the public anon key can ${verbs} public.${t.table} straight through PostgREST, without going through this application.${insertOnly ? " An insert-only policy is how deliberate public forms are written, so this is reported as medium: check that the table is meant to accept rows from strangers and that a rate limit and a validation trigger exist." : " Rows that belong to signed-in users can be changed or deleted by a stranger."}`,
|
|
6295
|
+
locations: locations2(p.location, t.location),
|
|
6296
|
+
data: {
|
|
6297
|
+
deterministic: true,
|
|
6298
|
+
ruleId: this.id,
|
|
6299
|
+
table: t.table,
|
|
6300
|
+
policy: p.name,
|
|
6301
|
+
command: p.command,
|
|
6302
|
+
roles: p.roles
|
|
6303
|
+
}
|
|
6304
|
+
},
|
|
6305
|
+
{
|
|
6306
|
+
kind: "trace",
|
|
6307
|
+
summary: [
|
|
6308
|
+
"Anyone with the public anon key",
|
|
6309
|
+
"PostgREST",
|
|
6310
|
+
`policy "${p.name}" (${commandLabel(p)}, to ${roleLabel(p)}, ${clause})`,
|
|
6311
|
+
`public.${t.table}`
|
|
6312
|
+
].join(" -> ")
|
|
6313
|
+
}
|
|
6314
|
+
];
|
|
6315
|
+
out.push(
|
|
6316
|
+
finding2(
|
|
6317
|
+
ctx,
|
|
6318
|
+
this,
|
|
6319
|
+
{
|
|
6320
|
+
title: `Policy "${p.name}" lets anyone ${p.command === "all" ? "write" : p.command} "${t.table}"`,
|
|
6321
|
+
entrypoints: [DATA_API],
|
|
6322
|
+
sources: ["anon-key"],
|
|
6323
|
+
sinks: [`supabase.${p.command === "all" ? "insert" : p.command}:public.${t.table}`],
|
|
6324
|
+
path: [
|
|
6325
|
+
"Anyone with the public anon key",
|
|
6326
|
+
"PostgREST",
|
|
6327
|
+
`policy "${p.name}" (${commandLabel(p)}, to ${roleLabel(p)})`,
|
|
6328
|
+
`public.${t.table}`
|
|
6329
|
+
],
|
|
6330
|
+
evidence
|
|
6331
|
+
},
|
|
6332
|
+
severity
|
|
6333
|
+
)
|
|
6334
|
+
);
|
|
6335
|
+
}
|
|
6336
|
+
}
|
|
6337
|
+
return out;
|
|
6338
|
+
}
|
|
6339
|
+
};
|
|
6340
|
+
var supabaseSqlPoliciesPack = [
|
|
6341
|
+
rlsPolicyTrustsUserMetadata,
|
|
6342
|
+
policiesWithoutRlsEnabled,
|
|
6343
|
+
anonWritePolicy
|
|
6344
|
+
];
|
|
6345
|
+
|
|
5424
6346
|
// packages/rules/src/packs/supabase-storage-rpc.ts
|
|
5425
6347
|
function reaches(ctx) {
|
|
5426
6348
|
const out = [];
|
|
@@ -5443,7 +6365,7 @@ function reaches(ctx) {
|
|
|
5443
6365
|
}
|
|
5444
6366
|
return out;
|
|
5445
6367
|
}
|
|
5446
|
-
function
|
|
6368
|
+
function locations3(...refs) {
|
|
5447
6369
|
const out = [];
|
|
5448
6370
|
for (const r of refs) {
|
|
5449
6371
|
if (r && !out.some((o) => o.file === r.file && o.line === r.line)) out.push(r);
|
|
@@ -5453,7 +6375,7 @@ function locations2(...refs) {
|
|
|
5453
6375
|
function unique(values) {
|
|
5454
6376
|
return [...new Set(values)];
|
|
5455
6377
|
}
|
|
5456
|
-
function
|
|
6378
|
+
function finding3(ctx, rule, body, severity) {
|
|
5457
6379
|
return {
|
|
5458
6380
|
id: ctx.nextId(),
|
|
5459
6381
|
ruleId: rule.id,
|
|
@@ -5509,7 +6431,7 @@ var storageObjectAccessWithoutOwnerScope = {
|
|
|
5509
6431
|
{
|
|
5510
6432
|
kind: "rule",
|
|
5511
6433
|
summary: `storage.from(${s.bucket === null ? "\u2026" : `"${s.bucket}"`}).${s.op}(${s.pathText}) runs through ${client}, a service-role client, so storage policies on storage.objects do not apply. The object path comes from the request and is neither prefixed with nor checked against the caller's user id, so any signed-in user can ${verb} any object in ${bucketLabel(s.bucket)}, including other users' files. The handler authenticates the caller but never ties the path to them.${via}`,
|
|
5512
|
-
locations:
|
|
6434
|
+
locations: locations3(r.handler.location, r.query.location, r.client?.location),
|
|
5513
6435
|
data: {
|
|
5514
6436
|
deterministic: false,
|
|
5515
6437
|
ruleId: this.id,
|
|
@@ -5522,7 +6444,7 @@ var storageObjectAccessWithoutOwnerScope = {
|
|
|
5522
6444
|
{ kind: "trace", summary: path.join(" -> ") }
|
|
5523
6445
|
];
|
|
5524
6446
|
out.push(
|
|
5525
|
-
|
|
6447
|
+
finding3(ctx, this, {
|
|
5526
6448
|
title: `Cross-user storage ${s.op} in ${bucketLabel(s.bucket)} via service-role client`,
|
|
5527
6449
|
entrypoints: [r.handlerData.entry],
|
|
5528
6450
|
sources: r.inputs.map((i) => `${i.kind}:${i.name}`),
|
|
@@ -5629,7 +6551,7 @@ var storagePolicyWithoutOwnerCheck = {
|
|
|
5629
6551
|
];
|
|
5630
6552
|
const reachNote = entries.length > 0 ? ` The app reaches the bucket from ${entries.join(", ")} with a client that relies on this policy.` : "";
|
|
5631
6553
|
out.push(
|
|
5632
|
-
|
|
6554
|
+
finding3(ctx, this, {
|
|
5633
6555
|
title: `Storage policy "${p.name}" lets ${who} ${verb} every object in ${buckets.length > 0 ? `bucket ${where2}` : "every bucket"}`,
|
|
5634
6556
|
entrypoints: [...entries, storageEntry],
|
|
5635
6557
|
sources: unique([
|
|
@@ -5642,7 +6564,7 @@ var storagePolicyWithoutOwnerCheck = {
|
|
|
5642
6564
|
{
|
|
5643
6565
|
kind: "rule",
|
|
5644
6566
|
summary: `Policy "${p.name}" for ${p.command} on storage.objects ${clause} (${expr}) checks only the bucket: no auth.uid(), no storage.foldername(name) ownership, no owner column. So ${whoLong} can ${verb} every object in ${where2}, including other users' files, straight through the Storage API.${privateNote}${reachNote} Scope it to the owner, e.g. bucket_id = '${buckets[0] ?? "<bucket>"}' and (storage.foldername(name))[1] = (select auth.uid())::text.`,
|
|
5645
|
-
locations:
|
|
6567
|
+
locations: locations3(p.location, ...reached.map((r) => r.query.location)),
|
|
5646
6568
|
data: { deterministic: false, ruleId: this.id, policy: p.name, buckets }
|
|
5647
6569
|
},
|
|
5648
6570
|
{ kind: "trace", summary: path.join(" -> ") }
|
|
@@ -5690,7 +6612,7 @@ var securityDefinerFunctionWithoutCallerCheck = {
|
|
|
5690
6612
|
"no auth.uid() / auth.jwt() check"
|
|
5691
6613
|
];
|
|
5692
6614
|
out.push(
|
|
5693
|
-
|
|
6615
|
+
finding3(
|
|
5694
6616
|
ctx,
|
|
5695
6617
|
this,
|
|
5696
6618
|
{
|
|
@@ -5706,7 +6628,7 @@ var securityDefinerFunctionWithoutCallerCheck = {
|
|
|
5706
6628
|
{
|
|
5707
6629
|
kind: "rule",
|
|
5708
6630
|
summary: `public.${fn.name}() is SECURITY DEFINER: it runs with the rights of its owner and Row Level Security does not apply inside it. Its body never reads the caller's identity (auth.uid(), auth.jwt(), auth.email() or the request JWT), so whatever it returns or changes is available to every role that can execute it: ${roles.join(", ")}. ${who} at ${endpoint}.${callNote} Filter by auth.uid() inside the function, make it SECURITY INVOKER, or revoke EXECUTE from public, anon and authenticated.`,
|
|
5709
|
-
locations:
|
|
6631
|
+
locations: locations3(
|
|
5710
6632
|
fn.location,
|
|
5711
6633
|
...sites.flatMap((s) => [s.handler.location, s.query.location])
|
|
5712
6634
|
),
|
|
@@ -5751,8 +6673,40 @@ function runRules(rules, model, graph, opts = {}) {
|
|
|
5751
6673
|
}
|
|
5752
6674
|
}
|
|
5753
6675
|
findings = applySuppressions(findings, model, now);
|
|
6676
|
+
findings = applyPublicTables(findings, opts.publicTables ?? [], now);
|
|
5754
6677
|
return findings;
|
|
5755
6678
|
}
|
|
6679
|
+
var PUBLIC_TABLE_RULES = /* @__PURE__ */ new Set([
|
|
6680
|
+
"supabase.service-role-query-without-authentication",
|
|
6681
|
+
"supabase.rls-policy-without-caller-predicate"
|
|
6682
|
+
]);
|
|
6683
|
+
var READ_SINK = /^supabase\.select:public\.(.+)$/;
|
|
6684
|
+
function applyPublicTables(findings, publicTables2, now) {
|
|
6685
|
+
if (publicTables2.length === 0) return findings;
|
|
6686
|
+
const declared = new Set(publicTables2.map((t) => t.toLowerCase()));
|
|
6687
|
+
return findings.map((f) => {
|
|
6688
|
+
if (f.status === "suppressed" || !PUBLIC_TABLE_RULES.has(f.ruleId)) return f;
|
|
6689
|
+
if (f.sinks.length === 0) return f;
|
|
6690
|
+
const tables = [];
|
|
6691
|
+
for (const sink of f.sinks) {
|
|
6692
|
+
const table = READ_SINK.exec(sink)?.[1];
|
|
6693
|
+
if (table === void 0 || !declared.has(table.toLowerCase())) return f;
|
|
6694
|
+
if (!tables.includes(table)) tables.push(table);
|
|
6695
|
+
}
|
|
6696
|
+
const command = f.evidence[0]?.data?.command;
|
|
6697
|
+
if (f.ruleId === "supabase.rls-policy-without-caller-predicate" && command !== "select") {
|
|
6698
|
+
return f;
|
|
6699
|
+
}
|
|
6700
|
+
return transition(f, "suppressed", {
|
|
6701
|
+
evidence: {
|
|
6702
|
+
kind: "rule",
|
|
6703
|
+
summary: `Suppressed: declared public in audit.config.json (publicTables: ${tables.join(", ")}). Every query of this finding only reads ${tables.map((t) => `public.${t}`).join(", ")}; the declaration never covers insert, update, delete or rpc paths, nor the object-access and mass-assignment rules.`,
|
|
6704
|
+
data: { suppressed: true, ruleId: f.ruleId, publicTables: tables }
|
|
6705
|
+
},
|
|
6706
|
+
now
|
|
6707
|
+
});
|
|
6708
|
+
});
|
|
6709
|
+
}
|
|
5756
6710
|
function applySuppressions(findings, model, now) {
|
|
5757
6711
|
const byEntry = new Map(model.routes.map((h) => [h.entry, h.ignores]));
|
|
5758
6712
|
return findings.map((f) => {
|
|
@@ -5777,12 +6731,15 @@ function applySuppressions(findings, model, now) {
|
|
|
5777
6731
|
// packages/rules/src/index.ts
|
|
5778
6732
|
var defaultRules = [
|
|
5779
6733
|
...supabaseAuthorizationPack,
|
|
5780
|
-
...supabaseStorageRpcPack
|
|
6734
|
+
...supabaseStorageRpcPack,
|
|
6735
|
+
...supabaseSqlPoliciesPack
|
|
5781
6736
|
];
|
|
5782
6737
|
|
|
5783
6738
|
// packages/scanner/src/config.ts
|
|
5784
6739
|
import { existsSync, readFileSync as readFileSync3, realpathSync, statSync } from "node:fs";
|
|
5785
6740
|
import { isAbsolute, join as join4, resolve as resolve3, sep as sep2 } from "node:path";
|
|
6741
|
+
var MAX_PUBLIC_TABLES = 64;
|
|
6742
|
+
var TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5786
6743
|
var MAX_REPO_MIGRATION_DIRS = 16;
|
|
5787
6744
|
function shown2(value) {
|
|
5788
6745
|
return JSON.stringify(value.length > 120 ? `${value.slice(0, 120)}...` : value);
|
|
@@ -5808,14 +6765,42 @@ function loadAuditConfig(root) {
|
|
|
5808
6765
|
const obj = raw;
|
|
5809
6766
|
const ignore = strings(obj.ignore);
|
|
5810
6767
|
const migrations = strings(obj.migrations);
|
|
6768
|
+
const publicTables2 = publicTableNames(obj.publicTables);
|
|
5811
6769
|
return {
|
|
5812
6770
|
config: {
|
|
5813
6771
|
...ignore ? { ignore } : {},
|
|
5814
|
-
...migrations ? { migrations } : {}
|
|
6772
|
+
...migrations ? { migrations } : {},
|
|
6773
|
+
...publicTables2.tables ? { publicTables: publicTables2.tables } : {}
|
|
5815
6774
|
},
|
|
5816
|
-
warnings:
|
|
6775
|
+
warnings: publicTables2.warnings
|
|
5817
6776
|
};
|
|
5818
6777
|
}
|
|
6778
|
+
function publicTableNames(value) {
|
|
6779
|
+
const warnings = [];
|
|
6780
|
+
if (value === void 0) return { warnings };
|
|
6781
|
+
if (!Array.isArray(value)) {
|
|
6782
|
+
return { warnings: ["audit.config.json: publicTables ignored (not an array of table names)"] };
|
|
6783
|
+
}
|
|
6784
|
+
const tables = [];
|
|
6785
|
+
for (const entry of value) {
|
|
6786
|
+
if (typeof entry !== "string" || !TABLE_NAME.test(entry.trim())) {
|
|
6787
|
+
warnings.push(
|
|
6788
|
+
`audit.config.json: ignored publicTables entry ${shown2(String(entry))} (not a table name)`
|
|
6789
|
+
);
|
|
6790
|
+
continue;
|
|
6791
|
+
}
|
|
6792
|
+
const name = entry.trim().toLowerCase();
|
|
6793
|
+
if (tables.includes(name)) continue;
|
|
6794
|
+
if (tables.length >= MAX_PUBLIC_TABLES) {
|
|
6795
|
+
warnings.push(
|
|
6796
|
+
`audit.config.json: ignored publicTables entry ${shown2(entry)} (at most ${MAX_PUBLIC_TABLES} tables)`
|
|
6797
|
+
);
|
|
6798
|
+
continue;
|
|
6799
|
+
}
|
|
6800
|
+
tables.push(name);
|
|
6801
|
+
}
|
|
6802
|
+
return { tables, warnings };
|
|
6803
|
+
}
|
|
5819
6804
|
function inside(base, p) {
|
|
5820
6805
|
return p === base || p.startsWith(base.endsWith(sep2) ? base : `${base}${sep2}`);
|
|
5821
6806
|
}
|
|
@@ -5881,7 +6866,28 @@ function repoMigrationDirs(root, entries) {
|
|
|
5881
6866
|
}
|
|
5882
6867
|
|
|
5883
6868
|
// packages/scanner/src/scan.ts
|
|
5884
|
-
|
|
6869
|
+
var ScanError = class extends Error {
|
|
6870
|
+
constructor(code, path, message) {
|
|
6871
|
+
super(message);
|
|
6872
|
+
this.code = code;
|
|
6873
|
+
this.path = path;
|
|
6874
|
+
}
|
|
6875
|
+
code;
|
|
6876
|
+
path;
|
|
6877
|
+
name = "ScanError";
|
|
6878
|
+
};
|
|
6879
|
+
function ensureDirectory(path) {
|
|
6880
|
+
let isDirectory;
|
|
6881
|
+
try {
|
|
6882
|
+
isDirectory = statSync2(path).isDirectory();
|
|
6883
|
+
} catch (e) {
|
|
6884
|
+
const code = errorCode2(e);
|
|
6885
|
+
const why = code === "ENOENT" ? "no such file or directory" : code;
|
|
6886
|
+
throw new ScanError("path_not_found", path, `${path} is not a directory (${why})`);
|
|
6887
|
+
}
|
|
6888
|
+
if (!isDirectory) throw new ScanError("path_not_found", path, `${path} is not a directory`);
|
|
6889
|
+
}
|
|
6890
|
+
function summarize(model, rules, publicTables2 = []) {
|
|
5885
6891
|
const apiTables = model.tables.filter((t) => !t.table.includes("."));
|
|
5886
6892
|
return {
|
|
5887
6893
|
root: model.root,
|
|
@@ -5891,10 +6897,12 @@ function summarize(model, rules) {
|
|
|
5891
6897
|
tablesKnown: apiTables.length,
|
|
5892
6898
|
tablesWithRls: apiTables.filter((t) => t.rlsEnabled).length,
|
|
5893
6899
|
rules,
|
|
5894
|
-
warnings: model.warnings
|
|
6900
|
+
warnings: model.warnings,
|
|
6901
|
+
publicTables: [...publicTables2]
|
|
5895
6902
|
};
|
|
5896
6903
|
}
|
|
5897
6904
|
function runScan(path, opts = {}) {
|
|
6905
|
+
ensureDirectory(path);
|
|
5898
6906
|
const cfg = loadAuditConfig(path);
|
|
5899
6907
|
const repoDirs = repoMigrationDirs(path, cfg.config.migrations ?? []);
|
|
5900
6908
|
const sqlDirs = [...repoDirs.dirs, ...opts.sqlDirs ?? []];
|
|
@@ -5904,10 +6912,11 @@ function runScan(path, opts = {}) {
|
|
|
5904
6912
|
...ignore.globs.length > 0 ? { ignore: ignore.globs } : {}
|
|
5905
6913
|
});
|
|
5906
6914
|
const graph = buildGraph(model);
|
|
5907
|
-
const
|
|
6915
|
+
const publicTables2 = cfg.config.publicTables ?? [];
|
|
6916
|
+
const runOpts = { ...opts.now === void 0 ? {} : { now: opts.now }, publicTables: publicTables2 };
|
|
5908
6917
|
const findings = runRules(defaultRules, model, graph, runOpts);
|
|
5909
6918
|
const coverage = summarizeCoverage(findings);
|
|
5910
|
-
const summary = summarize(model, defaultRules.length);
|
|
6919
|
+
const summary = summarize(model, defaultRules.length, publicTables2);
|
|
5911
6920
|
return {
|
|
5912
6921
|
summary: {
|
|
5913
6922
|
...summary,
|
|
@@ -5941,6 +6950,8 @@ Options:
|
|
|
5941
6950
|
--migrations <dir> extra directory with Supabase migration SQL (repeatable)
|
|
5942
6951
|
-h, --help show this help
|
|
5943
6952
|
|
|
6953
|
+
Exit code: 0 clean, 1 a finding reached --fail-on, 2 usage error or <path> is not a directory
|
|
6954
|
+
|
|
5944
6955
|
Config: <path>/audit.config.json { "ignore": ["evals/**"], "migrations": ["supabase/migrations"] }
|
|
5945
6956
|
Suppress a finding: // auditai:ignore <ruleId|*> -- reason (above the handler or at the top of a file)
|
|
5946
6957
|
`;
|
|
@@ -5976,10 +6987,15 @@ function main(argv) {
|
|
|
5976
6987
|
return 2;
|
|
5977
6988
|
}
|
|
5978
6989
|
const migrations = values.migrations.map((m) => resolve4(m));
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
migrations.length > 0 ? { sqlDirs: migrations } : {}
|
|
5982
|
-
)
|
|
6990
|
+
let result;
|
|
6991
|
+
try {
|
|
6992
|
+
result = runScan(positionals[0] ?? ".", migrations.length > 0 ? { sqlDirs: migrations } : {});
|
|
6993
|
+
} catch (e) {
|
|
6994
|
+
if (!(e instanceof ScanError)) throw e;
|
|
6995
|
+
process.stderr.write(`error: ${e.message}
|
|
6996
|
+
`);
|
|
6997
|
+
return 2;
|
|
6998
|
+
}
|
|
5983
6999
|
process.stdout.write(
|
|
5984
7000
|
values.json ? `${JSON.stringify(result, null, 2)}
|
|
5985
7001
|
` : formatScanText(result)
|