auditai-scan 0.7.3 → 0.7.4
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 +269 -20
- package/package.json +1 -1
package/dist/auditai-scan.mjs
CHANGED
|
@@ -260,7 +260,7 @@ function policedTables(f, model) {
|
|
|
260
260
|
const out = [];
|
|
261
261
|
for (const n of names) {
|
|
262
262
|
const t = table(model, n);
|
|
263
|
-
if (!t
|
|
263
|
+
if (!t?.rlsEnabled || t.policies.length === 0)
|
|
264
264
|
return null;
|
|
265
265
|
out.push(qualified(t.table));
|
|
266
266
|
}
|
|
@@ -1568,6 +1568,45 @@ function callResultChecked(call) {
|
|
|
1568
1568
|
}
|
|
1569
1569
|
return cur !== void 0 && ts4.isIfStatement(cur) && cur.expression === child && exitKind(cur.thenStatement) !== null;
|
|
1570
1570
|
}
|
|
1571
|
+
function rowNamesOf(tail) {
|
|
1572
|
+
const { parent } = outerOf(tail);
|
|
1573
|
+
if (!parent || !ts4.isVariableDeclaration(parent)) return /* @__PURE__ */ new Set();
|
|
1574
|
+
const rows = resultNames(parent.name).data;
|
|
1575
|
+
const fn2 = enclosingFunction(parent);
|
|
1576
|
+
const scope = fn2 ? fn2.body : parent.getSourceFile();
|
|
1577
|
+
if (scope) {
|
|
1578
|
+
walkOwn(scope, (n) => {
|
|
1579
|
+
if (!ts4.isVariableDeclaration(n) || !n.initializer || n.pos < parent.end) return;
|
|
1580
|
+
const init = unwrap(n.initializer);
|
|
1581
|
+
if (!ts4.isIdentifier(init) || !rows.has(init.text) || ts4.isIdentifier(n.name)) return;
|
|
1582
|
+
for (const name of resultNames(n.name).data) rows.add(name);
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
return rows;
|
|
1586
|
+
}
|
|
1587
|
+
function checkedResultExit(call) {
|
|
1588
|
+
const { node, parent } = outerOf(call);
|
|
1589
|
+
if (!parent) return null;
|
|
1590
|
+
if (ts4.isVariableDeclaration(parent) && parent.initializer === node) {
|
|
1591
|
+
const names = new Set(boundNames(parent.name));
|
|
1592
|
+
for (const s of ifsAfter(parent)) {
|
|
1593
|
+
if (!checksMissing(s.expression, names)) continue;
|
|
1594
|
+
const kind = exitKind(s.thenStatement);
|
|
1595
|
+
if (kind) return kind;
|
|
1596
|
+
}
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
if (!ts4.isPrefixUnaryExpression(parent) || parent.operator !== ts4.SyntaxKind.ExclamationToken) {
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
let cur = parent.parent;
|
|
1603
|
+
let child = parent;
|
|
1604
|
+
while (cur && ts4.isParenthesizedExpression(cur)) {
|
|
1605
|
+
child = cur;
|
|
1606
|
+
cur = cur.parent;
|
|
1607
|
+
}
|
|
1608
|
+
return cur !== void 0 && ts4.isIfStatement(cur) && cur.expression === child ? exitKind(cur.thenStatement) : null;
|
|
1609
|
+
}
|
|
1571
1610
|
function compareOrder(a, b) {
|
|
1572
1611
|
const n = Math.min(a.length, b.length);
|
|
1573
1612
|
for (let i = 0; i < n; i++) {
|
|
@@ -4072,6 +4111,8 @@ var REFERENCE_SQL_DIRS = /* @__PURE__ */ new Set([
|
|
|
4072
4111
|
"examples",
|
|
4073
4112
|
"old"
|
|
4074
4113
|
]);
|
|
4114
|
+
var CLI_MIGRATION = /(?:^|\/)supabase\/migrations\/[0-9]+_[^/]*\.sql$/;
|
|
4115
|
+
var CLI_SEED = /(?:^|\/)supabase\/seed\.sql$/;
|
|
4075
4116
|
function appliedSqlFiles(rels) {
|
|
4076
4117
|
const applied = rels.filter(isAppliedSqlFile);
|
|
4077
4118
|
const slashed = (rel) => rel.split("\\").join("/");
|
|
@@ -4079,9 +4120,16 @@ function appliedSqlFiles(rels) {
|
|
|
4079
4120
|
(rel) => /(?:^|\/)supabase\/migrations\/[^/]+\.sql$/i.test(slashed(rel))
|
|
4080
4121
|
);
|
|
4081
4122
|
if (!hasMigrations) return applied;
|
|
4082
|
-
|
|
4123
|
+
const kept = applied.filter(
|
|
4083
4124
|
(rel) => !slashed(rel).split("/").slice(0, -1).some((dir) => REFERENCE_SQL_DIRS.has(dir.toLowerCase()))
|
|
4084
4125
|
);
|
|
4126
|
+
const migrations = kept.filter((rel) => CLI_MIGRATION.test(slashed(rel)));
|
|
4127
|
+
if (migrations.length === 0) return kept;
|
|
4128
|
+
const seeds = kept.filter((rel) => CLI_SEED.test(slashed(rel)));
|
|
4129
|
+
const byHand = kept.filter(
|
|
4130
|
+
(rel) => !CLI_MIGRATION.test(slashed(rel)) && !CLI_SEED.test(slashed(rel))
|
|
4131
|
+
);
|
|
4132
|
+
return [...byHand, ...migrations, ...seeds];
|
|
4085
4133
|
}
|
|
4086
4134
|
function parseSqlForRls(rel, text, into) {
|
|
4087
4135
|
if (!isAppliedSqlFile(rel)) return;
|
|
@@ -5640,14 +5688,23 @@ function analyzeFrame(p, frame, acc) {
|
|
|
5640
5688
|
call,
|
|
5641
5689
|
chain.segments.map((s) => s.name)
|
|
5642
5690
|
);
|
|
5643
|
-
const
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5691
|
+
const exits = rowExit === "throw" || rowExit === "return" && frame.exitPropagates;
|
|
5692
|
+
const checks = [
|
|
5693
|
+
// `if (!row || row.user_id !== user.id) return 404`: ownership checked in code after the read.
|
|
5694
|
+
...rowComparisons(call).filter((c) => c.exit === "throw" || frame.exitPropagates).map((c) => ({
|
|
5695
|
+
method: "compare",
|
|
5696
|
+
column: c.column,
|
|
5697
|
+
valueText: c.value.getText(sf).replace(/\s+/g, " "),
|
|
5698
|
+
inputDerived: derivedIn(frame, c.value),
|
|
5699
|
+
...identityOf(frame, c.value) !== void 0 ? { identity: true } : {}
|
|
5700
|
+
})),
|
|
5701
|
+
// `if (!(await canAccessRequest(user, request.org_id))) return null`: checked by a helper.
|
|
5702
|
+
...helperRowChecks(p, call, frame, scope)
|
|
5703
|
+
];
|
|
5650
5704
|
if (checks.length > 0) query.ownerChecks = checks;
|
|
5705
|
+
if (exits && query.operation === "select") {
|
|
5706
|
+
membershipChecks(acc.reads, query, filterValues, frame);
|
|
5707
|
+
}
|
|
5651
5708
|
acc.reads.push({
|
|
5652
5709
|
query,
|
|
5653
5710
|
keys: query.filters.map((f) => {
|
|
@@ -5655,8 +5712,10 @@ function analyzeFrame(p, frame, acc) {
|
|
|
5655
5712
|
return v ? valueKey(frame, v) : null;
|
|
5656
5713
|
}),
|
|
5657
5714
|
order: [...frame.pathPos, parsed.anchor.getStart(sf)],
|
|
5658
|
-
exits
|
|
5659
|
-
checks
|
|
5715
|
+
exits,
|
|
5716
|
+
checks,
|
|
5717
|
+
frame: frame.key,
|
|
5718
|
+
rows: rowNamesOf(call)
|
|
5660
5719
|
});
|
|
5661
5720
|
bindBuilder(call, query);
|
|
5662
5721
|
}
|
|
@@ -5676,6 +5735,187 @@ function analyzeFrame(p, frame, acc) {
|
|
|
5676
5735
|
analyzeFrame(p, child, acc);
|
|
5677
5736
|
}
|
|
5678
5737
|
}
|
|
5738
|
+
function paramRole(e, roles) {
|
|
5739
|
+
let u = unwrap(e);
|
|
5740
|
+
while (ts11.isPropertyAccessExpression(u) || ts11.isElementAccessExpression(u)) {
|
|
5741
|
+
if (ts11.isPropertyAccessExpression(u) && u.name.text === "user_metadata") return null;
|
|
5742
|
+
u = unwrap(u.expression);
|
|
5743
|
+
}
|
|
5744
|
+
return ts11.isIdentifier(u) ? roles.get(u.text) ?? null : null;
|
|
5745
|
+
}
|
|
5746
|
+
var EQUALS = /* @__PURE__ */ new Set([
|
|
5747
|
+
ts11.SyntaxKind.EqualsEqualsEqualsToken,
|
|
5748
|
+
ts11.SyntaxKind.EqualsEqualsToken
|
|
5749
|
+
]);
|
|
5750
|
+
var NOT_EQUALS = /* @__PURE__ */ new Set([
|
|
5751
|
+
ts11.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
5752
|
+
ts11.SyntaxKind.ExclamationEqualsToken
|
|
5753
|
+
]);
|
|
5754
|
+
function comparesRowAndCaller(e, roles, ops) {
|
|
5755
|
+
const u = unwrap(e);
|
|
5756
|
+
if (!ts11.isBinaryExpression(u) || !ops.has(u.operatorToken.kind)) return false;
|
|
5757
|
+
const a = paramRole(u.left, roles);
|
|
5758
|
+
const b = paramRole(u.right, roles);
|
|
5759
|
+
return a === "row" && b === "caller" || a === "caller" && b === "row";
|
|
5760
|
+
}
|
|
5761
|
+
function isCallerRoleTest(e, roles) {
|
|
5762
|
+
const u = unwrap(e);
|
|
5763
|
+
const onCaller = (x) => {
|
|
5764
|
+
const ux = unwrap(x);
|
|
5765
|
+
return ts11.isPropertyAccessExpression(ux) && paramRole(ux, roles) === "caller";
|
|
5766
|
+
};
|
|
5767
|
+
if (ts11.isBinaryExpression(u)) {
|
|
5768
|
+
const op = u.operatorToken.kind;
|
|
5769
|
+
if (op === ts11.SyntaxKind.BarBarToken || op === ts11.SyntaxKind.AmpersandAmpersandToken) {
|
|
5770
|
+
return isCallerRoleTest(u.left, roles) && isCallerRoleTest(u.right, roles);
|
|
5771
|
+
}
|
|
5772
|
+
if (!EQUALS.has(op)) return false;
|
|
5773
|
+
return onCaller(u.left) && ts11.isStringLiteralLike(unwrap(u.right)) || onCaller(u.right) && ts11.isStringLiteralLike(unwrap(u.left));
|
|
5774
|
+
}
|
|
5775
|
+
if (!ts11.isCallExpression(u) || !ts11.isPropertyAccessExpression(u.expression) || u.expression.name.text !== "includes") {
|
|
5776
|
+
return false;
|
|
5777
|
+
}
|
|
5778
|
+
const list = unwrap(u.expression.expression);
|
|
5779
|
+
const arg = u.arguments[0];
|
|
5780
|
+
return ts11.isArrayLiteralExpression(list) && list.elements.length > 0 && list.elements.every((el) => ts11.isStringLiteralLike(el)) && u.arguments.length === 1 && arg !== void 0 && onCaller(arg);
|
|
5781
|
+
}
|
|
5782
|
+
function isDenyValue(e) {
|
|
5783
|
+
const u = unwrap(e);
|
|
5784
|
+
return u.kind === ts11.SyntaxKind.FalseKeyword || u.kind === ts11.SyntaxKind.NullKeyword || ts11.isIdentifier(u) && u.text === "undefined";
|
|
5785
|
+
}
|
|
5786
|
+
function operandsOf(e, op) {
|
|
5787
|
+
const u = unwrap(e);
|
|
5788
|
+
return ts11.isBinaryExpression(u) && u.operatorToken.kind === op ? [...operandsOf(u.left, op), ...operandsOf(u.right, op)] : [u];
|
|
5789
|
+
}
|
|
5790
|
+
function thenBranchIf(ret) {
|
|
5791
|
+
let child = ret;
|
|
5792
|
+
for (let cur = ret.parent; cur && !isFunctionLikeNode(cur); cur = cur.parent) {
|
|
5793
|
+
if (ts11.isIfStatement(cur)) return cur.thenStatement === child ? cur : null;
|
|
5794
|
+
child = cur;
|
|
5795
|
+
}
|
|
5796
|
+
return null;
|
|
5797
|
+
}
|
|
5798
|
+
function onlyDenies(stmt) {
|
|
5799
|
+
let stops = false;
|
|
5800
|
+
let allows = false;
|
|
5801
|
+
walkOwn(stmt, (n) => {
|
|
5802
|
+
if (ts11.isThrowStatement(n)) stops = true;
|
|
5803
|
+
else if (ts11.isReturnStatement(n)) {
|
|
5804
|
+
if (!n.expression || isDenyValue(n.expression)) stops = true;
|
|
5805
|
+
else allows = true;
|
|
5806
|
+
}
|
|
5807
|
+
});
|
|
5808
|
+
return stops && !allows;
|
|
5809
|
+
}
|
|
5810
|
+
function helperComparison(fn2, rowIndex, callerIndexes, sf) {
|
|
5811
|
+
const roles = /* @__PURE__ */ new Map();
|
|
5812
|
+
fn2.parameters.forEach((param, i) => {
|
|
5813
|
+
if (!ts11.isIdentifier(param.name)) return;
|
|
5814
|
+
if (i === rowIndex) roles.set(param.name.text, "row");
|
|
5815
|
+
else if (callerIndexes.includes(i)) roles.set(param.name.text, "caller");
|
|
5816
|
+
});
|
|
5817
|
+
const kinds = new Set(roles.values());
|
|
5818
|
+
if (!kinds.has("row") || !kinds.has("caller") || !fn2.body) return null;
|
|
5819
|
+
let denied = false;
|
|
5820
|
+
if (ts11.isBlock(fn2.body)) {
|
|
5821
|
+
walkOwn(fn2.body, (n) => {
|
|
5822
|
+
if (ts11.isIfStatement(n) && operandsOf(n.expression, ts11.SyntaxKind.BarBarToken).some(
|
|
5823
|
+
(c) => comparesRowAndCaller(c, roles, NOT_EQUALS)
|
|
5824
|
+
) && onlyDenies(n.thenStatement)) {
|
|
5825
|
+
denied = true;
|
|
5826
|
+
}
|
|
5827
|
+
});
|
|
5828
|
+
}
|
|
5829
|
+
const bypasses = [];
|
|
5830
|
+
let compared = false;
|
|
5831
|
+
for (const ret of ownReturns(fn2)) {
|
|
5832
|
+
if (isDenyValue(ret)) continue;
|
|
5833
|
+
if (unwrap(ret).kind === ts11.SyntaxKind.TrueKeyword) {
|
|
5834
|
+
const gate = thenBranchIf(ret);
|
|
5835
|
+
if (gate && isCallerRoleTest(gate.expression, roles)) {
|
|
5836
|
+
bypasses.push(gate.expression.getText(sf).replace(/\s+/g, " "));
|
|
5837
|
+
continue;
|
|
5838
|
+
}
|
|
5839
|
+
if (gate === null && denied) {
|
|
5840
|
+
compared = true;
|
|
5841
|
+
continue;
|
|
5842
|
+
}
|
|
5843
|
+
return null;
|
|
5844
|
+
}
|
|
5845
|
+
for (const alt of operandsOf(ret, ts11.SyntaxKind.BarBarToken)) {
|
|
5846
|
+
const conjuncts = operandsOf(alt, ts11.SyntaxKind.AmpersandAmpersandToken);
|
|
5847
|
+
if (conjuncts.some((c) => comparesRowAndCaller(c, roles, EQUALS))) compared = true;
|
|
5848
|
+
else if (isCallerRoleTest(alt, roles)) bypasses.push(alt.getText(sf).replace(/\s+/g, " "));
|
|
5849
|
+
else return null;
|
|
5850
|
+
}
|
|
5851
|
+
}
|
|
5852
|
+
if (!compared) return null;
|
|
5853
|
+
return bypasses.length > 0 ? { bypass: bypasses.join(" || ") } : {};
|
|
5854
|
+
}
|
|
5855
|
+
function helperRowChecks(p, tail, frame, scope) {
|
|
5856
|
+
const rows = rowNamesOf(tail);
|
|
5857
|
+
const decl = outerOf(tail).parent;
|
|
5858
|
+
const fn2 = decl ? enclosingFunction(decl) : null;
|
|
5859
|
+
if (rows.size === 0 || !decl || !fn2?.body) return [];
|
|
5860
|
+
const out = [];
|
|
5861
|
+
walkOwn(fn2.body, (n) => {
|
|
5862
|
+
if (!ts11.isCallExpression(n) || n.pos < decl.end) return;
|
|
5863
|
+
const rowIndex = n.arguments.findIndex((a) => {
|
|
5864
|
+
const u = unwrap(a);
|
|
5865
|
+
if (!ts11.isPropertyAccessExpression(u)) return false;
|
|
5866
|
+
const base = unwrap(u.expression);
|
|
5867
|
+
return ts11.isIdentifier(base) && rows.has(base.text);
|
|
5868
|
+
});
|
|
5869
|
+
const rowArg = unwrap(n.arguments[rowIndex] ?? n);
|
|
5870
|
+
if (rowIndex < 0 || !ts11.isPropertyAccessExpression(rowArg)) return;
|
|
5871
|
+
const exit = checkedResultExit(n);
|
|
5872
|
+
if (exit === null || exit === "return" && !frame.exitPropagates) return;
|
|
5873
|
+
const callerIndexes = n.arguments.flatMap(
|
|
5874
|
+
(a, i) => i !== rowIndex && identityOf(frame, a) !== void 0 ? [i] : []
|
|
5875
|
+
);
|
|
5876
|
+
if (callerIndexes.length === 0) return;
|
|
5877
|
+
const target = callTarget(p, n, frame, scope);
|
|
5878
|
+
const targetSf = target ? p.sources.get(target.facts.file) : void 0;
|
|
5879
|
+
if (!target || !targetSf) return;
|
|
5880
|
+
const verdict = helperComparison(target.fn, rowIndex, callerIndexes, targetSf);
|
|
5881
|
+
if (!verdict) return;
|
|
5882
|
+
out.push({
|
|
5883
|
+
method: "compare",
|
|
5884
|
+
column: rowArg.name.text,
|
|
5885
|
+
valueText: n.getText(frame.sf).replace(/\s+/g, " ").slice(0, 160),
|
|
5886
|
+
inputDerived: false,
|
|
5887
|
+
identity: true,
|
|
5888
|
+
...verdict.bypass !== void 0 ? { bypass: verdict.bypass } : {}
|
|
5889
|
+
});
|
|
5890
|
+
});
|
|
5891
|
+
return out;
|
|
5892
|
+
}
|
|
5893
|
+
function membershipChecks(reads, query, values, frame) {
|
|
5894
|
+
if (!query.filters.some((f) => f.identity === true && !f.inputDerived && f.method === "eq")) {
|
|
5895
|
+
return;
|
|
5896
|
+
}
|
|
5897
|
+
for (const f of query.filters) {
|
|
5898
|
+
if (f.identity === true || f.inputDerived || f.method !== "eq") continue;
|
|
5899
|
+
const v = values.get(f);
|
|
5900
|
+
const u = v ? unwrap(v) : void 0;
|
|
5901
|
+
if (!u || !ts11.isPropertyAccessExpression(u)) continue;
|
|
5902
|
+
const base = unwrap(u.expression);
|
|
5903
|
+
if (!ts11.isIdentifier(base) || normalizeColumn(u.name.text) !== normalizeColumn(f.column)) {
|
|
5904
|
+
continue;
|
|
5905
|
+
}
|
|
5906
|
+
const row = [...reads].reverse().find((r) => r.frame === frame.key && r.rows.has(base.text));
|
|
5907
|
+
if (!row) continue;
|
|
5908
|
+
const check = {
|
|
5909
|
+
method: "compare",
|
|
5910
|
+
column: u.name.text,
|
|
5911
|
+
valueText: `a ${query.table} row of the caller (${query.location.file}:${query.location.line})`,
|
|
5912
|
+
inputDerived: false,
|
|
5913
|
+
identity: true
|
|
5914
|
+
};
|
|
5915
|
+
row.checks.push(check);
|
|
5916
|
+
row.query.ownerChecks = [...row.query.ownerChecks ?? [], check];
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5679
5919
|
function childFrame(p, call, target, frame) {
|
|
5680
5920
|
const tsf = p.sources.get(target.facts.file);
|
|
5681
5921
|
if (!tsf) return null;
|
|
@@ -6618,7 +6858,10 @@ function tableDataOf(ctx, table2) {
|
|
|
6618
6858
|
return ctx.graph.nodes.get(`table:${table2}`)?.data;
|
|
6619
6859
|
}
|
|
6620
6860
|
function callerCheck(checks) {
|
|
6621
|
-
|
|
6861
|
+
const tying = (checks ?? []).filter(
|
|
6862
|
+
(c) => !c.inputDerived && (c.identity === true || isScopeColumn(c.column))
|
|
6863
|
+
);
|
|
6864
|
+
return tying.find((c) => c.bypass === void 0) ?? tying[0];
|
|
6622
6865
|
}
|
|
6623
6866
|
function guardTiesRowToCaller(guard, table2, callerFns) {
|
|
6624
6867
|
const callerFilter = guard.filters.find(
|
|
@@ -6636,7 +6879,8 @@ function guardTiesRowToCaller(guard, table2, callerFns) {
|
|
|
6636
6879
|
return {
|
|
6637
6880
|
tied: true,
|
|
6638
6881
|
how: `its ${compared.column} compared with ${compared.valueText} in code, stopping otherwise`,
|
|
6639
|
-
why: ""
|
|
6882
|
+
why: "",
|
|
6883
|
+
...compared.bypass !== void 0 ? { bypass: compared.bypass } : {}
|
|
6640
6884
|
};
|
|
6641
6885
|
}
|
|
6642
6886
|
if (guard.client !== "user_scoped") {
|
|
@@ -6682,20 +6926,23 @@ var serviceRoleObjectAccessWithoutTenantScope = {
|
|
|
6682
6926
|
(f) => isScopeColumn(f.column) || f.identity === true && !f.inputDerived
|
|
6683
6927
|
);
|
|
6684
6928
|
if (!idFilter || scoped) continue;
|
|
6685
|
-
|
|
6929
|
+
const own = q.operation === "select" ? callerCheck(q.ownerChecks) : void 0;
|
|
6930
|
+
if (own && own.bypass === void 0) continue;
|
|
6686
6931
|
const guard = q.guard;
|
|
6687
6932
|
const guardTable = guard?.parent ? tableDataOf(ctx, guard.table) : v.tableData;
|
|
6688
6933
|
const tied = guard ? guardTiesRowToCaller(guard, guardTable, callerFns) : null;
|
|
6689
|
-
if (guard && tied?.tied && guard.exitsWhenMissing) continue;
|
|
6934
|
+
if (guard && tied?.tied && guard.exitsWhenMissing && tied.bypass === void 0) continue;
|
|
6935
|
+
const bypassed = own ?? (guard && tied?.tied && guard.exitsWhenMissing ? callerCheck(guard.checks) : void 0);
|
|
6936
|
+
const bypass = bypassed?.bypass;
|
|
6690
6937
|
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}"`;
|
|
6691
|
-
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}.` : "";
|
|
6938
|
+
const guardNote = guard && tied && bypass === void 0 && (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}.` : "";
|
|
6692
6939
|
const tableName = v.tableData?.table ?? q.table;
|
|
6693
6940
|
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.";
|
|
6694
6941
|
const gate = adminOnly(ctx, h, v.tableData);
|
|
6695
6942
|
const admin = gate.check;
|
|
6696
6943
|
const anonPolicy = q.operation === "select" ? anonReadPolicy(v.tableData) : void 0;
|
|
6697
|
-
const downgrade = admin || anonPolicy ? "medium" : void 0;
|
|
6698
|
-
const downgradeNote = (admin ? adminOnlyNote(admin, tableName) : "") + (gate.selfGrant ? ` The handler is behind a role check, but ${gate.selfGrant}.` : "") + (anonPolicy ? publicReadNote(anonPolicy, tableName) : "");
|
|
6944
|
+
const downgrade = admin || anonPolicy || bypass !== void 0 ? "medium" : void 0;
|
|
6945
|
+
const downgradeNote = (admin ? adminOnlyNote(admin, tableName) : "") + (gate.selfGrant ? ` The handler is behind a role check, but ${gate.selfGrant}.` : "") + (anonPolicy ? publicReadNote(anonPolicy, tableName) : "") + (bypassed && bypass !== void 0 ? ` The row's ${bypassed.column} is checked against the caller (${bypassed.valueText}), but ${bypass} lets the caller through without that check, so the finding is lowered rather than dropped: verify those roles belong to the service's staff and a user cannot grant them to themselves.` : "");
|
|
6699
6946
|
const path = [
|
|
6700
6947
|
h.data.kind === "server_action" ? "Server action call" : "HTTP request",
|
|
6701
6948
|
h.data.entry,
|
|
@@ -6721,18 +6968,20 @@ var serviceRoleObjectAccessWithoutTenantScope = {
|
|
|
6721
6968
|
authenticated: h.authenticated,
|
|
6722
6969
|
query: q.text,
|
|
6723
6970
|
...admin ? { adminOnly: true, roleCheck: admin.source } : {},
|
|
6724
|
-
...anonPolicy ? { anonReadPolicy: anonPolicy.name } : {}
|
|
6971
|
+
...anonPolicy ? { anonReadPolicy: anonPolicy.name } : {},
|
|
6972
|
+
...bypass !== void 0 ? { roleBypass: bypass } : {}
|
|
6725
6973
|
}
|
|
6726
6974
|
},
|
|
6727
6975
|
{ kind: "trace", summary: path.join(" -> ") }
|
|
6728
6976
|
];
|
|
6729
6977
|
const who = admin ? "Admin-only" : h.authenticated ? "Cross-tenant" : "Unauthenticated";
|
|
6978
|
+
const verify = admin ? " (verify the admin check)" : bypass !== void 0 ? " (verify the role exception)" : "";
|
|
6730
6979
|
out.push(
|
|
6731
6980
|
finding(
|
|
6732
6981
|
ctx,
|
|
6733
6982
|
this,
|
|
6734
6983
|
{
|
|
6735
|
-
title: `${who} ${q.operation} on "${tableName}" via ${clientNoun(v.clientData)}${
|
|
6984
|
+
title: `${who} ${q.operation} on "${tableName}" via ${clientNoun(v.clientData)}${verify}`,
|
|
6736
6985
|
entrypoints: [h.data.entry],
|
|
6737
6986
|
sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
|
|
6738
6987
|
sinks: [`supabase.${q.operation}:public.${tableName}`],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auditai-scan",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Deterministic security scanner for Next.js + Supabase apps: cross-tenant reads, RLS gaps, service-role misuse, mass assignment. No account, no model, seconds.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|