← back to Directory Core
src/_ident.ts
39 lines
// directory-core / _ident
//
// Defends every SQL identifier interpolation site (table names, column names,
// schema-qualified column refs) against injection. Audit P1-c 2026-05-04: all
// the *.ts modules that template identifiers into SQL strings — auth.ts (T_USERS,
// T_SESSIONS, SESSION_PK, SESSION_FK, USER_COLUMNS), match.ts (opts.table,
// opts.whereExtra), geo.ts (makePgZipCache table) — were trusting their callers.
// Today every caller is hard-coded, but one `createAuth({ userTable: req.query.t })`
// mistake = full DB takeover. This module makes the trust boundary explicit.
const SAFE_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/;
/**
* Throws if `s` is not a safe SQL identifier. Accepts plain idents (`app_users`)
* and one level of schema-qualification (`public.app_users`). Rejects spaces,
* quotes, semicolons, comment markers, and anything else that could break out
* of an identifier position in PostgreSQL grammar.
*
* @param s candidate identifier
* @param what optional label for the error (e.g. 'userTable', 'sessionPkColumn')
*/
export function assertSafeIdent(s: unknown, what = 'identifier'): asserts s is string {
if (typeof s !== 'string' || !SAFE_IDENT.test(s)) {
throw new Error(
`directory-core: refusing to interpolate unsafe ${what} into SQL: ${JSON.stringify(s)}`
);
}
}
/**
* Asserts every element of an array is a safe identifier. Used for column lists.
*/
export function assertSafeIdents(arr: unknown, what = 'identifier list'): asserts arr is string[] {
if (!Array.isArray(arr)) {
throw new Error(`directory-core: ${what} must be an array of strings`);
}
for (const s of arr) assertSafeIdent(s, `${what} item`);
}