Skip to content

[Security Fix] Multiple Critical/High Vulnerabilities: Auth Bypass, Weak Verification, and DoS Vectors #852

@h411265

Description

@h411265

Which package is this bug report for?

general

Issue description

Hi Statsify team,

While conducting a security audit of the Statsify monorepo, I identified several vulnerabilities ranging from Critical to High severity. These include a potential authentication bypass in production and a weak verification code generation logic that is vulnerable to brute-force attacks.

🚨 Identified Vulnerabilities

  1. CRITICAL: Authentication Bypass via ignoreAuth flag
    In apps/api/src/auth/auth.guard.ts, a global config flag can completely disable authentication. If misconfigured in production, this grants unrestricted access to all API endpoints.

  2. CRITICAL: Insecure Randomness & Brute-force in Verification
    apps/verify-server/src/generate-code.ts uses Math.random() and a 4-digit range, which is not cryptographically secure. An attacker can brute-force all possible combinations within the 5-minute expiry window to hijack account linkings.

  3. HIGH: Redis DoS via Uncapped Range Queries
    PlayerGroupDto lacks a maximum limit on the end parameter, allowing an attacker to request millions of records and exhaust Redis/API memory.

  4. HIGH: Missing Ownership Check on Session Reset
    The /session reset endpoint only checks for a valid MEMBER key but doesn't verify if the target player UUID belongs to the caller.

(Additional High-severity issues include disabled CSP headers, missing global security headers, and an overly permissive CORS policy).


💡 The Solution (AI Repair Prompt)

To help you fix these issues efficiently, I have generated a structured AI Repair Prompt. You can paste this directly into Cursor, Copilot, or Claude to apply the fixes across the monorepo:

AI Repair Prompt:

Fix the following 7 security vulnerabilities (2 CRITICAL, 5 HIGH) in the
Statsify monorepo. Apply every fix without breaking existing functionality.
The test suite must still pass after all changes.


Fix 1 (CRITICAL) — Remove the ignoreAuth authentication bypass

File: apps/api/src/auth/auth.guard.ts

Delete this entire block:
if (await config("api.ignoreAuth", { required: false })) {
return true;
}

Also remove the "api.ignoreAuth" key from config.schema.js (or equivalent
config schema file). This flag must not exist in any code path reachable from
production. For local development without auth, use a .env.local file that is
explicitly excluded from production deployments — never a runtime config flag.


Fix 2 (CRITICAL) — Replace Math.random() with crypto.randomInt() and add brute-force lockout

File: apps/verify-server/src/generate-code.ts

Replace:
const createCode = () =>
Math.floor(Math.random() * (9999 - 1000 + 1) + 1000).toString();

With:
import { randomInt } from "crypto";
const createCode = () => randomInt(1000, 10000).toString();

Then add brute-force protection to the verify endpoint:
File: apps/api/src/user/user.controller.ts (PUT /user)

  • Add an "attempts" field (Number, default 0) to the VerifyCode schema.
  • On each failed code match: increment "attempts". If "attempts" >= 5,
    delete the VerifyCode document and return HTTP 429 Too Many Requests.
  • On a successful match: delete the VerifyCode document immediately.

Fix 3 (HIGH) — Cap PlayerGroupDto range to prevent Redis DoS

File: apps/api/src/dtos/player-group.dto.ts

Add a @max constraint and cross-field validation:

@min(0)
public start: number;

@max(100) // add this
@min(1)
public end: number;

Also add a class-level validator ensuring end - start <= 100. Use a custom
@ValidatorConstraint or a @ValidateIf guard to reject any request where the
range exceeds 100 entries.


Fix 4 (HIGH) — Enforce ownership check on session reset endpoint

File: apps/api/src/session/session.controller.ts (PATCH /session)

Currently any MEMBER-level API key can reset any player's session. Fix:

  1. Extract the caller's Discord user ID from the authenticated request context.
  2. Query the User collection for the Minecraft UUID linked to that Discord ID.
  3. If the requested player tag/UUID does not match the caller's linked UUID,
    return HTTP 403 Forbidden: "You can only reset your own session."
  4. Only call getAndReset() after ownership is confirmed.

Fix 5 (HIGH) — Replace the disabled CSP on the API docs endpoint

File: apps/api/src/app.controller.ts

Replace the existing Content-Security-Policy header value (which currently
uses "default-src * 'unsafe-inline' 'unsafe-eval'" — effectively no CSP)
with a strict per-request nonce-based policy:

import { randomBytes } from "crypto";
const nonce = randomBytes(16).toString("base64");

@Header(
"Content-Security-Policy",
default-src 'none'; +
script-src 'nonce-${nonce}' https://cdn.jsdelivr.net; +
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; +
font-src https://fonts.gstatic.com; +
img-src 'self' data:; +
frame-ancestors 'none'; +
object-src 'none'; +
base-uri 'self';
)

Generate a fresh nonce on every request, not once at module load time.


Fix 6 (HIGH) — Add global HTTP security headers via @fastify/helmet

File: apps/api/src/index.ts (NestJS bootstrap)

  1. Install the package:
    pnpm add @fastify/helmet --filter @statsify/api

  2. Register helmet in the bootstrap function before app.listen():

    import helmet from "@fastify/helmet";

    await app.register(helmet, {
    contentSecurityPolicy: false, // CSP is handled per-route (Fix 5)
    crossOriginEmbedderPolicy: false,
    });

This adds X-Content-Type-Options: nosniff, X-Frame-Options: DENY,
Strict-Transport-Security, Referrer-Policy, and Permissions-Policy to
every API response automatically.


Fix 7 (HIGH) — Explicitly configure CORS with an origin allowlist

File: apps/api/src/index.ts (NestJS bootstrap)

Replace implicit Fastify CORS behavior with an explicit allowlist:

app.enableCors({
origin: [
"https://statsify.net",
"https://www.statsify.net",
...(process.env.NODE_ENV === "development"
? ["http://localhost:3000"]
: []),
],
methods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Authorization", "Content-Type"],
credentials: false,
});


After applying all 7 fixes, verify the following:

  1. A request to any protected endpoint without an API key returns HTTP 401,
    confirming the ignoreAuth bypass is fully removed.
  2. GET /player/group?start=0&end=101 returns HTTP 400 (validation error),
    confirming the range cap is enforced.
  3. PATCH /session for a player UUID not owned by the caller returns HTTP 403.
  4. GET / (API docs) returns a Content-Security-Policy header that does NOT
    contain "unsafe-eval" or "unsafe-inline" in script-src.
  5. Every API response includes the header X-Content-Type-Options: nosniff.
  6. A cross-origin request from an unlisted origin is rejected with a CORS error.
  7. The full test suite passes with no regressions.

AI Repair Prompt:

I have the detailed implementation logic for each fix ready. Let me know if you would like me to provide the specific code blocks or open a PR!

Best regards,
[Vincent/Vibe Guard]

Operating system

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions