auditai-scan 0.4.0 → 0.5.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 +1132 -386
- package/package.json +1 -1
package/dist/auditai-scan.mjs
CHANGED
|
@@ -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);
|
|
@@ -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
|
|
|
@@ -3052,48 +3309,148 @@ function parseSqlForRls(rel, text, into) {
|
|
|
3052
3309
|
if (!isAppliedSqlFile(rel)) return;
|
|
3053
3310
|
const state = schemaStateFor(into);
|
|
3054
3311
|
for (const stmt of splitSqlStatements(text)) {
|
|
3055
|
-
|
|
3056
|
-
if (!
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
rest
|
|
3072
|
-
);
|
|
3073
|
-
const roles = rolesMatch?.[1] ? rolesMatch[1].split(/\s*,\s*/).map((r) => r.toLowerCase()) : [];
|
|
3074
|
-
let using = null;
|
|
3075
|
-
let check = null;
|
|
3076
|
-
const u = /\busing\s*\(/i.exec(rest);
|
|
3077
|
-
if (u) using = balanced(rest, u.index + u[0].length - 1)?.inner.trim() ?? null;
|
|
3078
|
-
const c = /\bwith\s+check\s*\(/i.exec(rest);
|
|
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
|
-
});
|
|
3312
|
+
applyStatement(state, stmt, rel);
|
|
3313
|
+
if (!isWord(stmt.tokens[0], "do")) continue;
|
|
3314
|
+
const expanded = expandDoBlock(stmt);
|
|
3315
|
+
for (const s of expanded.statements) applyStatement(state, s, rel);
|
|
3316
|
+
if (expanded.dynamic) warnDynamicSql(state, rel);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
function warnDynamicSql(state, rel) {
|
|
3320
|
+
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`;
|
|
3321
|
+
if (!state.warnings.includes(w)) state.warnings.push(w);
|
|
3322
|
+
}
|
|
3323
|
+
function applyStatement(state, stmt, rel) {
|
|
3324
|
+
const cp = CREATE_POLICY.exec(stmt.text);
|
|
3325
|
+
if (!cp?.[1] || !cp[4]) {
|
|
3326
|
+
applySchemaStatement(state, stmt, rel);
|
|
3327
|
+
return;
|
|
3089
3328
|
}
|
|
3329
|
+
const t = ensureTable(
|
|
3330
|
+
state,
|
|
3331
|
+
qualifiedKey({ schema: cp[3] ?? null, name: cp[4] }),
|
|
3332
|
+
rel,
|
|
3333
|
+
stmt.line
|
|
3334
|
+
);
|
|
3335
|
+
const name = cp[2] ?? cp[1];
|
|
3336
|
+
const rest = stmt.text.slice(cp[0].length);
|
|
3337
|
+
const cmdMatch = /\bfor\s+(select|insert|update|delete|all)\b/i.exec(rest);
|
|
3338
|
+
const command = cmdMatch?.[1]?.toLowerCase() ?? "all";
|
|
3339
|
+
const rolesMatch = /\bto\s+([A-Za-z_][A-Za-z0-9_]*(?:\s*,\s*[A-Za-z_][A-Za-z0-9_]*)*)/i.exec(
|
|
3340
|
+
rest
|
|
3341
|
+
);
|
|
3342
|
+
const roles = rolesMatch?.[1] ? rolesMatch[1].split(/\s*,\s*/).map((r) => r.toLowerCase()) : [];
|
|
3343
|
+
let using = null;
|
|
3344
|
+
let check = null;
|
|
3345
|
+
const u = /\busing\s*\(/i.exec(rest);
|
|
3346
|
+
if (u) using = balanced(rest, u.index + u[0].length - 1)?.inner.trim() ?? null;
|
|
3347
|
+
const c = /\bwith\s+check\s*\(/i.exec(rest);
|
|
3348
|
+
if (c) check = balanced(rest, c.index + c[0].length - 1)?.inner.trim() ?? null;
|
|
3349
|
+
t.policies.push(name);
|
|
3350
|
+
t.policyDetails.push({
|
|
3351
|
+
name,
|
|
3352
|
+
command,
|
|
3353
|
+
roles,
|
|
3354
|
+
using,
|
|
3355
|
+
check,
|
|
3356
|
+
location: { file: rel, line: stmt.line }
|
|
3357
|
+
});
|
|
3090
3358
|
}
|
|
3091
3359
|
function sqlSchemaFor(into) {
|
|
3092
3360
|
return finishSchema(schemaStateFor(into));
|
|
3093
3361
|
}
|
|
3094
3362
|
|
|
3095
|
-
// packages/parser/src/
|
|
3363
|
+
// packages/parser/src/role-gates.ts
|
|
3096
3364
|
import ts7 from "typescript";
|
|
3365
|
+
var ROLE_PROPERTY = /^(role|roles|app_metadata|is_?admin|is_?superuser|permissions?|claims?)$/i;
|
|
3366
|
+
var EMAIL_PROPERTY = /^email$/i;
|
|
3367
|
+
function propertyPath(e) {
|
|
3368
|
+
const u = unwrap(e);
|
|
3369
|
+
if (ts7.isIdentifier(u)) return [u.text];
|
|
3370
|
+
if (ts7.isPropertyAccessExpression(u)) {
|
|
3371
|
+
const base = propertyPath(u.expression);
|
|
3372
|
+
return base === null ? null : [...base, u.name.text];
|
|
3373
|
+
}
|
|
3374
|
+
return null;
|
|
3375
|
+
}
|
|
3376
|
+
function isLiteral(e) {
|
|
3377
|
+
const u = unwrap(e);
|
|
3378
|
+
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;
|
|
3379
|
+
}
|
|
3380
|
+
function sessionClaim(e, sessionNames) {
|
|
3381
|
+
const path = propertyPath(e);
|
|
3382
|
+
const root = path?.[0];
|
|
3383
|
+
const last = path?.[path.length - 1];
|
|
3384
|
+
if (!path || path.length < 2 || root === void 0 || last === void 0) return null;
|
|
3385
|
+
if (!sessionNames.has(root)) return null;
|
|
3386
|
+
if (path.some((p) => p === "user_metadata")) return null;
|
|
3387
|
+
if (path.some((p) => ROLE_PROPERTY.test(p))) return { source: path.join("."), kind: "role" };
|
|
3388
|
+
if (EMAIL_PROPERTY.test(last)) return { source: path.join("."), kind: "email" };
|
|
3389
|
+
return null;
|
|
3390
|
+
}
|
|
3391
|
+
function claimIn(cond, sessionNames) {
|
|
3392
|
+
let found = null;
|
|
3393
|
+
const visit = (n) => {
|
|
3394
|
+
if (found !== null) return;
|
|
3395
|
+
if (ts7.isCallExpression(n)) {
|
|
3396
|
+
for (const a of n.arguments) {
|
|
3397
|
+
const c = sessionClaim(a, sessionNames);
|
|
3398
|
+
if (c) {
|
|
3399
|
+
found = c.source;
|
|
3400
|
+
return;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
} else if (ts7.isBinaryExpression(n)) {
|
|
3404
|
+
for (const [a, b] of [
|
|
3405
|
+
[n.left, n.right],
|
|
3406
|
+
[n.right, n.left]
|
|
3407
|
+
]) {
|
|
3408
|
+
const c = sessionClaim(a, sessionNames);
|
|
3409
|
+
if (!c) continue;
|
|
3410
|
+
if (c.kind === "role" || isLiteral(b) || envNamesIn(b).length > 0) {
|
|
3411
|
+
found = c.source;
|
|
3412
|
+
return;
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
} else if (ts7.isPrefixUnaryExpression(n) || ts7.isPropertyAccessExpression(n)) {
|
|
3416
|
+
const target = ts7.isPrefixUnaryExpression(n) ? n.operand : n;
|
|
3417
|
+
const c = sessionClaim(target, sessionNames);
|
|
3418
|
+
if (c?.kind === "role" && !ts7.isCallExpression(n.parent)) {
|
|
3419
|
+
found = c.source;
|
|
3420
|
+
return;
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
n.forEachChild(visit);
|
|
3424
|
+
};
|
|
3425
|
+
visit(cond);
|
|
3426
|
+
return found;
|
|
3427
|
+
}
|
|
3428
|
+
function sessionNamesIn(body, isSessionCall) {
|
|
3429
|
+
const out = /* @__PURE__ */ new Set();
|
|
3430
|
+
walkOwn(body, (n) => {
|
|
3431
|
+
if (!ts7.isVariableDeclaration(n) || !n.initializer) return;
|
|
3432
|
+
const init = unwrap(n.initializer);
|
|
3433
|
+
if (ts7.isCallExpression(init) && isSessionCall(init)) {
|
|
3434
|
+
for (const nm of boundNames(n.name)) out.add(nm);
|
|
3435
|
+
}
|
|
3436
|
+
});
|
|
3437
|
+
return out;
|
|
3438
|
+
}
|
|
3439
|
+
function roleGatesIn(body, sessionNames) {
|
|
3440
|
+
const out = [];
|
|
3441
|
+
if (sessionNames.size === 0) return out;
|
|
3442
|
+
walkOwn(body, (n) => {
|
|
3443
|
+
if (!ts7.isIfStatement(n)) return;
|
|
3444
|
+
const exit = exitKind(n.thenStatement);
|
|
3445
|
+
if (!exit) return;
|
|
3446
|
+
const source = claimIn(n.expression, sessionNames);
|
|
3447
|
+
if (source !== null) out.push({ node: n, source, exit });
|
|
3448
|
+
});
|
|
3449
|
+
return out;
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
// packages/parser/src/storage.ts
|
|
3453
|
+
import ts8 from "typescript";
|
|
3097
3454
|
var STORAGE_OPS = {
|
|
3098
3455
|
download: "select",
|
|
3099
3456
|
list: "select",
|
|
@@ -3109,14 +3466,14 @@ var STORAGE_OPS = {
|
|
|
3109
3466
|
var TWO_PATHS = /* @__PURE__ */ new Set(["move", "copy"]);
|
|
3110
3467
|
function isStorageRoot(e) {
|
|
3111
3468
|
const u = unwrap(e);
|
|
3112
|
-
return
|
|
3469
|
+
return ts8.isPropertyAccessExpression(u) && u.name.text === "storage";
|
|
3113
3470
|
}
|
|
3114
3471
|
function storageBindingsIn(body) {
|
|
3115
3472
|
const out = /* @__PURE__ */ new Map();
|
|
3116
|
-
for (const decl of collect(body,
|
|
3117
|
-
if (!decl.initializer || !
|
|
3473
|
+
for (const decl of collect(body, ts8.isVariableDeclaration)) {
|
|
3474
|
+
if (!decl.initializer || !ts8.isIdentifier(decl.name)) continue;
|
|
3118
3475
|
const init = unwrap(decl.initializer);
|
|
3119
|
-
if (!
|
|
3476
|
+
if (!ts8.isCallExpression(init) || !ts8.isPropertyAccessExpression(init.expression)) continue;
|
|
3120
3477
|
const callee = init.expression;
|
|
3121
3478
|
if (callee.name.text !== "from" || !isStorageRoot(callee.expression)) continue;
|
|
3122
3479
|
const root = unwrap(callee.expression);
|
|
@@ -3132,7 +3489,7 @@ function storageCallOf(chain, bound) {
|
|
|
3132
3489
|
const r = unwrap(root);
|
|
3133
3490
|
return { clientRoot: r.expression, bucketArg: first.args[0], op: known(second) };
|
|
3134
3491
|
}
|
|
3135
|
-
if (
|
|
3492
|
+
if (ts8.isIdentifier(root) && first) {
|
|
3136
3493
|
const b = bound.get(root.text);
|
|
3137
3494
|
if (b) return { clientRoot: b.clientRoot, bucketArg: b.bucketArg, op: known(first) };
|
|
3138
3495
|
}
|
|
@@ -3143,9 +3500,9 @@ function bucketName(arg, sf) {
|
|
|
3143
3500
|
const u = unwrap(arg);
|
|
3144
3501
|
const lit = stringLiteralValue(u);
|
|
3145
3502
|
if (lit !== null) return lit;
|
|
3146
|
-
if (!
|
|
3147
|
-
for (const d of collect(sf,
|
|
3148
|
-
if (!
|
|
3503
|
+
if (!ts8.isIdentifier(u)) return null;
|
|
3504
|
+
for (const d of collect(sf, ts8.isVariableDeclaration)) {
|
|
3505
|
+
if (!ts8.isIdentifier(d.name) || d.name.text !== u.text || !d.initializer) continue;
|
|
3149
3506
|
const v = stringLiteralValue(unwrap(d.initializer));
|
|
3150
3507
|
if (v !== null) return v;
|
|
3151
3508
|
}
|
|
@@ -3158,20 +3515,20 @@ var INPUT_OBJECT = /^(params|searchParams|body|query|headers|cookies|formData)$/
|
|
|
3158
3515
|
var CallerScope = class {
|
|
3159
3516
|
constructor(body, inputNames) {
|
|
3160
3517
|
this.inputNames = inputNames;
|
|
3161
|
-
for (const decl of collect(body,
|
|
3162
|
-
if (!decl.initializer || !
|
|
3518
|
+
for (const decl of collect(body, ts8.isVariableDeclaration)) {
|
|
3519
|
+
if (!decl.initializer || !ts8.isIdentifier(decl.name)) continue;
|
|
3163
3520
|
if (this.refersToCaller(decl.initializer)) this.scopedVars.add(decl.name.text);
|
|
3164
3521
|
}
|
|
3165
|
-
for (const call of collect(body,
|
|
3522
|
+
for (const call of collect(body, ts8.isCallExpression)) {
|
|
3166
3523
|
const callee = call.expression;
|
|
3167
|
-
if (!
|
|
3524
|
+
if (!ts8.isPropertyAccessExpression(callee) || callee.name.text !== "startsWith") continue;
|
|
3168
3525
|
const target = unwrap(callee.expression);
|
|
3169
3526
|
const arg = call.arguments[0];
|
|
3170
|
-
if (
|
|
3527
|
+
if (ts8.isIdentifier(target) && arg && this.refersToCaller(arg)) this.guarded.add(target.text);
|
|
3171
3528
|
}
|
|
3172
|
-
for (const bin of collect(body,
|
|
3529
|
+
for (const bin of collect(body, ts8.isBinaryExpression)) {
|
|
3173
3530
|
const k = bin.operatorToken.kind;
|
|
3174
|
-
if (k !==
|
|
3531
|
+
if (k !== ts8.SyntaxKind.EqualsEqualsEqualsToken && k !== ts8.SyntaxKind.ExclamationEqualsEqualsToken && k !== ts8.SyntaxKind.EqualsEqualsToken && k !== ts8.SyntaxKind.ExclamationEqualsToken) {
|
|
3175
3532
|
continue;
|
|
3176
3533
|
}
|
|
3177
3534
|
for (const [side, other] of [
|
|
@@ -3191,13 +3548,13 @@ var CallerScope = class {
|
|
|
3191
3548
|
let hit = false;
|
|
3192
3549
|
walk(e, (n) => {
|
|
3193
3550
|
if (hit) return false;
|
|
3194
|
-
if (
|
|
3551
|
+
if (ts8.isPropertyAccessExpression(n) && this.isIdentityAccess(n)) {
|
|
3195
3552
|
hit = true;
|
|
3196
3553
|
return false;
|
|
3197
3554
|
}
|
|
3198
|
-
if (
|
|
3555
|
+
if (ts8.isIdentifier(n)) {
|
|
3199
3556
|
const parent = n.parent;
|
|
3200
|
-
if (parent &&
|
|
3557
|
+
if (parent && ts8.isPropertyAccessExpression(parent) && parent.name === n) return void 0;
|
|
3201
3558
|
if (this.scopedVars.has(n.text)) hit = true;
|
|
3202
3559
|
else if (IDENTITY_VAR.test(n.text) && !this.inputNames.has(n.text)) hit = true;
|
|
3203
3560
|
}
|
|
@@ -3208,17 +3565,17 @@ var CallerScope = class {
|
|
|
3208
3565
|
/** A path argument is covered when it embeds the caller's id or is an identifier checked against it. */
|
|
3209
3566
|
covers(e) {
|
|
3210
3567
|
const u = unwrap(e);
|
|
3211
|
-
return this.refersToCaller(u) ||
|
|
3568
|
+
return this.refersToCaller(u) || ts8.isIdentifier(u) && this.guarded.has(u.text);
|
|
3212
3569
|
}
|
|
3213
3570
|
isIdentityAccess(pa) {
|
|
3214
3571
|
const field = pa.name.text;
|
|
3215
3572
|
const names = [];
|
|
3216
3573
|
let base = unwrap(pa.expression);
|
|
3217
|
-
while (
|
|
3574
|
+
while (ts8.isPropertyAccessExpression(base)) {
|
|
3218
3575
|
names.push(base.name.text);
|
|
3219
3576
|
base = unwrap(base.expression);
|
|
3220
3577
|
}
|
|
3221
|
-
if (!
|
|
3578
|
+
if (!ts8.isIdentifier(base) || this.inputNames.has(base.text)) return false;
|
|
3222
3579
|
names.push(base.text);
|
|
3223
3580
|
if (names.some((n) => INPUT_OBJECT.test(n))) return false;
|
|
3224
3581
|
if (OWNER_FIELD.test(field)) return true;
|
|
@@ -3227,20 +3584,20 @@ var CallerScope = class {
|
|
|
3227
3584
|
};
|
|
3228
3585
|
function splitHead(e) {
|
|
3229
3586
|
const u = unwrap(e);
|
|
3230
|
-
if (!
|
|
3587
|
+
if (!ts8.isElementAccessExpression(u)) return null;
|
|
3231
3588
|
const idx = u.argumentExpression;
|
|
3232
|
-
if (!
|
|
3589
|
+
if (!ts8.isNumericLiteral(idx) || idx.text !== "0") return null;
|
|
3233
3590
|
const call = unwrap(u.expression);
|
|
3234
|
-
if (!
|
|
3591
|
+
if (!ts8.isCallExpression(call) || !ts8.isPropertyAccessExpression(call.expression)) return null;
|
|
3235
3592
|
if (call.expression.name.text !== "split") return null;
|
|
3236
3593
|
const target = unwrap(call.expression.expression);
|
|
3237
|
-
return
|
|
3594
|
+
return ts8.isIdentifier(target) ? target.text : null;
|
|
3238
3595
|
}
|
|
3239
3596
|
function storageAccessOf(op, bucket, sf, scope, derived) {
|
|
3240
3597
|
const pathArgs = op.args.slice(0, TWO_PATHS.has(op.name) ? 2 : 1);
|
|
3241
3598
|
const parts = pathArgs.flatMap((a) => {
|
|
3242
3599
|
const u = unwrap(a);
|
|
3243
|
-
return
|
|
3600
|
+
return ts8.isArrayLiteralExpression(u) ? [...u.elements] : [a];
|
|
3244
3601
|
});
|
|
3245
3602
|
const tainted = parts.filter((p) => derived(p));
|
|
3246
3603
|
const judged = tainted.length > 0 ? tainted : parts;
|
|
@@ -3254,22 +3611,22 @@ function storageAccessOf(op, bucket, sf, scope, derived) {
|
|
|
3254
3611
|
}
|
|
3255
3612
|
|
|
3256
3613
|
// packages/parser/src/supabase.ts
|
|
3257
|
-
import
|
|
3614
|
+
import ts9 from "typescript";
|
|
3258
3615
|
var CREATE_CLIENT_CALLEES = /^(createClient|createServerClient|createBrowserClient)$/;
|
|
3259
3616
|
function isCreateClientCall(call, sf) {
|
|
3260
3617
|
const callee = call.expression;
|
|
3261
|
-
if (
|
|
3262
|
-
if (
|
|
3618
|
+
if (ts9.isIdentifier(callee)) return CREATE_CLIENT_CALLEES.test(callee.text);
|
|
3619
|
+
if (ts9.isPropertyAccessExpression(callee)) return CREATE_CLIENT_CALLEES.test(callee.name.text);
|
|
3263
3620
|
return CREATE_CLIENT_CALLEES.test(callee.getText(sf));
|
|
3264
3621
|
}
|
|
3265
3622
|
function resolveArgText(expr, sf) {
|
|
3266
3623
|
const u = unwrap(expr);
|
|
3267
|
-
if (!
|
|
3624
|
+
if (!ts9.isIdentifier(u)) return expr.getText(sf);
|
|
3268
3625
|
let scope = expr.parent;
|
|
3269
3626
|
while (scope) {
|
|
3270
|
-
if (
|
|
3271
|
-
const decl = collect(scope,
|
|
3272
|
-
(d) =>
|
|
3627
|
+
if (ts9.isFunctionLike(scope) || ts9.isBlock(scope) || ts9.isSourceFile(scope)) {
|
|
3628
|
+
const decl = collect(scope, ts9.isVariableDeclaration).find(
|
|
3629
|
+
(d) => ts9.isIdentifier(d.name) && d.name.text === u.text && d.initializer
|
|
3273
3630
|
);
|
|
3274
3631
|
if (decl?.initializer) return `${u.text} = ${decl.initializer.getText(sf)}`;
|
|
3275
3632
|
}
|
|
@@ -3311,7 +3668,7 @@ function classifyCreateClientCall(call, sf) {
|
|
|
3311
3668
|
}
|
|
3312
3669
|
var AUTH_CALL = /\.auth\.(getUser|getSession|getClaims)\s*\(/;
|
|
3313
3670
|
function hasModifier(node, kind) {
|
|
3314
|
-
const mods =
|
|
3671
|
+
const mods = ts9.canHaveModifiers(node) ? ts9.getModifiers(node) : void 0;
|
|
3315
3672
|
return mods?.some((m) => m.kind === kind) ?? false;
|
|
3316
3673
|
}
|
|
3317
3674
|
function analyzeModule(rel, sf) {
|
|
@@ -3320,33 +3677,33 @@ function analyzeModule(rel, sf) {
|
|
|
3320
3677
|
const exportedNames = /* @__PURE__ */ new Set();
|
|
3321
3678
|
let defaultExport = null;
|
|
3322
3679
|
for (const stmt of sf.statements) {
|
|
3323
|
-
if (
|
|
3680
|
+
if (ts9.isImportDeclaration(stmt) && ts9.isStringLiteral(stmt.moduleSpecifier)) {
|
|
3324
3681
|
const spec = stmt.moduleSpecifier.text;
|
|
3325
3682
|
const clause = stmt.importClause;
|
|
3326
3683
|
if (!clause) continue;
|
|
3327
3684
|
if (clause.name) imports.set(clause.name.text, { spec, imported: "default" });
|
|
3328
3685
|
const nb = clause.namedBindings;
|
|
3329
|
-
if (nb &&
|
|
3686
|
+
if (nb && ts9.isNamedImports(nb)) {
|
|
3330
3687
|
for (const el of nb.elements) {
|
|
3331
3688
|
imports.set(el.name.text, { spec, imported: (el.propertyName ?? el.name).text });
|
|
3332
3689
|
}
|
|
3333
3690
|
}
|
|
3334
|
-
if (nb &&
|
|
3335
|
-
} else if (
|
|
3336
|
-
const spec = stmt.moduleSpecifier &&
|
|
3691
|
+
if (nb && ts9.isNamespaceImport(nb)) imports.set(nb.name.text, { spec, imported: "*" });
|
|
3692
|
+
} else if (ts9.isExportDeclaration(stmt)) {
|
|
3693
|
+
const spec = stmt.moduleSpecifier && ts9.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : null;
|
|
3337
3694
|
const clause = stmt.exportClause;
|
|
3338
3695
|
if (spec && !clause) reexports.push({ star: true, spec });
|
|
3339
|
-
else if (clause &&
|
|
3696
|
+
else if (clause && ts9.isNamedExports(clause)) {
|
|
3340
3697
|
for (const el of clause.elements) {
|
|
3341
3698
|
const name = (el.propertyName ?? el.name).text;
|
|
3342
3699
|
if (spec) reexports.push({ star: false, name, alias: el.name.text, spec });
|
|
3343
3700
|
else exportedNames.add(name);
|
|
3344
3701
|
}
|
|
3345
3702
|
}
|
|
3346
|
-
} else if (
|
|
3703
|
+
} else if (ts9.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
|
3347
3704
|
const e = unwrap(stmt.expression);
|
|
3348
|
-
if (
|
|
3349
|
-
} else if (
|
|
3705
|
+
if (ts9.isIdentifier(e)) defaultExport = e.text;
|
|
3706
|
+
} else if (ts9.isFunctionDeclaration(stmt) && stmt.name && hasModifier(stmt, ts9.SyntaxKind.DefaultKeyword)) {
|
|
3350
3707
|
defaultExport = stmt.name.text;
|
|
3351
3708
|
}
|
|
3352
3709
|
}
|
|
@@ -3361,15 +3718,15 @@ function analyzeModule(rel, sf) {
|
|
|
3361
3718
|
const moduleVars = /* @__PURE__ */ new Map();
|
|
3362
3719
|
const drizzleTables = /* @__PURE__ */ new Map();
|
|
3363
3720
|
for (const stmt of sf.statements) {
|
|
3364
|
-
if (!
|
|
3365
|
-
const exported = hasModifier(stmt,
|
|
3721
|
+
if (!ts9.isVariableStatement(stmt)) continue;
|
|
3722
|
+
const exported = hasModifier(stmt, ts9.SyntaxKind.ExportKeyword);
|
|
3366
3723
|
for (const d of stmt.declarationList.declarations) {
|
|
3367
|
-
if (!
|
|
3724
|
+
if (!ts9.isIdentifier(d.name) || !d.initializer || functions.has(d.name.text)) continue;
|
|
3368
3725
|
const init = clientCreatingOperand(d.initializer);
|
|
3369
|
-
if (!
|
|
3370
|
-
if (
|
|
3726
|
+
if (!ts9.isCallExpression(init) && !ts9.isNewExpression(init)) continue;
|
|
3727
|
+
if (ts9.isCallExpression(init)) {
|
|
3371
3728
|
const callee = init.expression;
|
|
3372
|
-
const fn =
|
|
3729
|
+
const fn = ts9.isIdentifier(callee) ? callee.text : ts9.isPropertyAccessExpression(callee) ? callee.name.text : "";
|
|
3373
3730
|
const tableName = stringLiteralValue(init.arguments[0]);
|
|
3374
3731
|
if ((DRIZZLE_TABLE_FNS.has(fn) || fn === "table") && tableName !== null) {
|
|
3375
3732
|
drizzleTables.set(d.name.text, tableName);
|
|
@@ -3392,8 +3749,8 @@ function analyzeModule(rel, sf) {
|
|
|
3392
3749
|
);
|
|
3393
3750
|
if (nextAuthLocal.size > 0) {
|
|
3394
3751
|
for (const stmt of sf.statements) {
|
|
3395
|
-
if (!
|
|
3396
|
-
const exported = hasModifier(stmt,
|
|
3752
|
+
if (!ts9.isVariableStatement(stmt)) continue;
|
|
3753
|
+
const exported = hasModifier(stmt, ts9.SyntaxKind.ExportKeyword);
|
|
3397
3754
|
for (const name of nextAuthSessionNames(stmt, nextAuthLocal)) {
|
|
3398
3755
|
const helper = {
|
|
3399
3756
|
name,
|
|
@@ -3416,7 +3773,7 @@ function analyzeModule(rel, sf) {
|
|
|
3416
3773
|
});
|
|
3417
3774
|
continue;
|
|
3418
3775
|
}
|
|
3419
|
-
const creates = collect(f.fn,
|
|
3776
|
+
const creates = collect(f.fn, ts9.isCallExpression).filter((c) => isCreateClientCall(c, sf));
|
|
3420
3777
|
const first = creates[0];
|
|
3421
3778
|
if (first) {
|
|
3422
3779
|
const { kind, evidence } = classifyCreateClientCall(first, sf);
|
|
@@ -3439,7 +3796,7 @@ function analyzeModule(rel, sf) {
|
|
|
3439
3796
|
}
|
|
3440
3797
|
|
|
3441
3798
|
// packages/parser/src/whole-input.ts
|
|
3442
|
-
import
|
|
3799
|
+
import ts10 from "typescript";
|
|
3443
3800
|
var ELEMENT_PRESERVING = /* @__PURE__ */ new Set([
|
|
3444
3801
|
"filter",
|
|
3445
3802
|
"slice",
|
|
@@ -3455,35 +3812,35 @@ var ELEMENT_PRESERVING = /* @__PURE__ */ new Set([
|
|
|
3455
3812
|
]);
|
|
3456
3813
|
var RESHAPING = /* @__PURE__ */ new Set(["map", "flatMap"]);
|
|
3457
3814
|
var LOGICAL = /* @__PURE__ */ new Set([
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3815
|
+
ts10.SyntaxKind.QuestionQuestionToken,
|
|
3816
|
+
ts10.SyntaxKind.BarBarToken,
|
|
3817
|
+
ts10.SyntaxKind.AmpersandAmpersandToken
|
|
3461
3818
|
]);
|
|
3462
3819
|
function isWholeInput(e, cx) {
|
|
3463
3820
|
const u = unwrap(e);
|
|
3464
|
-
if (
|
|
3465
|
-
if (
|
|
3821
|
+
if (ts10.isIdentifier(u)) return cx.wholeName(u.text);
|
|
3822
|
+
if (ts10.isPropertyAccessExpression(u) || ts10.isElementAccessExpression(u)) {
|
|
3466
3823
|
return isWholeInput(u.expression, cx);
|
|
3467
3824
|
}
|
|
3468
|
-
if (
|
|
3469
|
-
return u.properties.some((pr) =>
|
|
3825
|
+
if (ts10.isObjectLiteralExpression(u)) {
|
|
3826
|
+
return u.properties.some((pr) => ts10.isSpreadAssignment(pr) && isWholeInput(pr.expression, cx));
|
|
3470
3827
|
}
|
|
3471
|
-
if (
|
|
3472
|
-
return u.elements.some((el) => isWholeInput(
|
|
3828
|
+
if (ts10.isArrayLiteralExpression(u)) {
|
|
3829
|
+
return u.elements.some((el) => isWholeInput(ts10.isSpreadElement(el) ? el.expression : el, cx));
|
|
3473
3830
|
}
|
|
3474
|
-
if (
|
|
3831
|
+
if (ts10.isConditionalExpression(u)) {
|
|
3475
3832
|
return isWholeInput(u.whenTrue, cx) || isWholeInput(u.whenFalse, cx);
|
|
3476
3833
|
}
|
|
3477
|
-
if (
|
|
3834
|
+
if (ts10.isBinaryExpression(u) && LOGICAL.has(u.operatorToken.kind)) {
|
|
3478
3835
|
return isWholeInput(u.left, cx) || isWholeInput(u.right, cx);
|
|
3479
3836
|
}
|
|
3480
|
-
if (
|
|
3837
|
+
if (ts10.isCallExpression(u)) return isWholeCall(u, cx);
|
|
3481
3838
|
return false;
|
|
3482
3839
|
}
|
|
3483
3840
|
function isWholeCall(call, cx) {
|
|
3484
3841
|
if (cx.requestBody(call)) return true;
|
|
3485
3842
|
const callee = call.expression;
|
|
3486
|
-
if (
|
|
3843
|
+
if (ts10.isPropertyAccessExpression(callee)) {
|
|
3487
3844
|
const method = callee.name.text;
|
|
3488
3845
|
const receiverWhole = isWholeInput(callee.expression, cx);
|
|
3489
3846
|
if (RESHAPING.has(method)) return callbackKeepsWhole(call.arguments[0], receiverWhole, cx);
|
|
@@ -3491,13 +3848,13 @@ function isWholeCall(call, cx) {
|
|
|
3491
3848
|
}
|
|
3492
3849
|
return call.arguments.some((a) => {
|
|
3493
3850
|
const ua = unwrap(a);
|
|
3494
|
-
return isWholeInput(a, cx) ||
|
|
3851
|
+
return isWholeInput(a, cx) || ts10.isIdentifier(ua) && cx.requestName(ua.text);
|
|
3495
3852
|
});
|
|
3496
3853
|
}
|
|
3497
3854
|
function callbackKeepsWhole(cb, receiverWhole, cx) {
|
|
3498
3855
|
if (!cb) return false;
|
|
3499
3856
|
const f = unwrap(cb);
|
|
3500
|
-
if (!
|
|
3857
|
+
if (!ts10.isArrowFunction(f) && !ts10.isFunctionExpression(f)) return receiverWhole;
|
|
3501
3858
|
const element = f.parameters[0];
|
|
3502
3859
|
const elementNames = new Set(receiverWhole && element ? boundNames(element.name) : []);
|
|
3503
3860
|
const inner = {
|
|
@@ -3508,19 +3865,19 @@ function callbackKeepsWhole(cb, receiverWhole, cx) {
|
|
|
3508
3865
|
return ownReturns(f).some((r) => isWholeInput(r, inner));
|
|
3509
3866
|
}
|
|
3510
3867
|
function propertyKey(name) {
|
|
3511
|
-
if (
|
|
3868
|
+
if (ts10.isIdentifier(name) || ts10.isStringLiteral(name)) return name.text;
|
|
3512
3869
|
return null;
|
|
3513
3870
|
}
|
|
3514
3871
|
function wholeParamNames(param, arg, cx) {
|
|
3515
3872
|
if (!arg) return [];
|
|
3516
3873
|
const u = unwrap(arg);
|
|
3517
|
-
if (
|
|
3874
|
+
if (ts10.isObjectBindingPattern(param) && ts10.isObjectLiteralExpression(u) && !u.properties.some(ts10.isSpreadAssignment)) {
|
|
3518
3875
|
const out = [];
|
|
3519
3876
|
for (const el of param.elements) {
|
|
3520
3877
|
if (el.dotDotDotToken) continue;
|
|
3521
3878
|
const key = propertyKey(el.propertyName ?? el.name);
|
|
3522
3879
|
const prop = u.properties.find((pr) => pr.name && propertyKey(pr.name) === key);
|
|
3523
|
-
const value = prop &&
|
|
3880
|
+
const value = prop && ts10.isPropertyAssignment(prop) ? prop.initializer : prop && ts10.isShorthandPropertyAssignment(prop) ? prop.name : void 0;
|
|
3524
3881
|
if (value && isWholeInput(value, cx)) out.push(...boundNames(el.name));
|
|
3525
3882
|
}
|
|
3526
3883
|
return out;
|
|
@@ -3654,8 +4011,8 @@ function scopeOf(p, facts) {
|
|
|
3654
4011
|
return scope;
|
|
3655
4012
|
}
|
|
3656
4013
|
function symOfCallee(p, callee, scope) {
|
|
3657
|
-
if (
|
|
3658
|
-
if (
|
|
4014
|
+
if (ts11.isIdentifier(callee)) return scope.get(callee.text);
|
|
4015
|
+
if (ts11.isPropertyAccessExpression(callee) && ts11.isIdentifier(callee.expression)) {
|
|
3659
4016
|
const ns = scope.get(callee.expression.text);
|
|
3660
4017
|
if (ns?.kind === "namespace") return exportedSym(p, ns.facts, callee.name.text, 0) ?? void 0;
|
|
3661
4018
|
}
|
|
@@ -3663,8 +4020,8 @@ function symOfCallee(p, callee, scope) {
|
|
|
3663
4020
|
}
|
|
3664
4021
|
function returnedExpressions(fn) {
|
|
3665
4022
|
if (!fn.body) return [];
|
|
3666
|
-
if (!
|
|
3667
|
-
return collect(fn.body,
|
|
4023
|
+
if (!ts11.isBlock(fn.body)) return [fn.body];
|
|
4024
|
+
return collect(fn.body, ts11.isReturnStatement).map((r) => r.expression).filter((e) => e !== void 0);
|
|
3668
4025
|
}
|
|
3669
4026
|
function factoryOfFunction(p, sym, depth) {
|
|
3670
4027
|
const key = `${sym.facts.file}#${sym.name}`;
|
|
@@ -3676,7 +4033,7 @@ function factoryOfFunction(p, sym, depth) {
|
|
|
3676
4033
|
let found = null;
|
|
3677
4034
|
for (const ret of returnedExpressions(sym.fn)) {
|
|
3678
4035
|
const u = unwrap(ret);
|
|
3679
|
-
if (!
|
|
4036
|
+
if (!ts11.isCallExpression(u)) continue;
|
|
3680
4037
|
found = classifyCall(p, u, sf, scope, null, depth + 1);
|
|
3681
4038
|
if (found) break;
|
|
3682
4039
|
}
|
|
@@ -3713,7 +4070,7 @@ function classifyCall(p, call, sf, scope, frame, depth) {
|
|
|
3713
4070
|
}
|
|
3714
4071
|
function prismaExtensionBase(p, call, sf, scope, frame, depth) {
|
|
3715
4072
|
const callee = call.expression;
|
|
3716
|
-
if (!
|
|
4073
|
+
if (!ts11.isPropertyAccessExpression(callee) || callee.name.text !== "$extends") return null;
|
|
3717
4074
|
if (depth > 5) return null;
|
|
3718
4075
|
const base = unwrap(callee.expression);
|
|
3719
4076
|
if (isPrismaNew(base)) {
|
|
@@ -3723,8 +4080,8 @@ function prismaExtensionBase(p, call, sf, scope, frame, depth) {
|
|
|
3723
4080
|
location: { file: sf.fileName, line: lineOf(sf, call) }
|
|
3724
4081
|
};
|
|
3725
4082
|
}
|
|
3726
|
-
if (
|
|
3727
|
-
if (!
|
|
4083
|
+
if (ts11.isCallExpression(base)) return classifyCall(p, base, sf, scope, frame, depth + 1);
|
|
4084
|
+
if (!ts11.isIdentifier(base)) return null;
|
|
3728
4085
|
const bound = frame?.clients.get(base.text);
|
|
3729
4086
|
if (bound) return bound;
|
|
3730
4087
|
const sym = scope.get(base.text);
|
|
@@ -3734,15 +4091,15 @@ function instanceOfCall(p, call, frame, scope) {
|
|
|
3734
4091
|
const sym = symOfCallee(p, call.expression, scope);
|
|
3735
4092
|
if (sym?.kind !== "function") return null;
|
|
3736
4093
|
const fscope = scopeOf(p, sym.facts);
|
|
3737
|
-
const params = sym.fn.parameters.map((pp) =>
|
|
4094
|
+
const params = sym.fn.parameters.map((pp) => ts11.isIdentifier(pp.name) ? pp.name.text : null);
|
|
3738
4095
|
for (const ret of returnedExpressions(sym.fn)) {
|
|
3739
4096
|
const u = unwrap(ret);
|
|
3740
|
-
if (!
|
|
4097
|
+
if (!ts11.isNewExpression(u) || !ts11.isIdentifier(u.expression)) continue;
|
|
3741
4098
|
const cs = fscope.get(u.expression.text);
|
|
3742
4099
|
if (cs?.kind !== "class") continue;
|
|
3743
4100
|
const ctorArgs = (u.arguments ?? []).map((a) => {
|
|
3744
4101
|
const ua = unwrap(a);
|
|
3745
|
-
if (
|
|
4102
|
+
if (ts11.isIdentifier(ua)) {
|
|
3746
4103
|
const j = params.indexOf(ua.text);
|
|
3747
4104
|
if (j >= 0 && frame) return argBinding(p, call.arguments[j], frame);
|
|
3748
4105
|
}
|
|
@@ -3774,7 +4131,7 @@ function varBinding(p, sym) {
|
|
|
3774
4131
|
whole: false,
|
|
3775
4132
|
isRequest: false
|
|
3776
4133
|
};
|
|
3777
|
-
} else if (
|
|
4134
|
+
} else if (ts11.isCallExpression(init)) {
|
|
3778
4135
|
const found = classifyCall(p, init, sf, scope, null, 0);
|
|
3779
4136
|
const client = found ? {
|
|
3780
4137
|
kind: found.kind,
|
|
@@ -3783,7 +4140,7 @@ function varBinding(p, sym) {
|
|
|
3783
4140
|
} : null;
|
|
3784
4141
|
const instance = client ? null : instanceOfCall(p, init, null, scope);
|
|
3785
4142
|
out = { client, instance, tainted: false, whole: false, isRequest: false };
|
|
3786
|
-
} else if (
|
|
4143
|
+
} else if (ts11.isNewExpression(init) && ts11.isIdentifier(init.expression)) {
|
|
3787
4144
|
const cs = scope.get(init.expression.text);
|
|
3788
4145
|
if (cs?.kind === "class") {
|
|
3789
4146
|
out = {
|
|
@@ -3809,15 +4166,15 @@ function usesInput(frame, e) {
|
|
|
3809
4166
|
let hit = false;
|
|
3810
4167
|
walk(e, (n) => {
|
|
3811
4168
|
if (hit) return false;
|
|
3812
|
-
if (
|
|
4169
|
+
if (ts11.isCallExpression(n) && handsOverRequest(frame, n)) {
|
|
3813
4170
|
hit = true;
|
|
3814
4171
|
return false;
|
|
3815
4172
|
}
|
|
3816
|
-
if (!
|
|
4173
|
+
if (!ts11.isIdentifier(n)) return void 0;
|
|
3817
4174
|
const parent = n.parent;
|
|
3818
|
-
if (parent &&
|
|
3819
|
-
if (parent &&
|
|
3820
|
-
const member = parent &&
|
|
4175
|
+
if (parent && ts11.isPropertyAccessExpression(parent) && parent.name === n) return void 0;
|
|
4176
|
+
if (parent && ts11.isPropertyAssignment(parent) && parent.name === n) return void 0;
|
|
4177
|
+
const member = parent && ts11.isPropertyAccessExpression(parent) && parent.expression === n;
|
|
3821
4178
|
if (frame.inputNames.has(n.text)) hit = true;
|
|
3822
4179
|
else if (member && frame.reqNames.has(n.text) && REQUEST_MEMBER.test(parent.name.text)) {
|
|
3823
4180
|
hit = true;
|
|
@@ -3836,46 +4193,46 @@ function handsOverRequest(frame, call) {
|
|
|
3836
4193
|
if (IDENTITY_CALLEE.test(callee) || REQUEST_CLIENT_CALLEE.test(callee)) return false;
|
|
3837
4194
|
return call.arguments.some((a) => {
|
|
3838
4195
|
const u = unwrap(a);
|
|
3839
|
-
return
|
|
4196
|
+
return ts11.isIdentifier(u) && frame.reqNames.has(u.text);
|
|
3840
4197
|
});
|
|
3841
4198
|
}
|
|
3842
4199
|
function propertyKeyOf(name) {
|
|
3843
|
-
if (name && (
|
|
4200
|
+
if (name && (ts11.isIdentifier(name) || ts11.isStringLiteral(name))) return name.text;
|
|
3844
4201
|
return null;
|
|
3845
4202
|
}
|
|
3846
4203
|
function taintedProperties(frame, lit) {
|
|
3847
4204
|
const out = /* @__PURE__ */ new Set();
|
|
3848
4205
|
for (const pr of lit.properties) {
|
|
3849
|
-
if (
|
|
4206
|
+
if (ts11.isSpreadAssignment(pr)) {
|
|
3850
4207
|
if (derivedIn(frame, pr.expression)) return null;
|
|
3851
4208
|
continue;
|
|
3852
4209
|
}
|
|
3853
4210
|
const key = propertyKeyOf(pr.name);
|
|
3854
4211
|
if (key === null) continue;
|
|
3855
|
-
if (
|
|
3856
|
-
else if (
|
|
4212
|
+
if (ts11.isPropertyAssignment(pr) && derivedIn(frame, pr.initializer)) out.add(key);
|
|
4213
|
+
else if (ts11.isShorthandPropertyAssignment(pr) && derivedIn(frame, pr.name)) out.add(key);
|
|
3857
4214
|
}
|
|
3858
4215
|
return out;
|
|
3859
4216
|
}
|
|
3860
4217
|
function bindParamTaint(child, param, arg, frame) {
|
|
3861
4218
|
const u = arg ? unwrap(arg) : void 0;
|
|
3862
4219
|
let props = null;
|
|
3863
|
-
if (u &&
|
|
3864
|
-
else if (u &&
|
|
4220
|
+
if (u && ts11.isObjectLiteralExpression(u)) props = taintedProperties(frame, u);
|
|
4221
|
+
else if (u && ts11.isIdentifier(u) && !frame.inputNames.has(u.text)) {
|
|
3865
4222
|
props = frame.partialInputs.get(u.text) ?? null;
|
|
3866
4223
|
}
|
|
3867
4224
|
if (props === null) {
|
|
3868
4225
|
for (const nm of boundNames(param)) child.inputNames.add(nm);
|
|
3869
4226
|
return;
|
|
3870
4227
|
}
|
|
3871
|
-
if (
|
|
4228
|
+
if (ts11.isIdentifier(param)) {
|
|
3872
4229
|
child.partialInputs.set(param.text, new Set(props));
|
|
3873
4230
|
return;
|
|
3874
4231
|
}
|
|
3875
4232
|
bindPatternFrom(child, param, props);
|
|
3876
4233
|
}
|
|
3877
4234
|
function bindPatternFrom(child, pattern, props) {
|
|
3878
|
-
if (!
|
|
4235
|
+
if (!ts11.isObjectBindingPattern(pattern)) {
|
|
3879
4236
|
if (props.size > 0) for (const nm of boundNames(pattern)) child.inputNames.add(nm);
|
|
3880
4237
|
return;
|
|
3881
4238
|
}
|
|
@@ -3888,11 +4245,11 @@ function bindPatternFrom(child, pattern, props) {
|
|
|
3888
4245
|
}
|
|
3889
4246
|
function isRequestBodyCall(frame, call) {
|
|
3890
4247
|
const callee = call.expression;
|
|
3891
|
-
if (!
|
|
4248
|
+
if (!ts11.isPropertyAccessExpression(callee) || !/^(json|formData|text)$/.test(callee.name.text)) {
|
|
3892
4249
|
return false;
|
|
3893
4250
|
}
|
|
3894
4251
|
const recv = unwrap(callee.expression);
|
|
3895
|
-
if (!
|
|
4252
|
+
if (!ts11.isIdentifier(recv)) return false;
|
|
3896
4253
|
if (frame.reqNames.has(recv.text)) return true;
|
|
3897
4254
|
return frame.depth === 0 && frame.reqNames.size === 0 && REQUEST_NAME.test(recv.text);
|
|
3898
4255
|
}
|
|
@@ -3905,7 +4262,7 @@ function wholeContext(frame) {
|
|
|
3905
4262
|
}
|
|
3906
4263
|
function receiverTainted(frame, call) {
|
|
3907
4264
|
const callee = call.expression;
|
|
3908
|
-
if (!
|
|
4265
|
+
if (!ts11.isPropertyAccessExpression(callee) && !ts11.isElementAccessExpression(callee)) return false;
|
|
3909
4266
|
return derivedIn(frame, callee);
|
|
3910
4267
|
}
|
|
3911
4268
|
function argBinding(p, arg, frame) {
|
|
@@ -3915,7 +4272,7 @@ function argBinding(p, arg, frame) {
|
|
|
3915
4272
|
let client = null;
|
|
3916
4273
|
let instance = null;
|
|
3917
4274
|
let isRequest = false;
|
|
3918
|
-
if (
|
|
4275
|
+
if (ts11.isIdentifier(u)) {
|
|
3919
4276
|
client = frame.clients.get(u.text) ?? null;
|
|
3920
4277
|
instance = frame.instances.get(u.text) ?? null;
|
|
3921
4278
|
isRequest = frame.reqNames.has(u.text);
|
|
@@ -3927,14 +4284,14 @@ function argBinding(p, arg, frame) {
|
|
|
3927
4284
|
instance = vb.instance;
|
|
3928
4285
|
}
|
|
3929
4286
|
}
|
|
3930
|
-
} else if (
|
|
4287
|
+
} else if (ts11.isPropertyAccessExpression(u) && u.expression.kind === ts11.SyntaxKind.ThisKeyword) {
|
|
3931
4288
|
const tp = frame.thisProps.get(u.name.text);
|
|
3932
4289
|
if (tp) {
|
|
3933
4290
|
client = tp.client;
|
|
3934
4291
|
instance = tp.instance;
|
|
3935
4292
|
isRequest = tp.isRequest;
|
|
3936
4293
|
}
|
|
3937
|
-
} else if (
|
|
4294
|
+
} else if (ts11.isCallExpression(u)) {
|
|
3938
4295
|
client = classifyCall(p, u, frame.sf, scope, frame, 0);
|
|
3939
4296
|
if (!client) instance = instanceOfCall(p, u, frame, scope);
|
|
3940
4297
|
} else if (isPrismaNew(u)) {
|
|
@@ -3943,7 +4300,7 @@ function argBinding(p, arg, frame) {
|
|
|
3943
4300
|
name: "PrismaClient",
|
|
3944
4301
|
location: { file: frame.rel, line: lineOf(frame.sf, u) }
|
|
3945
4302
|
};
|
|
3946
|
-
} else if (
|
|
4303
|
+
} else if (ts11.isNewExpression(u) && ts11.isIdentifier(u.expression)) {
|
|
3947
4304
|
const cs = scope.get(u.expression.text);
|
|
3948
4305
|
if (cs?.kind === "class") {
|
|
3949
4306
|
instance = {
|
|
@@ -3971,7 +4328,7 @@ function methodTarget(inst, method) {
|
|
|
3971
4328
|
}
|
|
3972
4329
|
function callTarget(p, call, frame, scope) {
|
|
3973
4330
|
const callee = call.expression;
|
|
3974
|
-
if (
|
|
4331
|
+
if (ts11.isIdentifier(callee)) {
|
|
3975
4332
|
const sym = scope.get(callee.text);
|
|
3976
4333
|
if (sym?.kind === "function") {
|
|
3977
4334
|
return { fn: sym.fn, facts: sym.facts, name: sym.name, cls: null, thisProps: /* @__PURE__ */ new Map() };
|
|
@@ -3987,10 +4344,10 @@ function callTarget(p, call, frame, scope) {
|
|
|
3987
4344
|
}
|
|
3988
4345
|
return null;
|
|
3989
4346
|
}
|
|
3990
|
-
if (!
|
|
4347
|
+
if (!ts11.isPropertyAccessExpression(callee)) return null;
|
|
3991
4348
|
const obj = callee.expression;
|
|
3992
4349
|
const method = callee.name.text;
|
|
3993
|
-
if (
|
|
4350
|
+
if (ts11.isIdentifier(obj)) {
|
|
3994
4351
|
const inst = frame.instances.get(obj.text);
|
|
3995
4352
|
if (inst) return methodTarget(inst, method);
|
|
3996
4353
|
const sym = scope.get(obj.text);
|
|
@@ -4008,7 +4365,7 @@ function callTarget(p, call, frame, scope) {
|
|
|
4008
4365
|
}
|
|
4009
4366
|
return null;
|
|
4010
4367
|
}
|
|
4011
|
-
if (obj.kind ===
|
|
4368
|
+
if (obj.kind === ts11.SyntaxKind.ThisKeyword && frame.cls) {
|
|
4012
4369
|
const m = frame.cls.methods.get(method);
|
|
4013
4370
|
if (!m) return null;
|
|
4014
4371
|
return {
|
|
@@ -4019,13 +4376,13 @@ function callTarget(p, call, frame, scope) {
|
|
|
4019
4376
|
thisProps: frame.thisProps
|
|
4020
4377
|
};
|
|
4021
4378
|
}
|
|
4022
|
-
if (
|
|
4379
|
+
if (ts11.isPropertyAccessExpression(obj) && obj.expression.kind === ts11.SyntaxKind.ThisKeyword) {
|
|
4023
4380
|
const tp = frame.thisProps.get(obj.name.text);
|
|
4024
4381
|
if (tp?.instance) return methodTarget(tp.instance, method);
|
|
4025
4382
|
}
|
|
4026
4383
|
return null;
|
|
4027
4384
|
}
|
|
4028
|
-
function
|
|
4385
|
+
function bindDeclarations(p, frame, acc) {
|
|
4029
4386
|
const { rel, sf, fn } = frame;
|
|
4030
4387
|
const scope = scopeOf(p, frame.facts);
|
|
4031
4388
|
const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
|
|
@@ -4057,24 +4414,24 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4057
4414
|
for (const nm of names) addInput(kind, nm, decl, true);
|
|
4058
4415
|
bindInput(names, true);
|
|
4059
4416
|
};
|
|
4060
|
-
for (const decl of collect(body,
|
|
4417
|
+
for (const decl of collect(body, ts11.isVariableDeclaration)) {
|
|
4061
4418
|
if (!decl.initializer) continue;
|
|
4062
4419
|
const init = clientCreatingOperand(decl.initializer);
|
|
4063
4420
|
const names = boundNames(decl.name);
|
|
4064
4421
|
const text = init.getText(sf);
|
|
4065
4422
|
aliasLocal(frame, decl.name, init);
|
|
4066
|
-
const client =
|
|
4067
|
-
if (client &&
|
|
4423
|
+
const client = ts11.isCallExpression(init) ? classifyCall(p, init, sf, scope, frame, 0) : null;
|
|
4424
|
+
if (client && ts11.isIdentifier(decl.name)) {
|
|
4068
4425
|
frame.clients.set(decl.name.text, client);
|
|
4069
4426
|
continue;
|
|
4070
4427
|
}
|
|
4071
|
-
const rowsOfQuery =
|
|
4428
|
+
const rowsOfQuery = ts11.isCallExpression(init) && (isQueryChain(init) || isDbChain(init, frame));
|
|
4072
4429
|
if (frame.depth === 0 && !rowsOfQuery) {
|
|
4073
4430
|
if (/^(params|context\.params|ctx\.params|props\.params)$/.test(text)) {
|
|
4074
4431
|
handlerInput("route_param", names, decl);
|
|
4075
4432
|
continue;
|
|
4076
4433
|
}
|
|
4077
|
-
if (
|
|
4434
|
+
if (ts11.isCallExpression(init) && isRequestCall(text)) {
|
|
4078
4435
|
handlerInput("body", names, decl);
|
|
4079
4436
|
continue;
|
|
4080
4437
|
}
|
|
@@ -4087,9 +4444,9 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4087
4444
|
continue;
|
|
4088
4445
|
}
|
|
4089
4446
|
}
|
|
4090
|
-
if (
|
|
4447
|
+
if (ts11.isCallExpression(init)) {
|
|
4091
4448
|
const inst = instanceOfCall(p, init, frame, scope);
|
|
4092
|
-
if (inst &&
|
|
4449
|
+
if (inst && ts11.isIdentifier(decl.name)) {
|
|
4093
4450
|
frame.instances.set(decl.name.text, inst);
|
|
4094
4451
|
continue;
|
|
4095
4452
|
}
|
|
@@ -4097,34 +4454,41 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4097
4454
|
if (returnsIdentity(p, init, scope)) continue;
|
|
4098
4455
|
const args = init.arguments.map((a) => argBinding(p, a, frame));
|
|
4099
4456
|
if (args.some((a) => a.tainted || a.isRequest) || receiverTainted(frame, init)) {
|
|
4100
|
-
|
|
4457
|
+
const rt = returnTaint(p, init, frame, scope, 0);
|
|
4458
|
+
if (rt === null || rt.tainted && rt.props === null) {
|
|
4459
|
+
bindInput(names, isWholeInput(init, wholeContext(frame)));
|
|
4460
|
+
} else if (rt.tainted && rt.props !== null) {
|
|
4461
|
+
if (ts11.isIdentifier(decl.name))
|
|
4462
|
+
frame.partialInputs.set(decl.name.text, new Set(rt.props));
|
|
4463
|
+
else bindPatternFrom(frame, decl.name, rt.props);
|
|
4464
|
+
}
|
|
4101
4465
|
}
|
|
4102
|
-
} else if (isPrismaNew(init) &&
|
|
4466
|
+
} else if (isPrismaNew(init) && ts11.isIdentifier(decl.name)) {
|
|
4103
4467
|
frame.clients.set(decl.name.text, {
|
|
4104
4468
|
kind: "direct_db",
|
|
4105
4469
|
name: decl.name.text,
|
|
4106
4470
|
location: loc2(init)
|
|
4107
4471
|
});
|
|
4108
|
-
} else if (
|
|
4472
|
+
} else if (ts11.isNewExpression(init) && ts11.isIdentifier(init.expression) && scope.get(init.expression.text)?.kind === "class") {
|
|
4109
4473
|
const cs = scope.get(init.expression.text);
|
|
4110
|
-
if (cs?.kind === "class" &&
|
|
4474
|
+
if (cs?.kind === "class" && ts11.isIdentifier(decl.name)) {
|
|
4111
4475
|
frame.instances.set(decl.name.text, {
|
|
4112
4476
|
cls: cs.cls,
|
|
4113
4477
|
facts: cs.facts,
|
|
4114
4478
|
ctorArgs: (init.arguments ?? []).map((a) => argBinding(p, a, frame))
|
|
4115
4479
|
});
|
|
4116
4480
|
}
|
|
4117
|
-
} else if (
|
|
4481
|
+
} else if (ts11.isIdentifier(init)) {
|
|
4118
4482
|
const c = frame.clients.get(init.text);
|
|
4119
|
-
if (c &&
|
|
4483
|
+
if (c && ts11.isIdentifier(decl.name)) frame.clients.set(decl.name.text, c);
|
|
4120
4484
|
const i = frame.instances.get(init.text);
|
|
4121
|
-
if (i &&
|
|
4485
|
+
if (i && ts11.isIdentifier(decl.name)) frame.instances.set(decl.name.text, i);
|
|
4122
4486
|
if (frame.inputNames.has(init.text)) {
|
|
4123
4487
|
if (frame.depth === 0) for (const nm of names) addInput("body", nm, decl, true);
|
|
4124
4488
|
bindInput(names, frame.wholeNames.has(init.text));
|
|
4125
4489
|
} else {
|
|
4126
4490
|
const partial = frame.partialInputs.get(init.text);
|
|
4127
|
-
if (partial &&
|
|
4491
|
+
if (partial && ts11.isIdentifier(decl.name)) {
|
|
4128
4492
|
frame.partialInputs.set(decl.name.text, new Set(partial));
|
|
4129
4493
|
} else if (partial) bindPatternFrom(frame, decl.name, partial);
|
|
4130
4494
|
}
|
|
@@ -4134,34 +4498,47 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4134
4498
|
}
|
|
4135
4499
|
if (frame.depth === 0) {
|
|
4136
4500
|
walk(body, (n) => {
|
|
4137
|
-
if (
|
|
4501
|
+
if (ts11.isPropertyAccessExpression(n) && ts11.isIdentifier(n.expression) && n.expression.text === "params") {
|
|
4138
4502
|
addInput("route_param", n.name.text, n, false);
|
|
4139
4503
|
}
|
|
4140
4504
|
return void 0;
|
|
4141
4505
|
});
|
|
4142
4506
|
}
|
|
4143
|
-
|
|
4507
|
+
}
|
|
4508
|
+
function analyzeFrame(p, frame, acc) {
|
|
4509
|
+
const { rel, sf, fn } = frame;
|
|
4510
|
+
const scope = scopeOf(p, frame.facts);
|
|
4511
|
+
const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
|
|
4512
|
+
const body = fn.body ?? fn;
|
|
4513
|
+
bindDeclarations(p, frame, acc);
|
|
4514
|
+
for (const call of collect(body, ts11.isCallExpression)) {
|
|
4144
4515
|
const callee = call.expression;
|
|
4145
|
-
if (!
|
|
4516
|
+
if (!ts11.isPropertyAccessExpression(callee) || !/^\$?transaction$/.test(callee.name.text))
|
|
4146
4517
|
continue;
|
|
4147
|
-
if (!
|
|
4518
|
+
if (!ts11.isIdentifier(callee.expression)) continue;
|
|
4148
4519
|
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
|
-
|
|
4520
|
+
const fn2 = call.arguments.map((a) => unwrap(a)).find((a) => ts11.isArrowFunction(a) || ts11.isFunctionExpression(a));
|
|
4521
|
+
const param = fn2 && (ts11.isArrowFunction(fn2) || ts11.isFunctionExpression(fn2)) ? fn2.parameters[0] : void 0;
|
|
4522
|
+
if (outer && param && ts11.isIdentifier(param.name)) frame.clients.set(param.name.text, outer);
|
|
4523
|
+
}
|
|
4524
|
+
const isSessionCall = (call) => /\.auth\.(getUser|getSession|getClaims)$/.test(call.expression.getText(sf)) || symOfCallee(p, call.expression, scope)?.kind === "auth";
|
|
4525
|
+
for (const call of collect(body, ts11.isCallExpression)) {
|
|
4526
|
+
if (isSessionCall(call)) acc.authChecks.push({ ...loc2(call), kind: "session" });
|
|
4527
|
+
}
|
|
4528
|
+
for (const gate of roleGatesIn(body, sessionNamesIn(body, isSessionCall))) {
|
|
4529
|
+
if (gate.exit === "return" && !frame.exitPropagates) continue;
|
|
4530
|
+
acc.roleChecks.push({
|
|
4531
|
+
...loc2(gate.node),
|
|
4532
|
+
source: gate.source,
|
|
4533
|
+
text: gate.node.expression.getText(sf).replace(/\s+/g, " ").slice(0, 160)
|
|
4534
|
+
});
|
|
4158
4535
|
}
|
|
4159
4536
|
for (const check of secretChecksIn(fn, sf)) {
|
|
4160
4537
|
acc.authChecks.push({ ...loc2(check.node), kind: "secret" });
|
|
4161
4538
|
}
|
|
4162
|
-
for (const pa of collect(body,
|
|
4539
|
+
for (const pa of collect(body, ts11.isPropertyAccessExpression)) {
|
|
4163
4540
|
const inner = pa.expression;
|
|
4164
|
-
if (
|
|
4541
|
+
if (ts11.isPropertyAccessExpression(inner) && (inner.name.text === "user_metadata" || inner.name.text === "app_metadata")) {
|
|
4165
4542
|
acc.metadataAccesses.push({
|
|
4166
4543
|
path: pa.getText(sf),
|
|
4167
4544
|
bucket: inner.name.text,
|
|
@@ -4173,15 +4550,15 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4173
4550
|
const seen = /* @__PURE__ */ new Set();
|
|
4174
4551
|
const clientOf = (root) => {
|
|
4175
4552
|
const u = unwrap(root);
|
|
4176
|
-
if (
|
|
4553
|
+
if (ts11.isIdentifier(u)) {
|
|
4177
4554
|
const b = frame.clients.get(u.text) ?? null;
|
|
4178
4555
|
return { binding: b, name: b ? null : u.text };
|
|
4179
4556
|
}
|
|
4180
|
-
if (
|
|
4557
|
+
if (ts11.isCallExpression(u)) {
|
|
4181
4558
|
const b = classifyCall(p, u, sf, scope, frame, 0);
|
|
4182
4559
|
return { binding: b, name: b ? null : u.expression.getText(sf) };
|
|
4183
4560
|
}
|
|
4184
|
-
if (
|
|
4561
|
+
if (ts11.isPropertyAccessExpression(u) && u.expression.kind === ts11.SyntaxKind.ThisKeyword) {
|
|
4185
4562
|
const b = frame.thisProps.get(u.name.text)?.client ?? null;
|
|
4186
4563
|
return { binding: b, name: b ? null : u.getText(sf) };
|
|
4187
4564
|
}
|
|
@@ -4190,11 +4567,11 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4190
4567
|
const tableSym = (e) => {
|
|
4191
4568
|
if (!e) return null;
|
|
4192
4569
|
const u = unwrap(e);
|
|
4193
|
-
if (
|
|
4570
|
+
if (ts11.isIdentifier(u)) {
|
|
4194
4571
|
const sym = scope.get(u.text);
|
|
4195
4572
|
return sym?.kind === "table" ? sym.table : null;
|
|
4196
4573
|
}
|
|
4197
|
-
if (
|
|
4574
|
+
if (ts11.isPropertyAccessExpression(u) && ts11.isIdentifier(u.expression)) {
|
|
4198
4575
|
const ns = scope.get(u.expression.text);
|
|
4199
4576
|
if (ns?.kind === "namespace") {
|
|
4200
4577
|
const sym = exportedSym(p, ns.facts, u.name.text, 0);
|
|
@@ -4251,7 +4628,46 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4251
4628
|
if (frame.via.length > 0) query.via = frame.via;
|
|
4252
4629
|
acc.queries.push(query);
|
|
4253
4630
|
};
|
|
4254
|
-
|
|
4631
|
+
const supabaseFilters = (segments) => {
|
|
4632
|
+
const filters = [];
|
|
4633
|
+
for (const s of segments) {
|
|
4634
|
+
if (!FILTER_METHODS.has(s.name)) continue;
|
|
4635
|
+
const firstArg = s.args[0];
|
|
4636
|
+
if (s.name === "match" && firstArg && ts11.isObjectLiteralExpression(firstArg)) {
|
|
4637
|
+
for (const pr of firstArg.properties) {
|
|
4638
|
+
if (ts11.isPropertyAssignment(pr)) {
|
|
4639
|
+
const f2 = {
|
|
4640
|
+
method: "match",
|
|
4641
|
+
column: pr.name.getText(sf).replace(/['"]/g, ""),
|
|
4642
|
+
valueText: pr.initializer.getText(sf),
|
|
4643
|
+
inputDerived: derivedIn(frame, pr.initializer)
|
|
4644
|
+
};
|
|
4645
|
+
filters.push(valued(f2, pr.initializer));
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
continue;
|
|
4649
|
+
}
|
|
4650
|
+
const val = s.args[1];
|
|
4651
|
+
const f = {
|
|
4652
|
+
method: s.name,
|
|
4653
|
+
column: stringLiteralValue(firstArg),
|
|
4654
|
+
valueText: val ? val.getText(sf) : "",
|
|
4655
|
+
inputDerived: val ? derivedIn(frame, val) : false
|
|
4656
|
+
};
|
|
4657
|
+
filters.push(valued(f, val));
|
|
4658
|
+
}
|
|
4659
|
+
return filters;
|
|
4660
|
+
};
|
|
4661
|
+
const builders = /* @__PURE__ */ new Map();
|
|
4662
|
+
const bindBuilder = (tail, query) => {
|
|
4663
|
+
const { node, parent } = outerOf(tail);
|
|
4664
|
+
if (parent && ts11.isVariableDeclaration(parent) && ts11.isIdentifier(parent.name)) {
|
|
4665
|
+
builders.set(parent.name.text, query);
|
|
4666
|
+
} else if (parent && ts11.isBinaryExpression(parent) && parent.operatorToken.kind === ts11.SyntaxKind.EqualsToken && parent.right === node && ts11.isIdentifier(parent.left)) {
|
|
4667
|
+
builders.set(parent.left.text, query);
|
|
4668
|
+
}
|
|
4669
|
+
};
|
|
4670
|
+
for (const call of collect(body, ts11.isCallExpression)) {
|
|
4255
4671
|
if (!isChainTail(call)) continue;
|
|
4256
4672
|
const chain = flattenChain(call);
|
|
4257
4673
|
const storageCall = storageCallOf(chain, storageHandles);
|
|
@@ -4260,6 +4676,18 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4260
4676
|
continue;
|
|
4261
4677
|
}
|
|
4262
4678
|
const root = unwrap(chain.root);
|
|
4679
|
+
const builder = ts11.isIdentifier(root) ? builders.get(root.text) : void 0;
|
|
4680
|
+
if (builder) {
|
|
4681
|
+
const condition = enclosingCondition(call, body);
|
|
4682
|
+
if (!condition || !derivedIn(frame, condition)) {
|
|
4683
|
+
const note = condition ? ` (when ${condition.getText(sf).replace(/\s+/g, " ")})` : "";
|
|
4684
|
+
for (const f of supabaseFilters(chain.segments)) {
|
|
4685
|
+
builder.filters.push(note ? { ...f, valueText: `${f.valueText}${note}` } : f);
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
bindBuilder(call, builder);
|
|
4689
|
+
continue;
|
|
4690
|
+
}
|
|
4263
4691
|
const first = chain.segments[0];
|
|
4264
4692
|
const fromIdx = chain.segments.findIndex((s) => s.name === "from");
|
|
4265
4693
|
const rpcIdx = chain.segments.findIndex((s) => s.name === "rpc");
|
|
@@ -4282,33 +4710,7 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4282
4710
|
break;
|
|
4283
4711
|
}
|
|
4284
4712
|
}
|
|
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
|
-
}
|
|
4713
|
+
const filters = supabaseFilters(after);
|
|
4312
4714
|
parsed = {
|
|
4313
4715
|
anchor: anchor.node,
|
|
4314
4716
|
table: fromTable ?? stringLiteralValue(anchor.args[0]) ?? "(dynamic)",
|
|
@@ -4337,7 +4739,7 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4337
4739
|
payload: payloadOf(payloadSeg?.args[0]),
|
|
4338
4740
|
clientRoot: chain.root
|
|
4339
4741
|
};
|
|
4340
|
-
} else if (first && DRIZZLE_QUERY_API.has(first.name) &&
|
|
4742
|
+
} else if (first && DRIZZLE_QUERY_API.has(first.name) && ts11.isPropertyAccessExpression(root) && ts11.isPropertyAccessExpression(root.expression) && root.expression.name.text === "query") {
|
|
4341
4743
|
const key = root.name.text;
|
|
4342
4744
|
parsed = {
|
|
4343
4745
|
anchor: first.node,
|
|
@@ -4349,7 +4751,7 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4349
4751
|
payload: null,
|
|
4350
4752
|
clientRoot: root.expression.expression
|
|
4351
4753
|
};
|
|
4352
|
-
} else if (first && PRISMA_OPS[first.name] &&
|
|
4754
|
+
} else if (first && PRISMA_OPS[first.name] && ts11.isPropertyAccessExpression(root) && ts11.isIdentifier(root.expression) && clientOf(root.expression).binding?.kind === "direct_db") {
|
|
4353
4755
|
const accessor = root.name.text;
|
|
4354
4756
|
const arg = first.args[0];
|
|
4355
4757
|
parsed = {
|
|
@@ -4382,6 +4784,13 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4382
4784
|
call,
|
|
4383
4785
|
chain.segments.map((s) => s.name)
|
|
4384
4786
|
);
|
|
4787
|
+
const checks = rowComparisons(call).filter((c) => c.exit === "throw" || frame.exitPropagates).map((c) => ({
|
|
4788
|
+
method: "compare",
|
|
4789
|
+
column: c.column,
|
|
4790
|
+
valueText: c.value.getText(sf).replace(/\s+/g, " "),
|
|
4791
|
+
inputDerived: derivedIn(frame, c.value)
|
|
4792
|
+
}));
|
|
4793
|
+
if (checks.length > 0) query.ownerChecks = checks;
|
|
4385
4794
|
acc.reads.push({
|
|
4386
4795
|
query,
|
|
4387
4796
|
keys: query.filters.map((f) => {
|
|
@@ -4389,76 +4798,151 @@ function analyzeFrame(p, frame, acc) {
|
|
|
4389
4798
|
return v ? valueKey(frame, v) : null;
|
|
4390
4799
|
}),
|
|
4391
4800
|
order: [...frame.pathPos, parsed.anchor.getStart(sf)],
|
|
4392
|
-
exits: rowExit === "throw" || rowExit === "return" && frame.exitPropagates
|
|
4801
|
+
exits: rowExit === "throw" || rowExit === "return" && frame.exitPropagates,
|
|
4802
|
+
checks
|
|
4393
4803
|
});
|
|
4804
|
+
bindBuilder(call, query);
|
|
4394
4805
|
}
|
|
4395
4806
|
if (query.filters.some((f) => f.inputDerived && isCredentialColumn(f.column))) {
|
|
4396
4807
|
acc.authChecks.push({ ...query.location, kind: "credential" });
|
|
4397
4808
|
}
|
|
4398
4809
|
}
|
|
4399
4810
|
if (frame.depth >= MAX_DEPTH) return;
|
|
4400
|
-
for (const call of collect(body,
|
|
4811
|
+
for (const call of collect(body, ts11.isCallExpression)) {
|
|
4401
4812
|
const target = callTarget(p, call, frame, scope);
|
|
4402
4813
|
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(",")}`;
|
|
4814
|
+
const child = childFrame(p, call, target, frame);
|
|
4815
|
+
if (!child) continue;
|
|
4816
|
+
const key = `${target.facts.file}#${target.name}#${frameSignature(child)}`;
|
|
4457
4817
|
if (acc.visited.has(key)) continue;
|
|
4458
4818
|
acc.visited.add(key);
|
|
4459
4819
|
analyzeFrame(p, child, acc);
|
|
4460
4820
|
}
|
|
4461
4821
|
}
|
|
4822
|
+
function childFrame(p, call, target, frame) {
|
|
4823
|
+
const tsf = p.sources.get(target.facts.file);
|
|
4824
|
+
if (!tsf) return null;
|
|
4825
|
+
const child = {
|
|
4826
|
+
rel: target.facts.file,
|
|
4827
|
+
sf: tsf,
|
|
4828
|
+
facts: target.facts,
|
|
4829
|
+
fn: target.fn,
|
|
4830
|
+
depth: frame.depth + 1,
|
|
4831
|
+
via: [...frame.via, `${target.name} (${target.facts.file}:${lineOf(tsf, target.fn)})`],
|
|
4832
|
+
inputNames: /* @__PURE__ */ new Set(),
|
|
4833
|
+
wholeNames: /* @__PURE__ */ new Set(),
|
|
4834
|
+
partialInputs: /* @__PURE__ */ new Map(),
|
|
4835
|
+
reqNames: /* @__PURE__ */ new Set(),
|
|
4836
|
+
clients: /* @__PURE__ */ new Map(),
|
|
4837
|
+
instances: /* @__PURE__ */ new Map(),
|
|
4838
|
+
cls: target.cls,
|
|
4839
|
+
thisProps: target.thisProps,
|
|
4840
|
+
key: `${target.facts.file}#${target.name}@${call.getStart(frame.sf)}`,
|
|
4841
|
+
aliases: /* @__PURE__ */ new Map(),
|
|
4842
|
+
pathPos: [...frame.pathPos, call.getStart(frame.sf)],
|
|
4843
|
+
exitPropagates: frame.exitPropagates && callResultChecked(call)
|
|
4844
|
+
};
|
|
4845
|
+
const cx = wholeContext(frame);
|
|
4846
|
+
target.fn.parameters.forEach((param, i) => {
|
|
4847
|
+
const arg = call.arguments[i];
|
|
4848
|
+
const ab = argBinding(p, arg, frame);
|
|
4849
|
+
const names = boundNames(param.name);
|
|
4850
|
+
const head = names[0];
|
|
4851
|
+
if (ab.client && head !== void 0 && ts11.isIdentifier(param.name)) {
|
|
4852
|
+
child.clients.set(head, ab.client);
|
|
4853
|
+
}
|
|
4854
|
+
if (ab.instance && head !== void 0 && ts11.isIdentifier(param.name)) {
|
|
4855
|
+
child.instances.set(head, ab.instance);
|
|
4856
|
+
}
|
|
4857
|
+
for (const nm of names) if (ab.isRequest) child.reqNames.add(nm);
|
|
4858
|
+
if (ab.tainted) {
|
|
4859
|
+
bindParamTaint(child, param.name, arg, frame);
|
|
4860
|
+
for (const nm of wholeParamNames(param.name, arg, cx)) {
|
|
4861
|
+
child.inputNames.add(nm);
|
|
4862
|
+
child.wholeNames.add(nm);
|
|
4863
|
+
}
|
|
4864
|
+
aliasParam(child, param.name, arg, frame);
|
|
4865
|
+
}
|
|
4866
|
+
});
|
|
4867
|
+
return child;
|
|
4868
|
+
}
|
|
4869
|
+
function frameSignature(child) {
|
|
4870
|
+
return [
|
|
4871
|
+
...[...child.clients].map(([n, c]) => `${n}=${c.kind}`),
|
|
4872
|
+
...[...child.inputNames].map((n) => `${n}!${child.wholeNames.has(n) ? "*" : ""}`),
|
|
4873
|
+
...[...child.partialInputs].map(([n, s]) => `${n}.{${[...s].sort().join("|")}}`),
|
|
4874
|
+
...[...child.reqNames].map((n) => `${n}?`),
|
|
4875
|
+
...[...child.thisProps].map(([n, b]) => `this.${n}=${b.client?.kind ?? "-"}`),
|
|
4876
|
+
...[...child.aliases].map(([n, k]) => `${n}~${k}`),
|
|
4877
|
+
`exit=${child.exitPropagates}`
|
|
4878
|
+
].sort().join(",");
|
|
4879
|
+
}
|
|
4880
|
+
var CLEAN = { tainted: false, props: null };
|
|
4881
|
+
var TAINTED = { tainted: true, props: null };
|
|
4882
|
+
var MAX_RETURN_DEPTH = 2;
|
|
4883
|
+
function branchesOf(e) {
|
|
4884
|
+
const u = unwrap(e);
|
|
4885
|
+
if (ts11.isConditionalExpression(u)) return [...branchesOf(u.whenTrue), ...branchesOf(u.whenFalse)];
|
|
4886
|
+
if (ts11.isBinaryExpression(u) && (u.operatorToken.kind === ts11.SyntaxKind.QuestionQuestionToken || u.operatorToken.kind === ts11.SyntaxKind.BarBarToken || u.operatorToken.kind === ts11.SyntaxKind.AmpersandAmpersandToken)) {
|
|
4887
|
+
return [...branchesOf(u.left), ...branchesOf(u.right)];
|
|
4888
|
+
}
|
|
4889
|
+
return [u];
|
|
4890
|
+
}
|
|
4891
|
+
function isLiteralValue(e) {
|
|
4892
|
+
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";
|
|
4893
|
+
}
|
|
4894
|
+
function mergeTaint(a, b) {
|
|
4895
|
+
if (!a.tainted) return b;
|
|
4896
|
+
if (!b.tainted) return a;
|
|
4897
|
+
if (a.props === null || b.props === null) return TAINTED;
|
|
4898
|
+
return { tainted: true, props: /* @__PURE__ */ new Set([...a.props, ...b.props]) };
|
|
4899
|
+
}
|
|
4900
|
+
function leafTaint(p, leaf, child, scope, depth) {
|
|
4901
|
+
if (isLiteralValue(leaf)) return CLEAN;
|
|
4902
|
+
if (ts11.isObjectLiteralExpression(leaf)) {
|
|
4903
|
+
const props = taintedProperties(child, leaf);
|
|
4904
|
+
if (props === null) return TAINTED;
|
|
4905
|
+
return props.size === 0 ? CLEAN : { tainted: true, props };
|
|
4906
|
+
}
|
|
4907
|
+
if (ts11.isCallExpression(leaf)) {
|
|
4908
|
+
if (isChainWithQuery(leaf) || isDbChain(leaf, child)) return CLEAN;
|
|
4909
|
+
if (returnsIdentity(p, leaf, scope)) return CLEAN;
|
|
4910
|
+
const nested = returnTaint(p, leaf, child, scope, depth + 1);
|
|
4911
|
+
if (nested !== null) return nested;
|
|
4912
|
+
}
|
|
4913
|
+
return derivedIn(child, leaf) ? TAINTED : CLEAN;
|
|
4914
|
+
}
|
|
4915
|
+
function returnTaint(p, call, frame, scope, depth) {
|
|
4916
|
+
if (depth > MAX_RETURN_DEPTH) return null;
|
|
4917
|
+
const target = callTarget(p, call, frame, scope);
|
|
4918
|
+
if (!target) return null;
|
|
4919
|
+
const child = childFrame(p, call, target, frame);
|
|
4920
|
+
if (!child) return null;
|
|
4921
|
+
const key = `${target.facts.file}#${target.name}#${frameSignature(child)}`;
|
|
4922
|
+
const cached = p.returnTaints.get(key);
|
|
4923
|
+
if (cached !== void 0) return cached;
|
|
4924
|
+
p.returnTaints.set(key, null);
|
|
4925
|
+
const scratch = {
|
|
4926
|
+
inputs: [],
|
|
4927
|
+
authChecks: [],
|
|
4928
|
+
roleChecks: [],
|
|
4929
|
+
queries: [],
|
|
4930
|
+
metadataAccesses: [],
|
|
4931
|
+
visited: /* @__PURE__ */ new Set(),
|
|
4932
|
+
reads: []
|
|
4933
|
+
};
|
|
4934
|
+
bindDeclarations(p, child, scratch);
|
|
4935
|
+
const childScope = scopeOf(p, child.facts);
|
|
4936
|
+
let out = CLEAN;
|
|
4937
|
+
for (const ret of ownReturns(target.fn)) {
|
|
4938
|
+
for (const leaf of branchesOf(ret)) {
|
|
4939
|
+
out = mergeTaint(out, leafTaint(p, leaf, child, childScope, depth));
|
|
4940
|
+
if (out.tainted && out.props === null) break;
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
p.returnTaints.set(key, out);
|
|
4944
|
+
return out;
|
|
4945
|
+
}
|
|
4462
4946
|
function pushQuery(acc, q) {
|
|
4463
4947
|
const same = acc.queries.find(
|
|
4464
4948
|
(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 +4963,8 @@ function pushQuery(acc, q) {
|
|
|
4479
4963
|
}
|
|
4480
4964
|
function dottedPath(e) {
|
|
4481
4965
|
const u = unwrap(e);
|
|
4482
|
-
if (
|
|
4483
|
-
if (
|
|
4966
|
+
if (ts11.isIdentifier(u)) return u.text;
|
|
4967
|
+
if (ts11.isPropertyAccessExpression(u)) {
|
|
4484
4968
|
const base = dottedPath(u.expression);
|
|
4485
4969
|
return base === null ? null : `${base}.${u.name.text}`;
|
|
4486
4970
|
}
|
|
@@ -4499,14 +4983,14 @@ function valueKey(frame, e) {
|
|
|
4499
4983
|
function aliasLocal(frame, name, init) {
|
|
4500
4984
|
const key = valueKey(frame, init);
|
|
4501
4985
|
if (key === null) return;
|
|
4502
|
-
if (
|
|
4986
|
+
if (ts11.isIdentifier(name)) {
|
|
4503
4987
|
frame.aliases.set(name.text, key);
|
|
4504
4988
|
return;
|
|
4505
4989
|
}
|
|
4506
|
-
if (!
|
|
4990
|
+
if (!ts11.isObjectBindingPattern(name)) return;
|
|
4507
4991
|
for (const el of name.elements) {
|
|
4508
4992
|
const k = propertyKeyOf(el.propertyName ?? el.name);
|
|
4509
|
-
if (k !== null && !el.dotDotDotToken &&
|
|
4993
|
+
if (k !== null && !el.dotDotDotToken && ts11.isIdentifier(el.name)) {
|
|
4510
4994
|
frame.aliases.set(el.name.text, `${key}.${k}`);
|
|
4511
4995
|
}
|
|
4512
4996
|
}
|
|
@@ -4514,51 +4998,102 @@ function aliasLocal(frame, name, init) {
|
|
|
4514
4998
|
function aliasParam(child, param, arg, frame) {
|
|
4515
4999
|
if (!arg) return;
|
|
4516
5000
|
const u = unwrap(arg);
|
|
4517
|
-
if (
|
|
5001
|
+
if (ts11.isIdentifier(param)) {
|
|
4518
5002
|
const key = valueKey(frame, arg);
|
|
4519
5003
|
if (key !== null) child.aliases.set(param.text, key);
|
|
4520
|
-
else if (
|
|
5004
|
+
else if (ts11.isObjectLiteralExpression(u)) {
|
|
4521
5005
|
for (const pr of u.properties) {
|
|
4522
5006
|
const k = propertyKeyOf(pr.name);
|
|
4523
|
-
const v =
|
|
5007
|
+
const v = ts11.isPropertyAssignment(pr) ? pr.initializer : ts11.isShorthandPropertyAssignment(pr) ? pr.name : void 0;
|
|
4524
5008
|
const vk = k !== null && v ? valueKey(frame, v) : null;
|
|
4525
5009
|
if (k !== null && vk !== null) child.aliases.set(`${param.text}.${k}`, vk);
|
|
4526
5010
|
}
|
|
4527
5011
|
}
|
|
4528
5012
|
return;
|
|
4529
5013
|
}
|
|
4530
|
-
if (!
|
|
5014
|
+
if (!ts11.isObjectBindingPattern(param)) return;
|
|
4531
5015
|
const base = valueKey(frame, arg);
|
|
4532
5016
|
for (const el of param.elements) {
|
|
4533
5017
|
const k = propertyKeyOf(el.propertyName ?? el.name);
|
|
4534
|
-
if (k === null || el.dotDotDotToken || !
|
|
5018
|
+
if (k === null || el.dotDotDotToken || !ts11.isIdentifier(el.name)) continue;
|
|
4535
5019
|
if (base !== null) {
|
|
4536
5020
|
child.aliases.set(el.name.text, `${base}.${k}`);
|
|
4537
5021
|
continue;
|
|
4538
5022
|
}
|
|
4539
|
-
if (!
|
|
5023
|
+
if (!ts11.isObjectLiteralExpression(u)) continue;
|
|
4540
5024
|
const pr = u.properties.find((x) => propertyKeyOf(x.name) === k);
|
|
4541
|
-
const v = pr &&
|
|
5025
|
+
const v = pr && ts11.isPropertyAssignment(pr) ? pr.initializer : pr && ts11.isShorthandPropertyAssignment(pr) ? pr.name : void 0;
|
|
4542
5026
|
const vk = v ? valueKey(frame, v) : null;
|
|
4543
5027
|
if (vk !== null) child.aliases.set(el.name.text, vk);
|
|
4544
5028
|
}
|
|
4545
5029
|
}
|
|
4546
|
-
function
|
|
4547
|
-
|
|
5030
|
+
function enclosingCondition(node, body) {
|
|
5031
|
+
let child = node;
|
|
5032
|
+
let cur = node.parent;
|
|
5033
|
+
while (cur && cur !== body && !isFunctionLikeNode(cur)) {
|
|
5034
|
+
if (ts11.isIfStatement(cur) && cur.expression !== child) return cur.expression;
|
|
5035
|
+
child = cur;
|
|
5036
|
+
cur = cur.parent;
|
|
5037
|
+
}
|
|
5038
|
+
return null;
|
|
5039
|
+
}
|
|
5040
|
+
var normalizeColumn = (c) => (c ?? "").toLowerCase().replace(/_/g, "");
|
|
5041
|
+
function singular(table) {
|
|
5042
|
+
if (table.endsWith("ies")) return `${table.slice(0, -3)}y`;
|
|
5043
|
+
if (table.endsWith("ses") || table.endsWith("xes")) return table.slice(0, -2);
|
|
5044
|
+
return table.endsWith("s") ? table.slice(0, -1) : table;
|
|
5045
|
+
}
|
|
5046
|
+
function columnNamesTable(column, table) {
|
|
5047
|
+
const c = normalizeColumn(column);
|
|
5048
|
+
if (!c.endsWith("id") || c === "id") return false;
|
|
5049
|
+
const stem = c.slice(0, -2);
|
|
5050
|
+
const t = normalizeColumn(table);
|
|
5051
|
+
return stem === t || stem === singular(t);
|
|
5052
|
+
}
|
|
5053
|
+
function guardMatch(q, g, tables) {
|
|
5054
|
+
const qTable = q.query.table.toLowerCase();
|
|
5055
|
+
const gTable = g.query.table.toLowerCase();
|
|
5056
|
+
const info = tables.get(qTable);
|
|
5057
|
+
for (let i = 0; i < q.query.filters.length; i += 1) {
|
|
5058
|
+
const f = q.query.filters[i];
|
|
5059
|
+
const k = q.keys[i];
|
|
5060
|
+
if (!f?.inputDerived || !f.column || k === null || k === void 0) continue;
|
|
5061
|
+
const j = g.keys.indexOf(k);
|
|
5062
|
+
const gf = j >= 0 ? g.query.filters[j] : void 0;
|
|
5063
|
+
if (!gf) continue;
|
|
5064
|
+
if (qTable === gTable) {
|
|
5065
|
+
if (normalizeColumn(gf.column) === normalizeColumn(f.column))
|
|
5066
|
+
return { read: g, column: f.column };
|
|
5067
|
+
continue;
|
|
5068
|
+
}
|
|
5069
|
+
const ref = info?.columnInfo?.find((c) => c.name === f.column?.toLowerCase())?.references;
|
|
5070
|
+
if (ref && ref.table === gTable && normalizeColumn(ref.column) === normalizeColumn(gf.column)) {
|
|
5071
|
+
return {
|
|
5072
|
+
read: g,
|
|
5073
|
+
column: f.column,
|
|
5074
|
+
parent: { table: g.query.table, column: gf.column ?? "id", how: "foreign key" }
|
|
5075
|
+
};
|
|
5076
|
+
}
|
|
5077
|
+
if (normalizeColumn(gf.column) === "id" && columnNamesTable(f.column, gTable)) {
|
|
5078
|
+
return {
|
|
5079
|
+
read: g,
|
|
5080
|
+
column: f.column,
|
|
5081
|
+
parent: { table: g.query.table, column: gf.column ?? "id", how: "column name" }
|
|
5082
|
+
};
|
|
5083
|
+
}
|
|
5084
|
+
}
|
|
5085
|
+
return null;
|
|
5086
|
+
}
|
|
5087
|
+
function linkGuards(reads, tables) {
|
|
4548
5088
|
for (const q of reads) {
|
|
4549
5089
|
let best = null;
|
|
4550
5090
|
for (const g2 of reads) {
|
|
4551
5091
|
if (g2 === q || !["select", "unknown"].includes(g2.query.operation)) continue;
|
|
4552
|
-
if (g2.query.table.toLowerCase() !== q.query.table.toLowerCase()) continue;
|
|
4553
5092
|
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 };
|
|
5093
|
+
const m = guardMatch(q, g2, tables);
|
|
5094
|
+
if (!m) continue;
|
|
5095
|
+
const stops = (r) => r.exits || r.checks.length > 0;
|
|
5096
|
+
if (!best || stops(m.read) && !stops(best.read)) best = m;
|
|
4562
5097
|
}
|
|
4563
5098
|
if (!best) continue;
|
|
4564
5099
|
const g = best.read.query;
|
|
@@ -4569,20 +5104,23 @@ function linkGuards(reads) {
|
|
|
4569
5104
|
clientName: g.clientName,
|
|
4570
5105
|
filters: g.filters,
|
|
4571
5106
|
column: best.column,
|
|
4572
|
-
|
|
5107
|
+
// A row that fails the comparison stops the entry point too; a missing one fails it as well.
|
|
5108
|
+
exitsWhenMissing: best.read.exits || best.read.checks.length > 0,
|
|
4573
5109
|
text: g.text,
|
|
4574
|
-
...g.via ? { via: g.via } : {}
|
|
5110
|
+
...g.via ? { via: g.via } : {},
|
|
5111
|
+
...best.read.checks.length > 0 ? { checks: best.read.checks } : {},
|
|
5112
|
+
...best.parent ? { parent: best.parent } : {}
|
|
4575
5113
|
};
|
|
4576
5114
|
}
|
|
4577
5115
|
}
|
|
4578
5116
|
function isChainWithQuery(e) {
|
|
4579
|
-
return
|
|
5117
|
+
return ts11.isCallExpression(e) && isQueryChain(e);
|
|
4580
5118
|
}
|
|
4581
5119
|
function isDbChain(call, frame) {
|
|
4582
5120
|
const root = unwrap(flattenChain(call).root);
|
|
4583
5121
|
let base = root;
|
|
4584
|
-
while (
|
|
4585
|
-
return
|
|
5122
|
+
while (ts11.isPropertyAccessExpression(base)) base = base.expression;
|
|
5123
|
+
return ts11.isIdentifier(base) && frame.clients.has(base.text);
|
|
4586
5124
|
}
|
|
4587
5125
|
var IDENTITY_CALLEE = /\.auth\.|user|session|claims|auth|principal|viewer/i;
|
|
4588
5126
|
function returnsIdentity(p, call, scope) {
|
|
@@ -4591,10 +5129,10 @@ function returnsIdentity(p, call, scope) {
|
|
|
4591
5129
|
}
|
|
4592
5130
|
function calleePath(e) {
|
|
4593
5131
|
const u = unwrap(e);
|
|
4594
|
-
if (
|
|
4595
|
-
if (
|
|
4596
|
-
if (
|
|
4597
|
-
if (
|
|
5132
|
+
if (ts11.isIdentifier(u)) return u.text;
|
|
5133
|
+
if (ts11.isPropertyAccessExpression(u)) return `${calleePath(u.expression)}.${u.name.text}`;
|
|
5134
|
+
if (ts11.isCallExpression(u)) return `${calleePath(u.expression)}()`;
|
|
5135
|
+
if (ts11.isElementAccessExpression(u)) return `${calleePath(u.expression)}[]`;
|
|
4598
5136
|
return "";
|
|
4599
5137
|
}
|
|
4600
5138
|
function analyzeHandler(p, h) {
|
|
@@ -4603,6 +5141,7 @@ function analyzeHandler(p, h) {
|
|
|
4603
5141
|
const acc = {
|
|
4604
5142
|
inputs: [],
|
|
4605
5143
|
authChecks: [],
|
|
5144
|
+
roleChecks: [],
|
|
4606
5145
|
queries: [],
|
|
4607
5146
|
metadataAccesses: [],
|
|
4608
5147
|
visited: /* @__PURE__ */ new Set(),
|
|
@@ -4632,7 +5171,7 @@ function analyzeHandler(p, h) {
|
|
|
4632
5171
|
};
|
|
4633
5172
|
const first = fn.parameters[0];
|
|
4634
5173
|
if (h.kind === "route" && first) {
|
|
4635
|
-
if (
|
|
5174
|
+
if (ts11.isIdentifier(first.name)) frame.reqNames.add(first.name.text);
|
|
4636
5175
|
else for (const nm of boundNames(first.name)) if (REQUEST_NAME.test(nm)) frame.reqNames.add(nm);
|
|
4637
5176
|
}
|
|
4638
5177
|
if (h.kind === "server_action") {
|
|
@@ -4651,7 +5190,7 @@ function analyzeHandler(p, h) {
|
|
|
4651
5190
|
acc.authChecks.push({ ...loc2(h.node), kind: "session" });
|
|
4652
5191
|
}
|
|
4653
5192
|
analyzeFrame(p, frame, acc);
|
|
4654
|
-
linkGuards(acc.reads);
|
|
5193
|
+
linkGuards(acc.reads, p.tables);
|
|
4655
5194
|
const entry = h.kind === "route" ? `${h.method} ${h.route}` : h.kind === "page" ? `PAGE ${h.route}` : `server action ${h.route}`;
|
|
4656
5195
|
const stmt = enclosingStatement(h.node);
|
|
4657
5196
|
const ignores = parseIgnoreDirectives(sf, stmt.getFullStart()).map((d) => ({
|
|
@@ -4669,19 +5208,20 @@ function analyzeHandler(p, h) {
|
|
|
4669
5208
|
authChecks: acc.authChecks,
|
|
4670
5209
|
queries: acc.queries,
|
|
4671
5210
|
metadataAccesses: acc.metadataAccesses,
|
|
4672
|
-
ignores
|
|
5211
|
+
ignores,
|
|
5212
|
+
roleChecks: acc.roleChecks
|
|
4673
5213
|
};
|
|
4674
5214
|
}
|
|
4675
5215
|
function publicSecretEnvReads(sf) {
|
|
4676
|
-
const isProcessEnv = (e) =>
|
|
5216
|
+
const isProcessEnv = (e) => ts11.isPropertyAccessExpression(e) && e.name.text === "env" && ts11.isIdentifier(e.expression) && e.expression.text === "process";
|
|
4677
5217
|
const out = [];
|
|
4678
5218
|
const visit = (n) => {
|
|
4679
|
-
if (
|
|
5219
|
+
if (ts11.isPropertyAccessExpression(n) && isProcessEnv(n.expression)) {
|
|
4680
5220
|
if (PUBLIC_SECRET_ENV.test(n.name.text)) out.push({ name: n.name.text, node: n });
|
|
4681
|
-
} else if (
|
|
5221
|
+
} else if (ts11.isElementAccessExpression(n) && isProcessEnv(n.expression) && ts11.isStringLiteralLike(n.argumentExpression) && PUBLIC_SECRET_ENV.test(n.argumentExpression.text)) {
|
|
4682
5222
|
out.push({ name: n.argumentExpression.text, node: n });
|
|
4683
5223
|
}
|
|
4684
|
-
|
|
5224
|
+
ts11.forEachChild(n, visit);
|
|
4685
5225
|
};
|
|
4686
5226
|
visit(sf);
|
|
4687
5227
|
return out;
|
|
@@ -4696,7 +5236,7 @@ function findExposures(rel, sf) {
|
|
|
4696
5236
|
});
|
|
4697
5237
|
}
|
|
4698
5238
|
if (isClientComponentFile(sf)) {
|
|
4699
|
-
for (const call of collect(sf,
|
|
5239
|
+
for (const call of collect(sf, ts11.isCallExpression)) {
|
|
4700
5240
|
if (!isCreateClientCall(call, sf)) continue;
|
|
4701
5241
|
const c = classifyCreateClientCall(call, sf);
|
|
4702
5242
|
if (c.kind === "service_role") {
|
|
@@ -4727,7 +5267,14 @@ function parseProject(rootInput, opts = {}) {
|
|
|
4727
5267
|
}
|
|
4728
5268
|
}
|
|
4729
5269
|
const registry = /* @__PURE__ */ new Map();
|
|
4730
|
-
for (const [rel, sf] of sources)
|
|
5270
|
+
for (const [rel, sf] of sources) {
|
|
5271
|
+
try {
|
|
5272
|
+
registry.set(rel, analyzeModule(rel, sf));
|
|
5273
|
+
} catch (e) {
|
|
5274
|
+
sources.delete(rel);
|
|
5275
|
+
warnings.push(`could not analyse ${rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
4731
5278
|
const tables = /* @__PURE__ */ new Map();
|
|
4732
5279
|
for (const rel of sql) {
|
|
4733
5280
|
try {
|
|
@@ -4759,12 +5306,46 @@ function parseProject(rootInput, opts = {}) {
|
|
|
4759
5306
|
varBindings: /* @__PURE__ */ new Map(),
|
|
4760
5307
|
drizzleTablesByExport,
|
|
4761
5308
|
prismaModels,
|
|
5309
|
+
returnTaints: /* @__PURE__ */ new Map(),
|
|
5310
|
+
tables,
|
|
4762
5311
|
warnings
|
|
4763
5312
|
};
|
|
4764
5313
|
const routes = [];
|
|
4765
5314
|
const exposures = [];
|
|
4766
5315
|
const fileIgnores = {};
|
|
4767
5316
|
for (const [rel, sf] of sources) {
|
|
5317
|
+
try {
|
|
5318
|
+
analyzeFile(project, rel, sf, registry.get(rel) ?? analyzeModule(rel, sf), {
|
|
5319
|
+
routes,
|
|
5320
|
+
exposures,
|
|
5321
|
+
fileIgnores
|
|
5322
|
+
});
|
|
5323
|
+
} catch (e) {
|
|
5324
|
+
warnings.push(`could not analyse ${rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
routes.sort((a, b) => a.entry.localeCompare(b.entry));
|
|
5328
|
+
const all = [...registry.values()];
|
|
5329
|
+
const schema = sqlSchemaFor(tables);
|
|
5330
|
+
warnings.push(...schema.warnings);
|
|
5331
|
+
return {
|
|
5332
|
+
root,
|
|
5333
|
+
files: [...source, ...sql],
|
|
5334
|
+
routes,
|
|
5335
|
+
clientFactories: all.flatMap((f) => f.clientFactories),
|
|
5336
|
+
authHelpers: all.flatMap((f) => f.authHelpers),
|
|
5337
|
+
tables: [...tables.values()],
|
|
5338
|
+
exposures,
|
|
5339
|
+
fileIgnores,
|
|
5340
|
+
warnings,
|
|
5341
|
+
enums: schema.enums,
|
|
5342
|
+
sqlFunctions: schema.sqlFunctions,
|
|
5343
|
+
storageBuckets: schema.storageBuckets
|
|
5344
|
+
};
|
|
5345
|
+
}
|
|
5346
|
+
function analyzeFile(project, rel, sf, facts, out) {
|
|
5347
|
+
const { routes, exposures, fileIgnores } = out;
|
|
5348
|
+
{
|
|
4768
5349
|
const top = parseIgnoreDirectives(sf, 0).map((d) => ({
|
|
4769
5350
|
ruleId: d.ruleId,
|
|
4770
5351
|
reason: d.reason,
|
|
@@ -4772,7 +5353,6 @@ function parseProject(rootInput, opts = {}) {
|
|
|
4772
5353
|
}));
|
|
4773
5354
|
if (top.length > 0) fileIgnores[rel] = top;
|
|
4774
5355
|
exposures.push(...findExposures(rel, sf));
|
|
4775
|
-
const facts = registry.get(rel) ?? analyzeModule(rel, sf);
|
|
4776
5356
|
const route = routeFromFile(rel);
|
|
4777
5357
|
if (route) {
|
|
4778
5358
|
for (const { method, exported } of routeHandlersIn(sf)) {
|
|
@@ -4825,23 +5405,6 @@ function parseProject(rootInput, opts = {}) {
|
|
|
4825
5405
|
}
|
|
4826
5406
|
}
|
|
4827
5407
|
}
|
|
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
5408
|
}
|
|
4846
5409
|
|
|
4847
5410
|
// packages/rules/src/packs/sql-functions.ts
|
|
@@ -4885,16 +5448,16 @@ var SCOPE_COLUMNS = /* @__PURE__ */ new Set([
|
|
|
4885
5448
|
"author_id",
|
|
4886
5449
|
"profile_id"
|
|
4887
5450
|
]);
|
|
4888
|
-
function
|
|
5451
|
+
function normalizeColumn2(column) {
|
|
4889
5452
|
return column.toLowerCase().replace(/_/g, "");
|
|
4890
5453
|
}
|
|
4891
|
-
var SCOPE_KEYS = new Set([...SCOPE_COLUMNS].map(
|
|
5454
|
+
var SCOPE_KEYS = new Set([...SCOPE_COLUMNS].map(normalizeColumn2));
|
|
4892
5455
|
function isScopeColumn(column) {
|
|
4893
|
-
return column !== null && SCOPE_KEYS.has(
|
|
5456
|
+
return column !== null && SCOPE_KEYS.has(normalizeColumn2(column));
|
|
4894
5457
|
}
|
|
4895
5458
|
function isObjectIdColumn(column) {
|
|
4896
5459
|
if (column === null || isScopeColumn(column)) return false;
|
|
4897
|
-
const c =
|
|
5460
|
+
const c = normalizeColumn2(column);
|
|
4898
5461
|
return c === "id" || c === "uuid" || c === "slug" || c.endsWith("id");
|
|
4899
5462
|
}
|
|
4900
5463
|
function bypassesRls(kind) {
|
|
@@ -4925,7 +5488,8 @@ function handlerViews(ctx) {
|
|
|
4925
5488
|
data,
|
|
4926
5489
|
inputs: data.inputs ?? [],
|
|
4927
5490
|
authenticated: kinds.length > 0,
|
|
4928
|
-
operatorOnly: kinds.length > 0 && kinds.every((k) => k === "secret")
|
|
5491
|
+
operatorOnly: kinds.length > 0 && kinds.every((k) => k === "secret"),
|
|
5492
|
+
roleChecks: data.roleChecks ?? []
|
|
4929
5493
|
};
|
|
4930
5494
|
});
|
|
4931
5495
|
}
|
|
@@ -4955,12 +5519,12 @@ function rlsNote(t, tableName, kind) {
|
|
|
4955
5519
|
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
5520
|
return `RLS is enabled on public.${tableName} with ${t.policies.length} polic${t.policies.length === 1 ? "y" : "ies"}, but ${bypass}.`;
|
|
4957
5521
|
}
|
|
4958
|
-
function finding(ctx, rule, partial) {
|
|
5522
|
+
function finding(ctx, rule, partial, severity) {
|
|
4959
5523
|
return {
|
|
4960
5524
|
id: ctx.nextId(),
|
|
4961
5525
|
ruleId: rule.id,
|
|
4962
5526
|
status: "likely",
|
|
4963
|
-
severity: rule.severity,
|
|
5527
|
+
severity: severity ?? rule.severity,
|
|
4964
5528
|
confidence: rule.confidence,
|
|
4965
5529
|
cwe: rule.cwe,
|
|
4966
5530
|
createdAt: ctx.now,
|
|
@@ -4968,6 +5532,39 @@ function finding(ctx, rule, partial) {
|
|
|
4968
5532
|
...partial
|
|
4969
5533
|
};
|
|
4970
5534
|
}
|
|
5535
|
+
function anonReadPolicy(t) {
|
|
5536
|
+
if (!t?.known || !t.rlsEnabled) return void 0;
|
|
5537
|
+
return t.policyDetails.find(
|
|
5538
|
+
(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"))
|
|
5539
|
+
);
|
|
5540
|
+
}
|
|
5541
|
+
function publicReadNote(p, table) {
|
|
5542
|
+
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.`;
|
|
5543
|
+
}
|
|
5544
|
+
function adminOnly(ctx, h, t) {
|
|
5545
|
+
const check = h.roleChecks[0];
|
|
5546
|
+
if (!check || !t?.known || !singleTenantTable(ctx, t.table)) return void 0;
|
|
5547
|
+
return check;
|
|
5548
|
+
}
|
|
5549
|
+
function singleTenantTable(ctx, table) {
|
|
5550
|
+
const info = ctx.model.tables.find((x) => x.table === table.toLowerCase());
|
|
5551
|
+
if (!info || info.columns.some((c) => isScopeColumn(c))) return false;
|
|
5552
|
+
for (const col of info.columnInfo ?? []) {
|
|
5553
|
+
if (!col.references || col.nullable) continue;
|
|
5554
|
+
const parent = ctx.model.tables.find((x) => x.table === col.references?.table);
|
|
5555
|
+
if (parent?.columns.some((c) => isScopeColumn(c))) return false;
|
|
5556
|
+
}
|
|
5557
|
+
return true;
|
|
5558
|
+
}
|
|
5559
|
+
function adminOnlyNote(check, table) {
|
|
5560
|
+
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.`;
|
|
5561
|
+
}
|
|
5562
|
+
function tableDataOf(ctx, table) {
|
|
5563
|
+
return ctx.graph.nodes.get(`table:${table}`)?.data;
|
|
5564
|
+
}
|
|
5565
|
+
function callerCheck(checks) {
|
|
5566
|
+
return checks?.find((c) => isScopeColumn(c.column) && !c.inputDerived);
|
|
5567
|
+
}
|
|
4971
5568
|
function guardTiesRowToCaller(guard, table, callerFns) {
|
|
4972
5569
|
const callerFilter = guard.filters.find((f) => isScopeColumn(f.column) && !f.inputDerived);
|
|
4973
5570
|
if (callerFilter) {
|
|
@@ -4977,6 +5574,14 @@ function guardTiesRowToCaller(guard, table, callerFns) {
|
|
|
4977
5574
|
why: ""
|
|
4978
5575
|
};
|
|
4979
5576
|
}
|
|
5577
|
+
const compared = callerCheck(guard.checks);
|
|
5578
|
+
if (compared) {
|
|
5579
|
+
return {
|
|
5580
|
+
tied: true,
|
|
5581
|
+
how: `its ${compared.column} compared with ${compared.valueText} in code, stopping otherwise`,
|
|
5582
|
+
why: ""
|
|
5583
|
+
};
|
|
5584
|
+
}
|
|
4980
5585
|
if (guard.client !== "user_scoped") {
|
|
4981
5586
|
return {
|
|
4982
5587
|
tied: false,
|
|
@@ -5017,12 +5622,19 @@ var serviceRoleObjectAccessWithoutTenantScope = {
|
|
|
5017
5622
|
const filters = q.filters;
|
|
5018
5623
|
const idFilter = filters.find((f) => f.inputDerived && isObjectIdColumn(f.column));
|
|
5019
5624
|
if (!idFilter || filters.some((f) => isScopeColumn(f.column))) continue;
|
|
5625
|
+
if (q.operation === "select" && callerCheck(q.ownerChecks)) continue;
|
|
5020
5626
|
const guard = q.guard;
|
|
5021
|
-
const
|
|
5627
|
+
const guardTable = guard?.parent ? tableDataOf(ctx, guard.table) : v.tableData;
|
|
5628
|
+
const tied = guard ? guardTiesRowToCaller(guard, guardTable, callerFns) : null;
|
|
5022
5629
|
if (guard && tied?.tied && guard.exitsWhenMissing) continue;
|
|
5023
|
-
const
|
|
5630
|
+
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}"`;
|
|
5631
|
+
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
5632
|
const tableName = v.tableData?.table ?? q.table;
|
|
5025
5633
|
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.";
|
|
5634
|
+
const admin = adminOnly(ctx, h, v.tableData);
|
|
5635
|
+
const anonPolicy = q.operation === "select" ? anonReadPolicy(v.tableData) : void 0;
|
|
5636
|
+
const downgrade = admin || anonPolicy ? "medium" : void 0;
|
|
5637
|
+
const downgradeNote = (admin ? adminOnlyNote(admin, tableName) : "") + (anonPolicy ? publicReadNote(anonPolicy, tableName) : "");
|
|
5026
5638
|
const path = [
|
|
5027
5639
|
h.data.kind === "server_action" ? "Server action call" : "HTTP request",
|
|
5028
5640
|
h.data.entry,
|
|
@@ -5034,26 +5646,40 @@ var serviceRoleObjectAccessWithoutTenantScope = {
|
|
|
5034
5646
|
const evidence = [
|
|
5035
5647
|
{
|
|
5036
5648
|
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(
|
|
5649
|
+
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}`,
|
|
5650
|
+
locations: locations(
|
|
5651
|
+
h.handler.location,
|
|
5652
|
+
v.query.location,
|
|
5653
|
+
v.client?.location,
|
|
5654
|
+
admin ? { file: admin.file, line: admin.line } : void 0,
|
|
5655
|
+
anonPolicy?.location
|
|
5656
|
+
),
|
|
5039
5657
|
data: {
|
|
5040
5658
|
deterministic: false,
|
|
5041
5659
|
ruleId: this.id,
|
|
5042
5660
|
authenticated: h.authenticated,
|
|
5043
|
-
query: q.text
|
|
5661
|
+
query: q.text,
|
|
5662
|
+
...admin ? { adminOnly: true, roleCheck: admin.source } : {},
|
|
5663
|
+
...anonPolicy ? { anonReadPolicy: anonPolicy.name } : {}
|
|
5044
5664
|
}
|
|
5045
5665
|
},
|
|
5046
5666
|
{ kind: "trace", summary: path.join(" -> ") }
|
|
5047
5667
|
];
|
|
5668
|
+
const who = admin ? "Admin-only" : h.authenticated ? "Cross-tenant" : "Unauthenticated";
|
|
5048
5669
|
out.push(
|
|
5049
|
-
finding(
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5670
|
+
finding(
|
|
5671
|
+
ctx,
|
|
5672
|
+
this,
|
|
5673
|
+
{
|
|
5674
|
+
title: `${who} ${q.operation} on "${tableName}" via ${clientNoun(v.clientData)}${admin ? " (verify the admin check)" : ""}`,
|
|
5675
|
+
entrypoints: [h.data.entry],
|
|
5676
|
+
sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
|
|
5677
|
+
sinks: [`supabase.${q.operation}:public.${tableName}`],
|
|
5678
|
+
path,
|
|
5679
|
+
evidence
|
|
5680
|
+
},
|
|
5681
|
+
downgrade
|
|
5682
|
+
)
|
|
5057
5683
|
);
|
|
5058
5684
|
}
|
|
5059
5685
|
}
|
|
@@ -5170,7 +5796,13 @@ var rlsPolicyWithoutCallerPredicate = {
|
|
|
5170
5796
|
],
|
|
5171
5797
|
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
5798
|
title: `RLS policy "${p.name}" on "${t.table}" does not scope rows to the caller`,
|
|
5173
|
-
data: {
|
|
5799
|
+
data: {
|
|
5800
|
+
deterministic: false,
|
|
5801
|
+
ruleId: this.id,
|
|
5802
|
+
policy: p.name,
|
|
5803
|
+
command: p.command,
|
|
5804
|
+
table: t.table
|
|
5805
|
+
},
|
|
5174
5806
|
tail: locations(p.location)
|
|
5175
5807
|
}));
|
|
5176
5808
|
addReach(g, h, v, `supabase.${op}:public.${t.table}`);
|
|
@@ -5331,6 +5963,14 @@ var serviceRoleQueryWithoutAuthentication = {
|
|
|
5331
5963
|
const v = views[0];
|
|
5332
5964
|
if (!v) continue;
|
|
5333
5965
|
const tables = [...new Set(views.map((x) => x.tableData?.table ?? x.data.table))];
|
|
5966
|
+
const anonPolicies = views.map(
|
|
5967
|
+
(x) => x.data.operation === "select" ? anonReadPolicy(x.tableData) : void 0
|
|
5968
|
+
);
|
|
5969
|
+
const allPublicReads = anonPolicies.every((x) => x !== void 0);
|
|
5970
|
+
const publicNote = allPublicReads ? views.map((x, i) => {
|
|
5971
|
+
const pol = anonPolicies[i];
|
|
5972
|
+
return pol ? publicReadNote(pol, x.tableData?.table ?? x.data.table) : "";
|
|
5973
|
+
}).join("") : "";
|
|
5334
5974
|
const path = [
|
|
5335
5975
|
h.data.kind === "server_action" ? "Server action call" : "HTTP request",
|
|
5336
5976
|
h.data.entry,
|
|
@@ -5339,24 +5979,37 @@ var serviceRoleQueryWithoutAuthentication = {
|
|
|
5339
5979
|
`public.${tables.join(", public.")}`
|
|
5340
5980
|
];
|
|
5341
5981
|
out.push(
|
|
5342
|
-
finding(
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5982
|
+
finding(
|
|
5983
|
+
ctx,
|
|
5984
|
+
this,
|
|
5985
|
+
{
|
|
5986
|
+
title: `Unauthenticated ${views.some((v2) => v2.clientData?.kind === "direct_db") ? "database" : "service-role"} access to "${tables.join('", "')}" in ${h.data.entry}`,
|
|
5987
|
+
entrypoints: [h.data.entry],
|
|
5988
|
+
sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
|
|
5989
|
+
sinks: views.map(
|
|
5990
|
+
(x) => `supabase.${x.data.operation}:public.${x.tableData?.table ?? x.data.table}`
|
|
5991
|
+
),
|
|
5992
|
+
path,
|
|
5993
|
+
evidence: [
|
|
5994
|
+
{
|
|
5995
|
+
kind: "rule",
|
|
5996
|
+
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}`,
|
|
5997
|
+
locations: locations(
|
|
5998
|
+
h.handler.location,
|
|
5999
|
+
...views.map((x) => x.query.location),
|
|
6000
|
+
...anonPolicies.map((x) => x?.location)
|
|
6001
|
+
),
|
|
6002
|
+
data: {
|
|
6003
|
+
deterministic: false,
|
|
6004
|
+
ruleId: this.id,
|
|
6005
|
+
...allPublicReads ? { anonReadPolicies: anonPolicies.map((x) => x?.name ?? "") } : {}
|
|
6006
|
+
}
|
|
6007
|
+
},
|
|
6008
|
+
{ kind: "trace", summary: path.join(" -> ") }
|
|
6009
|
+
]
|
|
6010
|
+
},
|
|
6011
|
+
allPublicReads ? "medium" : void 0
|
|
6012
|
+
)
|
|
5360
6013
|
);
|
|
5361
6014
|
}
|
|
5362
6015
|
return out;
|
|
@@ -5751,8 +6404,40 @@ function runRules(rules, model, graph, opts = {}) {
|
|
|
5751
6404
|
}
|
|
5752
6405
|
}
|
|
5753
6406
|
findings = applySuppressions(findings, model, now);
|
|
6407
|
+
findings = applyPublicTables(findings, opts.publicTables ?? [], now);
|
|
5754
6408
|
return findings;
|
|
5755
6409
|
}
|
|
6410
|
+
var PUBLIC_TABLE_RULES = /* @__PURE__ */ new Set([
|
|
6411
|
+
"supabase.service-role-query-without-authentication",
|
|
6412
|
+
"supabase.rls-policy-without-caller-predicate"
|
|
6413
|
+
]);
|
|
6414
|
+
var READ_SINK = /^supabase\.select:public\.(.+)$/;
|
|
6415
|
+
function applyPublicTables(findings, publicTables, now) {
|
|
6416
|
+
if (publicTables.length === 0) return findings;
|
|
6417
|
+
const declared = new Set(publicTables.map((t) => t.toLowerCase()));
|
|
6418
|
+
return findings.map((f) => {
|
|
6419
|
+
if (f.status === "suppressed" || !PUBLIC_TABLE_RULES.has(f.ruleId)) return f;
|
|
6420
|
+
if (f.sinks.length === 0) return f;
|
|
6421
|
+
const tables = [];
|
|
6422
|
+
for (const sink of f.sinks) {
|
|
6423
|
+
const table = READ_SINK.exec(sink)?.[1];
|
|
6424
|
+
if (table === void 0 || !declared.has(table.toLowerCase())) return f;
|
|
6425
|
+
if (!tables.includes(table)) tables.push(table);
|
|
6426
|
+
}
|
|
6427
|
+
const command = f.evidence[0]?.data?.command;
|
|
6428
|
+
if (f.ruleId === "supabase.rls-policy-without-caller-predicate" && command !== "select") {
|
|
6429
|
+
return f;
|
|
6430
|
+
}
|
|
6431
|
+
return transition(f, "suppressed", {
|
|
6432
|
+
evidence: {
|
|
6433
|
+
kind: "rule",
|
|
6434
|
+
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.`,
|
|
6435
|
+
data: { suppressed: true, ruleId: f.ruleId, publicTables: tables }
|
|
6436
|
+
},
|
|
6437
|
+
now
|
|
6438
|
+
});
|
|
6439
|
+
});
|
|
6440
|
+
}
|
|
5756
6441
|
function applySuppressions(findings, model, now) {
|
|
5757
6442
|
const byEntry = new Map(model.routes.map((h) => [h.entry, h.ignores]));
|
|
5758
6443
|
return findings.map((f) => {
|
|
@@ -5783,6 +6468,8 @@ var defaultRules = [
|
|
|
5783
6468
|
// packages/scanner/src/config.ts
|
|
5784
6469
|
import { existsSync, readFileSync as readFileSync3, realpathSync, statSync } from "node:fs";
|
|
5785
6470
|
import { isAbsolute, join as join4, resolve as resolve3, sep as sep2 } from "node:path";
|
|
6471
|
+
var MAX_PUBLIC_TABLES = 64;
|
|
6472
|
+
var TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5786
6473
|
var MAX_REPO_MIGRATION_DIRS = 16;
|
|
5787
6474
|
function shown2(value) {
|
|
5788
6475
|
return JSON.stringify(value.length > 120 ? `${value.slice(0, 120)}...` : value);
|
|
@@ -5808,14 +6495,42 @@ function loadAuditConfig(root) {
|
|
|
5808
6495
|
const obj = raw;
|
|
5809
6496
|
const ignore = strings(obj.ignore);
|
|
5810
6497
|
const migrations = strings(obj.migrations);
|
|
6498
|
+
const publicTables = publicTableNames(obj.publicTables);
|
|
5811
6499
|
return {
|
|
5812
6500
|
config: {
|
|
5813
6501
|
...ignore ? { ignore } : {},
|
|
5814
|
-
...migrations ? { migrations } : {}
|
|
6502
|
+
...migrations ? { migrations } : {},
|
|
6503
|
+
...publicTables.tables ? { publicTables: publicTables.tables } : {}
|
|
5815
6504
|
},
|
|
5816
|
-
warnings:
|
|
6505
|
+
warnings: publicTables.warnings
|
|
5817
6506
|
};
|
|
5818
6507
|
}
|
|
6508
|
+
function publicTableNames(value) {
|
|
6509
|
+
const warnings = [];
|
|
6510
|
+
if (value === void 0) return { warnings };
|
|
6511
|
+
if (!Array.isArray(value)) {
|
|
6512
|
+
return { warnings: ["audit.config.json: publicTables ignored (not an array of table names)"] };
|
|
6513
|
+
}
|
|
6514
|
+
const tables = [];
|
|
6515
|
+
for (const entry of value) {
|
|
6516
|
+
if (typeof entry !== "string" || !TABLE_NAME.test(entry.trim())) {
|
|
6517
|
+
warnings.push(
|
|
6518
|
+
`audit.config.json: ignored publicTables entry ${shown2(String(entry))} (not a table name)`
|
|
6519
|
+
);
|
|
6520
|
+
continue;
|
|
6521
|
+
}
|
|
6522
|
+
const name = entry.trim().toLowerCase();
|
|
6523
|
+
if (tables.includes(name)) continue;
|
|
6524
|
+
if (tables.length >= MAX_PUBLIC_TABLES) {
|
|
6525
|
+
warnings.push(
|
|
6526
|
+
`audit.config.json: ignored publicTables entry ${shown2(entry)} (at most ${MAX_PUBLIC_TABLES} tables)`
|
|
6527
|
+
);
|
|
6528
|
+
continue;
|
|
6529
|
+
}
|
|
6530
|
+
tables.push(name);
|
|
6531
|
+
}
|
|
6532
|
+
return { tables, warnings };
|
|
6533
|
+
}
|
|
5819
6534
|
function inside(base, p) {
|
|
5820
6535
|
return p === base || p.startsWith(base.endsWith(sep2) ? base : `${base}${sep2}`);
|
|
5821
6536
|
}
|
|
@@ -5881,7 +6596,28 @@ function repoMigrationDirs(root, entries) {
|
|
|
5881
6596
|
}
|
|
5882
6597
|
|
|
5883
6598
|
// packages/scanner/src/scan.ts
|
|
5884
|
-
|
|
6599
|
+
var ScanError = class extends Error {
|
|
6600
|
+
constructor(code, path, message) {
|
|
6601
|
+
super(message);
|
|
6602
|
+
this.code = code;
|
|
6603
|
+
this.path = path;
|
|
6604
|
+
}
|
|
6605
|
+
code;
|
|
6606
|
+
path;
|
|
6607
|
+
name = "ScanError";
|
|
6608
|
+
};
|
|
6609
|
+
function ensureDirectory(path) {
|
|
6610
|
+
let isDirectory;
|
|
6611
|
+
try {
|
|
6612
|
+
isDirectory = statSync2(path).isDirectory();
|
|
6613
|
+
} catch (e) {
|
|
6614
|
+
const code = errorCode2(e);
|
|
6615
|
+
const why = code === "ENOENT" ? "no such file or directory" : code;
|
|
6616
|
+
throw new ScanError("path_not_found", path, `${path} is not a directory (${why})`);
|
|
6617
|
+
}
|
|
6618
|
+
if (!isDirectory) throw new ScanError("path_not_found", path, `${path} is not a directory`);
|
|
6619
|
+
}
|
|
6620
|
+
function summarize(model, rules, publicTables = []) {
|
|
5885
6621
|
const apiTables = model.tables.filter((t) => !t.table.includes("."));
|
|
5886
6622
|
return {
|
|
5887
6623
|
root: model.root,
|
|
@@ -5891,10 +6627,12 @@ function summarize(model, rules) {
|
|
|
5891
6627
|
tablesKnown: apiTables.length,
|
|
5892
6628
|
tablesWithRls: apiTables.filter((t) => t.rlsEnabled).length,
|
|
5893
6629
|
rules,
|
|
5894
|
-
warnings: model.warnings
|
|
6630
|
+
warnings: model.warnings,
|
|
6631
|
+
publicTables: [...publicTables]
|
|
5895
6632
|
};
|
|
5896
6633
|
}
|
|
5897
6634
|
function runScan(path, opts = {}) {
|
|
6635
|
+
ensureDirectory(path);
|
|
5898
6636
|
const cfg = loadAuditConfig(path);
|
|
5899
6637
|
const repoDirs = repoMigrationDirs(path, cfg.config.migrations ?? []);
|
|
5900
6638
|
const sqlDirs = [...repoDirs.dirs, ...opts.sqlDirs ?? []];
|
|
@@ -5904,10 +6642,11 @@ function runScan(path, opts = {}) {
|
|
|
5904
6642
|
...ignore.globs.length > 0 ? { ignore: ignore.globs } : {}
|
|
5905
6643
|
});
|
|
5906
6644
|
const graph = buildGraph(model);
|
|
5907
|
-
const
|
|
6645
|
+
const publicTables = cfg.config.publicTables ?? [];
|
|
6646
|
+
const runOpts = { ...opts.now === void 0 ? {} : { now: opts.now }, publicTables };
|
|
5908
6647
|
const findings = runRules(defaultRules, model, graph, runOpts);
|
|
5909
6648
|
const coverage = summarizeCoverage(findings);
|
|
5910
|
-
const summary = summarize(model, defaultRules.length);
|
|
6649
|
+
const summary = summarize(model, defaultRules.length, publicTables);
|
|
5911
6650
|
return {
|
|
5912
6651
|
summary: {
|
|
5913
6652
|
...summary,
|
|
@@ -5941,6 +6680,8 @@ Options:
|
|
|
5941
6680
|
--migrations <dir> extra directory with Supabase migration SQL (repeatable)
|
|
5942
6681
|
-h, --help show this help
|
|
5943
6682
|
|
|
6683
|
+
Exit code: 0 clean, 1 a finding reached --fail-on, 2 usage error or <path> is not a directory
|
|
6684
|
+
|
|
5944
6685
|
Config: <path>/audit.config.json { "ignore": ["evals/**"], "migrations": ["supabase/migrations"] }
|
|
5945
6686
|
Suppress a finding: // auditai:ignore <ruleId|*> -- reason (above the handler or at the top of a file)
|
|
5946
6687
|
`;
|
|
@@ -5976,10 +6717,15 @@ function main(argv) {
|
|
|
5976
6717
|
return 2;
|
|
5977
6718
|
}
|
|
5978
6719
|
const migrations = values.migrations.map((m) => resolve4(m));
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
migrations.length > 0 ? { sqlDirs: migrations } : {}
|
|
5982
|
-
)
|
|
6720
|
+
let result;
|
|
6721
|
+
try {
|
|
6722
|
+
result = runScan(positionals[0] ?? ".", migrations.length > 0 ? { sqlDirs: migrations } : {});
|
|
6723
|
+
} catch (e) {
|
|
6724
|
+
if (!(e instanceof ScanError)) throw e;
|
|
6725
|
+
process.stderr.write(`error: ${e.message}
|
|
6726
|
+
`);
|
|
6727
|
+
return 2;
|
|
6728
|
+
}
|
|
5983
6729
|
process.stdout.write(
|
|
5984
6730
|
values.json ? `${JSON.stringify(result, null, 2)}
|
|
5985
6731
|
` : formatScanText(result)
|