aboutsummaryrefslogtreecommitdiffstats
path: root/routes
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-07 22:59:21 -0700
committerAdam Soltys <[email protected]>2026-08-07 23:29:25 -0700
commitd513ab75131bfaa16b1f0820d0fb18ad9e7f6ab1 (patch)
treebe028867721f69a1dc102e33b999cd6abdebf522 /routes
parent9f99bbdb765fc70373943930d543ce1cde7a5f35 (diff)
downloadcoinos-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.ts2
-rw-r--r--routes/payments.ts2
-rw-r--r--routes/users.ts9
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);