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
-
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.
-
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.
-
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.
-
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:
- Extract the caller's Discord user ID from the authenticated request context.
- Query the User collection for the Minecraft UUID linked to that Discord ID.
- 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."
- 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)
-
Install the package:
pnpm add @fastify/helmet --filter @statsify/api
-
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:
- A request to any protected endpoint without an API key returns HTTP 401,
confirming the ignoreAuth bypass is fully removed.
- GET /player/group?start=0&end=101 returns HTTP 400 (validation error),
confirming the range cap is enforced.
- PATCH /session for a player UUID not owned by the caller returns HTTP 403.
- GET / (API docs) returns a Content-Security-Policy header that does NOT
contain "unsafe-eval" or "unsafe-inline" in script-src.
- Every API response includes the header X-Content-Type-Options: nosniff.
- A cross-origin request from an unlisted origin is rejected with a CORS error.
- 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
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
CRITICAL: Authentication Bypass via
ignoreAuthflagIn
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.CRITICAL: Insecure Randomness & Brute-force in Verification
apps/verify-server/src/generate-code.tsusesMath.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.HIGH: Redis DoS via Uncapped Range Queries
PlayerGroupDtolacks a maximum limit on theendparameter, allowing an attacker to request millions of records and exhaust Redis/API memory.HIGH: Missing Ownership Check on Session Reset
The
/sessionreset endpoint only checks for a validMEMBERkey 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:
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)
delete the VerifyCode document and return HTTP 429 Too Many Requests.
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:
return HTTP 403 Forbidden: "You can only reset your own session."
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)
Install the package:
pnpm add @fastify/helmet --filter @statsify/api
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:
confirming the ignoreAuth bypass is fully removed.
confirming the range cap is enforced.
contain "unsafe-eval" or "unsafe-inline" in script-src.
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