# OCDP Security Audit Report **Date:** 2026-05-11 **Target:** http://10.6.80.114:18080 **API Base:** http://10.6.80.114:18080/api/v1 --- ## Finding 1: User Enumeration via Login Error Messages | Field | Value | |-------|-------| | **Test** | Authentication Error Disclosure | | **Severity** | **Medium** | | **Endpoint** | `POST /api/v1/auth/login` | | **Status** | Confirmed | ### What I Did ```bash # Non-existent user curl -s -X POST http://10.6.80.114:18080/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"nonexistent_user_xyz","password":"test123"}' # Existing user with wrong password curl -s -X POST http://10.6.80.114:18080/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"wrongpassword"}' ``` ### Expected Both requests should return the same generic error message (e.g., "Invalid credentials") to prevent username enumeration. ### Actual - Non-existent user: `{"error":"Login failed","message":"user not found","code":401}` - Existing user: `{"error":"Login failed","message":"invalid password","code":401}` The error messages are different, allowing an attacker to determine whether a username exists in the system. ### Impact An attacker can enumerate valid usernames by observing the error message difference. This is the first step in a targeted brute force or credential stuffing attack. ### Recommendation Return identical error messages for both cases, e.g., `"Invalid username or password"`. --- ## Finding 2: No Rate Limiting on Login Endpoint | Field | Value | |-------|-------| | **Test** | Brute Force Protection | | **Severity** | **Medium** | | **Endpoint** | `POST /api/v1/auth/login` | | **Status** | Confirmed | ### What I Did ```bash for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code}" \ -X POST http://10.6.80.114:18080/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"wrongpassword"}' done ``` ### Expected After a threshold (e.g., 5 failed attempts), the server should return HTTP 429 Too Many Requests or temporarily lock the account. ### Actual All 10 rapid sequential attempts returned HTTP 401. No rate limiting, no account lockout, no progressive delay. ### Impact An attacker can brute force passwords without restriction. Combined with Finding 1 (user enumeration), the attack surface is increased. ### Recommendation - Implement rate limiting on the login endpoint (e.g., max 5 attempts per minute per IP). - Consider account lockout after N failed attempts. - Add progressive response delays after repeated failures. --- ## Finding 3: Server Version Disclosure | Field | Value | |-------|-------| | **Test** | Information Disclosure | | **Severity** | **Low** | | **Endpoint** | All (HTTP response headers) | | **Status** | Confirmed | ### What I Did ```bash curl -s -D - http://10.6.80.114:18080/ | head -10 ``` ### Expected Server header should be generic (e.g., `Server: nginx`) or removed entirely. ### Actual ```http Server: nginx/1.27.5 ``` ### Impact Knowing the exact nginx version helps attackers target known vulnerabilities for that specific version. ### Recommendation Disable or obfuscate the Server header in nginx configuration: ```nginx server_tokens off; ``` --- ## Finding 4: Permissive CORS Policy | Field | Value | |-------|-------| | **Test** | CORS Misconfiguration | | **Severity** | **Low** | | **Endpoint** | All API endpoints | | **Status** | Confirmed | ### What I Did ```bash curl -s -D - http://10.6.80.114:18080/api/v1/auth/login \ -X POST -H "Content-Type: application/json" \ -d '{"username":"test","password":"test"}' ``` ### Expected CORS `Access-Control-Allow-Origin` should be restricted to the application's origin (e.g., `http://10.6.80.114:18080`) rather than allowing all origins. ### Actual ```http Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With Access-Control-Max-Age: 86400 ``` ### Impact Any website can make cross-origin requests to the API. If a user is logged in, a malicious site could potentially make authenticated API calls on their behalf (CSRF-style attack, though mitigated by the Bearer token requirement). ### Recommendation Restrict `Access-Control-Allow-Origin` to the specific frontend origin(s) instead of `*`. --- ## Finding 5: Missing Security Headers | Field | Value | |-------|-------| | **Test** | Security Headers Audit | | **Severity** | **Low** | | **Endpoint** | All | | **Status** | Confirmed | ### What I Did ```bash curl -s -D - http://10.6.80.114:18080/ | head -20 ``` ### Expected Security headers should include: - `Strict-Transport-Security` - `X-Content-Type-Options: nosniff` - `X-Frame-Options: DENY` - `Content-Security-Policy` ### Actual None of these security headers are present in responses. ### Impact Increases attack surface for clickjacking, MIME-type confusion, and XSS attacks. ### Recommendation Add the following headers to nginx configuration: ``` add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "0" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always; ``` --- ## Finding 6: `/health` Endpoint Returns HTML Instead of Health Status | Field | Value | |-------|-------| | **Test** | Health Endpoint Behavior | | **Severity** | **Low** | | **Endpoint** | `GET /health` | | **Status** | Confirmed | ### What I Did ```bash curl -s http://10.6.80.114:18080/health ``` ### Expected A health check endpoint should return a structured JSON response (e.g., `{"status":"healthy"}`) with HTTP 200. ### Actual Returns the full `index.html` SPA page with HTTP 200: ```html