fix(auth): fail closed when the adminAuth roles join errors (stable) (#975)

Closes #968 on stable. Backport of #974.

The roles-join fallback in adminAuth fabricated role_name='super_admin' on ANY database error, so a transient fault silently granted super_admin for its duration. Gate it on isMissingRolesSchema(), moved to utils/dbErrors.js and shared with apiTokenAuth, with the predicate tightened to trust SQLSTATE 42P01/42703 on Postgres and exact driver phrasing on SQLite.
This commit is contained in:
Paul Nothaft
2026-08-03 14:48:33 +02:00
committed by GitHub
parent fecc18cbc8
commit cc49f6997a
5 changed files with 195 additions and 19 deletions
+1 -18
View File
@@ -1,6 +1,7 @@
const crypto = require('crypto');
const { db } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { isMissingRolesSchema } = require('../utils/dbErrors');
const logger = require('../utils/logger');
const TOKEN_PREFIX = 'pp_live_';
@@ -32,24 +33,6 @@ function parseScopes(raw) {
.filter((s) => VALID_SCOPES.includes(s));
}
/**
* Does this error mean the `roles` table/column genuinely isn't there yet
* (mid-upgrade), as opposed to the database being briefly unhappy?
*
* The distinction matters because the fallback below grants super_admin: a
* catch-all would turn any transient failure — connection reset, deadlock,
* statement timeout — into a privilege escalation that hands a demoted viewer
* exactly the access GHSA-9697 closes.
*/
function isMissingRolesSchema(err) {
const message = String(err?.message || '');
if (!/roles/i.test(message)) return false;
// PG: 42P01 undefined_table / 42703 undefined_column. SQLite carries no
// codes, so match its wording too.
return err?.code === '42P01' || err?.code === '42703'
|| /no such table|no such column|does not exist|unknown column/i.test(message);
}
/**
* Middleware: authenticate via API token. Maps the token to its owner
* admin user, attaches { req.admin, req.apiToken }, then defers to the