diff options
| author | Adam Soltys <[email protected]> | 2026-08-07 22:59:21 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-07 23:29:25 -0700 |
| commit | d513ab75131bfaa16b1f0820d0fb18ad9e7f6ab1 (patch) | |
| tree | be028867721f69a1dc102e33b999cd6abdebf522 /routes | |
| parent | 9f99bbdb765fc70373943930d543ce1cde7a5f35 (diff) | |
| download | coinos-server-d513ab75131bfaa16b1f0820d0fb18ad9e7f6ab1.tar.gz coinos-server-d513ab75131bfaa16b1f0820d0fb18ad9e7f6ab1.zip | |
auth: fail closed on adminpass, scrub sample secrets (S3)
The master-password path (isAdmin = password === config.adminpass) logs
in as any user, skipping password, 2FA, and captcha. Worse, when
adminpass is unset an omitted password field made undefined === undefined
true, authenticating as anyone. Require both the configured value and the
supplied password to be non-empty and match exactly, across every
adminpass comparison (login, recaptcha bypass, support email, freeze).
Also scrub config.ts.sample: the sample shipped a working-looking JWT
secret, two server nsec keys, and adminpass "pwa". Replace with
fail-closed placeholders and regenerate instructions. Rotate any
deployment that ever shipped the sample values.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diffstat (limited to 'routes')
| -rw-r--r-- | routes/email.ts | 2 | ||||
| -rw-r--r-- | routes/payments.ts | 2 | ||||
| -rw-r--r-- | routes/users.ts | 9 |
3 files changed, 9 insertions, 4 deletions
diff --git a/routes/email.ts b/routes/email.ts index 1b8dd9f0..eb256f1a 100644 --- a/routes/email.ts +++ b/routes/email.ts @@ -22,7 +22,7 @@ export default { }) .json(); - if (success || response === config.adminpass) { + if (success || (!!config.adminpass && response === config.adminpass)) { body.token = undefined; warn("support request from", email); diff --git a/routes/payments.ts b/routes/payments.ts index 9751b421..4f0e7f6e 100644 --- a/routes/payments.ts +++ b/routes/payments.ts @@ -625,7 +625,7 @@ export default { body: { secret }, } = req; try { - if (secret !== config.adminpass) fail("unauthorized"); + if (!config.adminpass || secret !== config.adminpass) fail("unauthorized"); await s("freeze", true); res.send("ok"); } catch (e) { diff --git a/routes/users.ts b/routes/users.ts index 63699777..eafcd74a 100644 --- a/routes/users.ts +++ b/routes/users.ts @@ -70,7 +70,7 @@ const verifyRecaptcha = async (response, req?) => { }, }) .json(); - return success || response === config.adminpass; + return success || (!!config.adminpass && response === config.adminpass); } catch { return false; } @@ -383,7 +383,12 @@ export default { if (ipCount === 1) await db.expire(ipKey, 10); if (ipCount > 30) return res.code(429).send({}); - const isAdmin = password === config?.adminpass; + // Fail closed: an unset adminpass must never authenticate, and an omitted + // password field must never coincide with an unset value (undefined === + // undefined). Both the configured value and the supplied one must be + // non-empty and match exactly. + const isAdmin = + !!config?.adminpass && !!password && password === config.adminpass; if (!isAdmin) { const recaptchaOk = await verifyRecaptcha(recaptcha, req); |
