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:
@@ -12,4 +12,37 @@ function isUniqueViolation(err) {
|
||||
return /unique/i.test(msg) || /sqlite_constraint/i.test(msg);
|
||||
}
|
||||
|
||||
module.exports = { isUniqueViolation };
|
||||
/**
|
||||
* 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 both auth paths fall back to granting
|
||||
* super_admin when the roles join fails: a catch-all would turn any transient
|
||||
* failure — connection reset, deadlock, statement timeout, pool exhaustion —
|
||||
* into a privilege escalation that hands a demoted viewer exactly the access
|
||||
* GHSA-9697 closes. Callers must rethrow anything this returns false for.
|
||||
*/
|
||||
function isMissingRolesSchema(err) {
|
||||
if (!err) return false;
|
||||
const message = String(err.message || '');
|
||||
|
||||
// Postgres is authoritative via SQLSTATE: 42P01 undefined_table, 42703
|
||||
// undefined_column. Both are schema conditions, never transient.
|
||||
if (err.code === '42P01' || err.code === '42703') return true;
|
||||
|
||||
// SQLite carries no SQLSTATE, so the driver's wording is all there is — but
|
||||
// it must be matched EXACTLY, naming the object the roles join needs. A
|
||||
// generic /does not exist/ test would be unsound here: knex prefixes the
|
||||
// failing SQL to err.message, and that SQL always names `roles` on this
|
||||
// join, so any "... does not exist" fault on the connection (e.g. pgbouncer
|
||||
// losing a named prepared statement, SQLSTATE 26000) would read as a missing
|
||||
// roles schema and fabricate super_admin.
|
||||
//
|
||||
// Two states are legitimate, per the migration order:
|
||||
// pre-054 → roles table absent
|
||||
// post-054, pre-057 → roles exists, admin_users.role_id not added yet
|
||||
return /no such table: roles\b/i.test(message)
|
||||
|| /no such column: (roles\.|admin_users\.role_id\b)/i.test(message);
|
||||
}
|
||||
|
||||
module.exports = { isUniqueViolation, isMissingRolesSchema };
|
||||
|
||||
Reference in New Issue
Block a user