fix: prevent admin escalation when AUTH_ENABLED=false

When auth was disabled, users could log out, reach the login page,
and authenticate with the default admin/admin credentials to gain
full admin privileges — defeating the purpose of AUTH_ENABLED=false.

Defense-in-depth fix across five layers:
- Skip ensureDefaultAdmin() when auth is disabled (no admin user seeded)
- Return 403 from POST /api/auth/login when auth is disabled
- Return synthetic anonymous user from GET /api/auth/session when auth is disabled
- Hide logout button in settings when auth is disabled
- Redirect /login and /change-password to / via AuthGuard when auth is disabled

Closes #90
This commit is contained in:
ashim-hq
2026-04-23 14:45:04 +08:00
parent 19df740880
commit 7047ce5fae
4 changed files with 45 additions and 15 deletions
+4 -2
View File
@@ -36,8 +36,10 @@ import { userFileRoutes } from "./routes/user-files.js";
runMigrations();
console.log("Database initialized");
// Create default admin user if no users exist
await ensureDefaultAdmin();
// Create default admin user if no users exist and auth is enabled
if (env.AUTH_ENABLED) {
await ensureDefaultAdmin();
}
function ensureInstanceId() {
const existing = db
+20 -1
View File
@@ -160,6 +160,10 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
"/api/auth/login",
{ config: { rateLimit: { max: getLoginAttemptLimit, timeWindow: "1 minute" } } },
async (request: FastifyRequest, reply: FastifyReply) => {
if (!env.AUTH_ENABLED) {
return reply.status(403).send({ error: "Authentication is disabled" });
}
const body = request.body as { username?: string; password?: string } | null;
if (!body?.username || !body?.password) {
@@ -230,6 +234,22 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
// GET /api/auth/session
app.get("/api/auth/session", async (request: FastifyRequest, reply: FastifyReply) => {
if (!env.AUTH_ENABLED) {
return reply.send({
user: {
id: "anonymous",
username: "anonymous",
role: "user",
mustChangePassword: false,
permissions: getPermissions("user"),
analyticsEnabled: null,
analyticsConsentShownAt: null,
analyticsConsentRemindAt: null,
},
expiresAt: null,
});
}
const token = extractToken(request);
if (!token) {
return reply.status(401).send({ error: "No session token provided" });
@@ -238,7 +258,6 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
const session = db.select().from(schema.sessions).where(eq(schema.sessions.id, token)).get();
if (!session || session.expiresAt < new Date()) {
// Clean up expired session if it exists
if (session) {
db.delete(schema.sessions).where(eq(schema.sessions.id, token)).run();
}