# 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 OCDP Platform ... ``` ### Impact Not a direct vulnerability, but misconfigured health checks can cause false positives in monitoring/load balancer health checks. It also means the SPA is served at `/health`, which is unexpected. ### Recommendation Implement a dedicated health endpoint that returns `{"status":"ok"}` with appropriate content type, or remove the `/health` route if not needed. --- ## Tests Passed (No Issues Found) | Test | Result | |------|--------| | **1. Unauthenticated Access** | **PASS** - All business endpoints return 401 | | **2. JWT Token Manipulation** | **PASS** - Tampered tokens, alg=none, invalid formats all rejected (401) | | **3. XSS/SQLi Testing** | **PASS** - Script injection, SQLi patterns safely handled | | **4. IDOR - Instance Access** | **PASS** - No instances deployed to test; cluster/registry isolation confirmed working | | **5. Sensitive Data Masking** | **PASS** - Cluster certs/keys and registry passwords masked as `••••••••` | | **6. Self-Registration** | **PASS** - Registration endpoint requires authentication (401) | | **7. Path Traversal** | **PASS** - Path traversal attempts return index.html (not /etc/passwd) | | **8. Admin Permission Escalation** | **PASS** - Regular users blocked from admin endpoints (403) | --- ## Summary | Severity | Count | Findings | |----------|-------|----------| | Critical | 0 | — | | High | 0 | — | | **Medium** | **2** | User enumeration, No rate limiting | | **Low** | **4** | Server version disclosure, Permissive CORS, Missing security headers, `/health` returns HTML | | **Total** | **6** | | The platform's core security controls (authentication, JWT validation, authorization, sensitive data masking) are properly implemented. The main areas for improvement are authentication hardening (rate limiting, user enumeration) and HTTP security hardening (headers, CORS).