# 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
<!doctype html>
<html lang="en">
  <head>
    <title>OCDP Platform</title>
    ...
```

### 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).
