# SPBU Manager — Audit Report

> Generated by project-audit · 2026-04-11
> Methodology: OWASP Top 10 (2021) + Code Quality + Dependency Scanning
> Scanners run: `npm audit` (frontend)
> Files read: ~25 files

---

## Executive Summary

| Severity | Count |
|---|---|
| Critical | 2 |
| High | 7 |
| Medium | 6 |
| Low | 4 |
| Informational | 3 |

**Top 3 must-fix (critical):**
1. **C-01** — `axios < 1.15.0` SSRF bypass — `frontend/package.json`
2. **C-02** — Default superadmin password hardcoded di `config.py` — exposed jika .env kosong/missing

**Overall health:** Codebase mature dan well-structured secara arsitektur. Layering router→service→repository konsisten. Masalah utama ada di **authorization gaps** (IDOR — user manapun bisa akses data SPBU lain), **tidak ada rate limiting** di auth endpoint, satu critical CVE di axios, dan tidak ada test coverage sama sekali. Fixes yang dibutuhkan sebagian besar targeted dan tidak perlu refactor besar.

---

## Critical Findings

### C-01 — axios SSRF via NO_PROXY hostname normalization bypass
- **Category**: A06 Vulnerable Components + A10 SSRF
- **Module**: frontend / http-client
- **File**: `frontend/package.json`
- **Evidence**:
  ```json
  "axios": "^1.14.0"
  ```
  npm audit: GHSA-3p68-rc4w-qgx5 — Axios < 1.15.0 memungkinkan SSRF via bypass NO_PROXY dengan hostname normalization. CVSS: unscored (critical)
- **Risk**: Attacker yang bisa mengontrol URL request (misal via AI assistant/form input) bisa bypass proxy restriction dan hit internal services.
- **Fix**: `npm install axios@^1.15.0 -C frontend`
- **Story points**: 1

### C-02 — Default superadmin password hardcoded di config.py
- **Category**: A05 Security Misconfiguration
- **Module**: backend / config
- **File**: `backend/app/core/config.py:23`
- **Evidence**:
  ```python
  SUPERADMIN_PASSWORD: str = "Admin123!"
  ```
  Jika server production di-deploy tanpa `SUPERADMIN_PASSWORD` di `.env`, password default ini langsung berlaku. Seed script menggunakan nilai ini untuk create superadmin account.
- **Risk**: Full admin takeover jika .env tidak di-setup dengan benar di server baru.
- **Fix**: Hapus default value — gunakan `SUPERADMIN_PASSWORD: str` (required). Server akan gagal start jika tidak di-set. Tambahkan validasi panjang minimum.
- **Story points**: 1

---

## High Findings

### H-01 — IDOR: Setiap user authenticated bisa akses data SPBU manapun
- **Category**: A01 Broken Access Control
- **Module**: backend / semua operational modules
- **File**: `backend/app/services/operational_service.py`, `stock_service.py`, `expenses_service.py`, dll
- **Evidence**:
  ```python
  # list_laporan dipanggil tanpa current_user parameter
  laporan_list, total = await operational_service.list_laporan(
      db, spbu_id, tanggal, shift_id, laporan_status, skip, limit
  )
  # Tidak ada cek: apakah current_user punya assignment di spbu_id ini?
  ```
  Comment di `laporan_shift.py:50`: *"Requires: penjualan:view permission (currently enforced by authentication)"* — artinya permission check **belum diimplementasikan**, hanya auth check.
- **Risk**: Seorang operator SPBU A bisa baca semua data SPBU B cukup dengan mengganti `spbu_id` di URL.
- **Fix**: Tambahkan helper `assert_user_has_spbu_access(user, spbu_id)` yang dicek di awal setiap service function. Superadmin bypass semua.
- **Story points**: 5

### H-02 — Tidak ada permission check di user CRUD endpoints
- **Category**: A01 Broken Access Control
- **Module**: backend / users
- **File**: `backend/app/routers/users.py:23, 38, 52`
- **Evidence**:
  ```python
  @router.post("", ...)  # create_user — tidak ada is_superadmin check di router
  @router.get("", ...)   # list_users — service scopes by SPBU, tapi tidak validasi permission modul
  @router.get("/{user_id}", ...)  # get_user — tidak ada ownership check
  ```
  `POST /api/v1/users` hanya membutuhkan token yang valid — user biasa (operator) bisa membuat user baru.
- **Risk**: Privilege escalation — operator bisa register akun baru dengan permission apapun.
- **Fix**: Tambahkan `require_superadmin` atau `has_permission(users:create)` check di `create_user`. Review endpoint lainnya.
- **Story points**: 2

### H-03 — Tidak ada rate limiting di login endpoint
- **Category**: A07 Authentication Failures
- **Module**: backend / auth
- **File**: `backend/app/routers/auth.py`, `backend/app/services/auth_service.py`
- **Evidence**: Tidak ada import `slowapi`, `limits`, atau middleware rate limiting di seluruh codebase.
- **Risk**: Brute-force attack terhadap akun manapun tanpa hambatan.
- **Fix**: Install `slowapi`, tambahkan `@limiter.limit("10/minute")` di `POST /api/v1/auth/login`. Juga apply ke `/refresh`.
- **Story points**: 2

### H-04 — Internal error messages ter-expose di production
- **Category**: A05 Security Misconfiguration
- **Module**: backend / main
- **File**: `backend/app/main.py:55-59`
- **Evidence**:
  ```python
  @app.exception_handler(Exception)
  async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
      return JSONResponse(
          status_code=500,
          content={"error": {"code": "INTERNAL_ERROR", "message": str(exc)}},
      )
  ```
  `str(exc)` bisa berisi stack trace, nama file, SQL query, atau connection string.
- **Risk**: Information disclosure — attacker bisa pelajari struktur internal dari error messages.
- **Fix**: Di production, return pesan generik. Di development, return detail.
  ```python
  msg = str(exc) if settings.ENVIRONMENT == "development" else "Internal server error"
  ```
- **Story points**: 1

### H-05 — API docs (Swagger/ReDoc) aktif di production
- **Category**: A05 Security Misconfiguration
- **Module**: backend / main
- **File**: `backend/app/main.py:10-16`
- **Evidence**:
  ```python
  app = FastAPI(docs_url="/api/docs", redoc_url="/api/redoc", ...)
  ```
  Tidak ada kondisi berdasarkan `ENVIRONMENT`. Di production, `/api/docs` ter-expose ke publik (diprotect Nginx htpasswd, tapi htpasswd mudah brute-force dan bypass).
- **Fix**:
  ```python
  docs_url="/api/docs" if not settings.is_production else None,
  redoc_url="/api/redoc" if not settings.is_production else None,
  ```
- **Story points**: 1

### H-06 — Uploaded files bisa diakses tanpa autentikasi
- **Category**: A01 Broken Access Control
- **Module**: backend / file-storage
- **File**: `backend/app/main.py:51`
- **Evidence**:
  ```python
  app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
  ```
  Semua file upload (bukti transfer, foto absensi, foto housekeeping, dokumen penebusan) bisa diakses siapa saja jika tahu URL-nya. URL dibentuk dari pola yang predictable: `/uploads/{env}/{spbu_code}/{tipe}/{tahun}{bulan}-{uuid8}-{filename}`.
- **Risk**: Data leak — foto karyawan, dokumen keuangan, bukti transfer bisa diakses publik.
- **Fix**: Hapus `StaticFiles` mount. Buat endpoint `/api/v1/files/{path}` dengan auth check. Atau gunakan pre-signed URLs jika pakai Google Drive.
- **Story points**: 3

### H-07 — Next.js 16.2.2 DoS via Server Components
- **Category**: A06 Vulnerable Components
- **Module**: frontend
- **File**: `frontend/package.json`
- **Evidence**:
  npm audit: GHSA-q4gf-8mx6-v5v3 — Next.js 16.0.0-beta.0 – 16.2.2 rentan terhadap DoS via Server Components. Requires 16.2.3+.
- **Fix**: `npm install next@^16.2.3 -C frontend`
- **Story points**: 1

---

## Medium Findings

### M-01 — Tidak ada security response headers
- **Category**: A05 Security Misconfiguration
- **Module**: backend / nginx
- **Evidence**: Tidak ada `X-Frame-Options`, `X-Content-Type-Options`, `Content-Security-Policy`, `Strict-Transport-Security`, `Referrer-Policy` di FastAPI middleware atau Nginx config.
- **Fix**: Tambahkan di Nginx config atau via FastAPI middleware (`starlette-security-headers`).
- **Story points**: 2

### M-02 — Access token JWT tidak memvalidasi audience/issuer
- **Category**: A07 Authentication Failures
- **Module**: backend / security
- **File**: `backend/app/core/security.py:41`
- **Evidence**:
  ```python
  payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
  # Tidak ada options={"require": ["aud", "iss"]}
  ```
- **Risk**: Token dari sistem lain yang menggunakan SECRET_KEY yang sama bisa diterima.
- **Fix**: Tambahkan `aud` dan `iss` claim saat create token, validate saat decode.
- **Story points**: 2

### M-03 — `deactivate_user` tidak membatasi aktor non-superadmin
- **Category**: A01 Broken Access Control
- **Module**: backend / users
- **File**: `backend/app/services/user_service.py:73-80`
- **Evidence**:
  ```python
  async def deactivate_user(db, user_id, actor):
      # Hanya cek: actor tidak bisa deactivate superadmin
      # Tapi operator/manager BISA deactivate sesama regular user!
      if user.is_superadmin and not actor.is_superadmin:
          raise PermissionError(...)
  ```
- **Fix**: Tambahkan `if not actor.is_superadmin: raise PermissionError(...)` di baris pertama.
- **Story points**: 1

### M-04 — `remove_assignment` tidak ada permission check
- **Category**: A01 Broken Access Control
- **Module**: backend / users
- **File**: `backend/app/services/user_service.py:116-121`
- **Evidence**:
  ```python
  async def remove_assignment(db, user_id, spbu_id, actor) -> None:
      deleted = await user_repository.delete_assignment(db, user_id, spbu_id)
      # Tidak ada cek: apakah actor boleh remove assignment di spbu_id ini?
  ```
- **Fix**: Tambahkan check bahwa actor adalah superadmin ATAU actor punya assignment di spbu_id yang sama.
- **Story points**: 1

### M-05 — CORS origin hardcoded localhost, tidak dikonfigurasi untuk production
- **Category**: A05 Security Misconfiguration
- **Module**: backend / main
- **File**: `backend/app/main.py:18-24`
- **Evidence**:
  ```python
  allow_origins=["http://localhost:8007", "http://127.0.0.1:8007"],
  ```
  Production URL (`https://spbu.goteku.com`) tidak ada di list.
- **Risk**: Di production, CORS akan block semua cross-origin request dari frontend, KECUALI jika Nginx meng-override header (yang bisa menyebabkan double-CORS).
- **Fix**: Baca dari env: `allow_origins=settings.CORS_ORIGINS.split(",")`.
- **Story points**: 2

### M-06 — Google Drive Folder ID hardcoded di config
- **Category**: A05 Security Misconfiguration
- **Module**: backend / config
- **File**: `backend/app/core/config.py:17`
- **Evidence**:
  ```python
  GDRIVE_ROOT_FOLDER_ID: str = "1wnTodR_XdkNdc9vjQH2GHn4Q92FjESjl"
  ```
- **Risk**: Folder ID ter-expose di git history. Siapapun dengan token Google Drive bisa akses folder ini.
- **Fix**: Hapus default value, jadikan required env var atau beri default empty string dengan validasi di startup.
- **Story points**: 1

---

## Low Findings

### L-01 — lodash code injection via `_.template` (dev dep)
- **Category**: A06 Vulnerable Components
- **Module**: frontend / devDependencies
- **Evidence**: npm audit GHSA-r5fr-rjxr-66jc — lodash <= 4.17.23 via `@tabler/icons-webfont`. CVSS 8.1, tapi ini dev dependency (icon font generation).
- **Risk**: Low — hanya terkena saat build, bukan runtime production.
- **Fix**: `npm update @tabler/icons-webfont -C frontend`
- **Story points**: 1

### L-02 — @xmldom/xmldom XML injection (dev dep)
- **Category**: A06 Vulnerable Components
- **Module**: frontend / devDependencies
- **Evidence**: npm audit GHSA-wh4c-j3r5-mjhp — via `@tabler/icons-webfont`. Dev dep only.
- **Risk**: Low — build-time only.
- **Fix**: Update `@tabler/icons-webfont` (sama dengan L-01).
- **Story points**: 1

### L-03 — Cookie tidak di-set dengan Secure/HttpOnly/SameSite dari backend
- **Category**: A02 Cryptographic Failures / A07 Auth Failures
- **Module**: backend / auth
- **Evidence**: Token disimpan di cookie `access_token` tapi atribut `HttpOnly`, `Secure`, `SameSite` di-set oleh frontend/Nginx, bukan backend. Perlu verifikasi di frontend auth handler.
- **Fix**: Verifikasi bahwa response dari `/api/v1/auth/login` menyertakan `Set-Cookie` header dengan atribut yang benar. Jika belum, set di FastAPI response.
- **Story points**: 2

### L-04 — Tidak ada .env.example di repository
- **Category**: A05 Security Misconfiguration / Developer Experience
- **Module**: backend
- **Evidence**: `backend/.env` di-gitignore dengan benar, tapi tidak ada `.env.example` sebagai template. Dev baru tidak tahu variabel apa yang diperlukan.
- **Fix**: Buat `backend/.env.example` dengan semua keys (nilai placeholder). Commit ke git.
- **Story points**: 1

---

## Informational

### I-01 — Zero test coverage
- **Module**: backend + frontend
- **Evidence**: `pyproject.toml` ada `pytest` di dev deps, tapi tidak ada file test (`find . -name "test_*.py"` → kosong). Frontend juga tidak ada jest/vitest.
- **Note**: Ini blocker untuk confident refactoring. Sebelum eksekusi item besar (H-01), tambahkan smoke test dulu.

### I-02 — AI Assistant endpoint tidak ada input sanitization
- **Module**: backend / assistant
- **File**: `backend/app/assistant/service.py`
- **Evidence**: `user_message` dikirim langsung ke LLM tanpa filtering. Prompt injection dari user ke LLM possible.
- **Note**: Resiko rendah untuk aplikasi internal, tapi perlu dimonitor jika sistem dibuka ke publik.

### I-03 — Audit log TODO tapi bisnis rule #14 mewajibkannya
- **Module**: backend
- **Evidence**: CLAUDE.md rule #14: "Audit log wajib: create, update, delete, submit, recall, review, unlock, import CSV". Model & tabel audit_log belum ada.
- **Note**: Ini compliance gap, bukan security vulnerability langsung. Tapi tanpa audit log, forensics saat insiden sangat terbatas.

---

## Scanner Output Summary

| Scanner | High | Critical | Total |
|---|---|---|---|
| npm audit (frontend) | 6 | 1 | 7 |
| bandit (backend) | tidak tersedia | tidak tersedia | — |
| pip-audit (backend) | tidak tersedia | tidak tersedia | — |

> bandit dan pip-audit tidak terinstall. Disarankan install dan run sebagai bagian dari CI.
