Skip to content

security: P0 hardening — authorization, JWT secret, role escalation, document ownership, FIR access control#440

Open
techwallahexplorer wants to merge 5 commits into
viru0909-dev:mainfrom
techwallahexplorer:fix/security-p0-hardening
Open

security: P0 hardening — authorization, JWT secret, role escalation, document ownership, FIR access control#440
techwallahexplorer wants to merge 5 commits into
viru0909-dev:mainfrom
techwallahexplorer:fix/security-p0-hardening

Conversation

@techwallahexplorer
Copy link
Copy Markdown

Security Hardening PR — P0 Critical Fixes

This PR addresses 5 critical (P0) security vulnerabilities identified in a production-readiness audit. The system was not safe for public deployment in its prior state. Each fix is documented with the specific attack vector it closes.


Fix 1 — application.properties: Remove hardcoded JWT secret

Attack: The JWT signing secret nyaysetu-2024-secure-jwt-signing-key-minimum-256-bits-required was committed to a public repository with a hardcoded default. Any attacker could forge a valid JWT for any user (including JUDGE, ADMIN) using this string.

Fix: jwt.secret=${JWT_SECRET} — no default value. The application will now refuse to start if JWT_SECRET is not set as an environment variable.

Additional hardening in this file:

  • spring.main.lazy-initialization=false — config errors now surface at startup, not silently at first user request
  • Upload limit reduced from 200MB → 20MB (DoS vector)
  • Spring Security and Web logging downgraded from DEBUG/TRACE → WARN (JWT tokens were being written to logs)
  • Actuator health.show-details=never (was always — exposed DB and disk state)
  • Flyway validate-on-migrate=true re-enabled (was disabled — schema drift went undetected)

Fix 2 — SecurityConfig.java: Replace anyRequest().permitAll() with role-based rules

Attack: Every API endpoint — FIR records, court documents, judge rulings, police investigations — was fully accessible without any authentication token. The JWT filter ran but its rejection was overridden by permitAll().

Fix: Explicit authorization rules:

/api/auth/** + /actuator/health + /swagger-ui/** → public
/api/police/**                                   → POLICE or ADMIN only
/api/judge/**                                    → JUDGE or ADMIN only
everything else                                  → authenticated (valid JWT required)

@EnableMethodSecurity added to enable @PreAuthorize on individual methods for fine-grained control.


Fix 3 — AuthController.java: Prevent role self-assignment + fix face endpoint auth

Attack (Finding 23): register() accepted role from the request body. Any caller could POST {"role": "JUDGE"} and receive a JUDGE-level JWT immediately.

Fix: role field from request body is intentionally ignored. All self-registrations are hardcoded to Role.LITIGANT.

Attack (Finding 2): /face/enroll, /face/disable, /face/status were unauthenticated and accepted userId from the request body/params. An attacker could enroll their own face as any judge's account, then log in as that judge.

Fix: All three endpoints now require a valid JWT. The userId is derived from authentication.getName() — callers cannot target other accounts.

Attack (Finding 3): /forgot-password returned different status codes and echoed the submitted email back, acting as a user enumeration oracle.

Fix: Always returns the same 200 response body regardless of whether the email is registered.


Fix 4 — DocumentManagementController.java: Add ownership checks to all document endpoints

Attack (Finding 4): /documents/{id}/download, /{id}, /{id}/analysis, /{id}/certificate had no ownership check. Any authenticated user could access sealed legal evidence for any case by guessing or enumerating UUIDs.

Fix: All endpoints now call isAuthorizedForDocument() before serving data.

Access policy: document owner OR JUDGE OR POLICE OR ADMIN.

Internal exception messages are no longer forwarded to HTTP response bodies.


Fix 5 — FirController.java: Enforce POLICE role on FIR operations + fix findAll() OOM

Attack (Finding 6): getPendingFirs() and updateFirStatus() had no role check. A litigant who filed an FIR could approve or reject it themselves, or deny it to force a case to be dropped.

Fix: Class-level @PreAuthorize("hasAnyRole('POLICE', 'ADMIN')") enforces this on every method in the controller (defence-in-depth on top of SecurityConfig path rules).

DoS fix: getSummonsTasks() called caseRepository.findAll() — loading the entire cases table into JVM heap and filtering in-memory. At scale this causes OOM crashes. Replaced with caseRepository.findBySummonsStatus("IN_TRANSIT").

Note: CaseRepository needs the method List<CaseEntity> findBySummonsStatus(String status) added (Spring Data JPA derives this automatically from the method name — no SQL required).


⚠️ Required Actions Before Merging

  1. Set JWT_SECRET environment variable on Render/Railway/your platform before deploying. Generate with: openssl rand -hex 64
  2. Add findBySummonsStatus to CaseRepository (Fix 5 depends on it)
  3. Verify DocumentDto has getUploadedBy() returning the owner's Long user ID (Fix 4 depends on it)
  4. Rotate the old JWT secret — it is in git history. All existing sessions will be invalidated once the new secret is deployed, which is the correct behavior.

…ization

CRITICAL FIX: Every endpoint was publicly accessible — authenticated or not.
anyRequest().permitAll() is replaced with:
- Explicit public whitelist for /api/auth/** and docs
- /api/police/** restricted to POLICE/ADMIN roles
- /api/judge/** restricted to JUDGE/ADMIN roles
- All other requests require a valid JWT

Also: @EnableMethodSecurity added for @PreAuthorize support on methods.
CORS allowed headers narrowed from wildcard to explicit list.
…oint auth

Finding 23 — Role self-assignment: req.getRole() accepted by register()
allowed any caller to become JUDGE/POLICE/ADMIN. Now hardcoded to LITIGANT.

Finding 2 — Unauthenticated face management: /face/enroll, /face/disable,
/face/status accepted userId from request body/params with no authentication.
All three now require JWT and derive userId from the authenticated principal.

Finding 3 — User enumeration: /forgot-password returned different responses
for registered vs unregistered emails. Now always returns the same body.
Finding 4 — Document exfiltration: /documents/{id}/download, /{id},
/{id}/analysis, /{id}/certificate had no ownership check — any authenticated
user could access any legal document by guessing its UUID.

All document endpoints now call isAuthorizedForDocument() before serving data.
Access policy: document owner | JUDGE | POLICE | ADMIN.

Internal error messages are no longer echoed to callers (stack trace leak).
…findAll() DoS

Finding 6 — FIR access control: getPendingFirs() and updateFirStatus() had
no role check, allowing any litigant to approve/reject their own FIR.
Class-level @PreAuthorize("hasAnyRole('POLICE','ADMIN')") now enforces this
on every method, with defence-in-depth beyond SecurityConfig path rules.

Finding 6b — OOM DoS: getSummonsTasks() called caseRepository.findAll()
loading the entire cases table into JVM heap, then filtered in-memory.
Replaced with caseRepository.findBySummonsStatus("IN_TRANSIT") — requires
adding the derived query method to CaseRepository (no custom SQL needed).

Internal exception messages are no longer propagated to HTTP responses.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 17, 2026

@techwallahexplorer is attempting to deploy a commit to the CodeBlooded's projects Team on Vercel.

A member of the Team first needs to authorize it.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nyaysetu Ready Ready Preview, Comment May 18, 2026 2:04am

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant