286 lines
8.4 KiB
Markdown
286 lines
8.4 KiB
Markdown
# Security Audit Report — GuardDog Nexus
|
||
|
||
**Date:** 2026-05-10
|
||
**Auditor:** Automated security audit
|
||
**Last updated:** 2026-05-11
|
||
**Scope:** Full codebase review — security vulnerabilities, logic errors, missing controls
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
| Severity | Count | Fixed | Rejected | Remaining |
|
||
|----------|-------|-------|----------|-----------|
|
||
| CRITICAL | 5 | 2 | 2 | 1 |
|
||
| HIGH | 7 | 2 | 3 | 2 |
|
||
| MEDIUM | 8 | 3 | 0 | 5 |
|
||
| LOW | 6 | 2 | 0 | 4 |
|
||
| **Total**| **26**| **9** | **5** | **12** |
|
||
|
||
---
|
||
|
||
## CRITICAL (5)
|
||
|
||
### C1. SSRF via webhook downloadUrl ✅ FIXED
|
||
**Severity:** CRITICAL
|
||
**Fix:** `NEXUS_ALLOWED_HOSTS` env var + `_validate_download_url()` in `core/nexus.py`.
|
||
|
||
**Problem:** `downloadUrl` из webhook-пэйлода передаётся напрямую в `httpx.AsyncClient.get()` без валидации.
|
||
|
||
```python
|
||
download_url = asset.get("downloadUrl") or _build_download_url(repository, asset_path)
|
||
# ...
|
||
response = await client.get(download_url) # no validation
|
||
```
|
||
|
||
**Real-world impact:** Атакующий отправляет webhook с `downloadUrl: "http://169.254.169.254/latest/meta-data/iam/security-credentials/"` → сервер скачивает IAM-учётные данные облака.
|
||
|
||
**Fix:** Validate URL scheme (http/https only), block private IP ranges (10.x, 172.16.x, 192.168.x, 127.x, 169.254.x, ::1), optionally whitelist domain against `config.nexus_url`.
|
||
|
||
---
|
||
|
||
### C2. Webhook secret not enforced by default ❌ ACCEPTED RISK
|
||
**Severity:** CRITICAL
|
||
**Decision:** Внутренний сервис, секрет опционален.
|
||
|
||
---
|
||
|
||
### C3. Default admin credentials ✅ FIXED
|
||
**Severity:** CRITICAL
|
||
**Fix:** Убран BasicAuth из всех запросов к Nexus (анонимный доступ).
|
||
|
||
---
|
||
|
||
### C4. XSS via LLM report verdict ❌ NOT DANGEROUS
|
||
**Severity:** CRITICAL — downgraded to INFO
|
||
**Decision:** Jinja2 autoescape блокирует инъекцию в атрибутах.
|
||
|
||
---
|
||
|
||
### C5. LLM Prompt Injection ⚠️ PARTIALLY MITIGATED
|
||
**Severity:** CRITICAL
|
||
**Mitigation:** System prompt gives priority to system instructions. Raw finding data still in user message.
|
||
|
||
---
|
||
|
||
## HIGH (7)
|
||
|
||
### H1. No rate limiting ❌ REJECTED
|
||
### H2. Path traversal ⚠️ LOW RISK
|
||
**Severity:** HIGH — downgraded
|
||
**Analysis:** `os.path.basename("../../../etc/passwd")` → `"passwd"`, traversal невозможен.
|
||
|
||
---
|
||
|
||
### H3. Sensitive data in API ❌ REJECTED (source_ip is a feature)
|
||
### H4. No authentication ❌ REJECTED (internal service)
|
||
### H5. Memory leak in locks ✅ FIXED (bg cleanup every 30min)
|
||
### H6. Race condition in URL locking ✅ FIXED (DB re-check inside lock)
|
||
### H7. CSV export bounded ❌ REJECTED (acceptable for internal tool)
|
||
|
||
---
|
||
|
||
## MEDIUM (8)
|
||
|
||
### M1. No LLM response schema validation
|
||
**Severity:** MEDIUM
|
||
**File:** `core/llm.py:80-82`
|
||
|
||
**Problem:** LLM response parsed as JSON but not validated against schema. Missing `report.verdict` → Jinja2 renders empty string → CSS broken.
|
||
|
||
**Fix:** Pydantic model для валидации LLM response.
|
||
|
||
---
|
||
|
||
### M2. No CSRF protection
|
||
**Severity:** MEDIUM
|
||
**File:** `routes/web.py:205-274`
|
||
|
||
**Problem:** POST `/api/v1/findings/{id}/analyze` без CSRF token.
|
||
|
||
**Fix:** Добавить CSRF token для всех POST endpoints.
|
||
|
||
---
|
||
|
||
### M3. No security headers
|
||
**Severity:** MEDIUM
|
||
**File:** `main.py`
|
||
|
||
**Problem:** Отсутствие CSP, X-Content-Type-Options, X-Frame-Options, X-XSS-Protection.
|
||
|
||
**Fix:** Middleware для security headers.
|
||
|
||
---
|
||
|
||
### M4. SQLite without WAL mode
|
||
**Severity:** MEDIUM
|
||
**File:** `db/engine.py:12`
|
||
|
||
**Problem:** Concurrent readers block writers → poor performance under load.
|
||
|
||
**Fix:** `PRAGMA journal_mode=WAL` in connection setup.
|
||
|
||
---
|
||
|
||
### M5. Scoped npm packages not supported
|
||
**Severity:** MEDIUM
|
||
**File:** `core/nexus.py:54-70`
|
||
|
||
**Problem:** `extract_npm_info` returns `None` для `@scope/package` → пропускаются сканирования.
|
||
|
||
**Fix:** Обновить extractor для scoped packages.
|
||
|
||
---
|
||
|
||
### M6. Dashboard stats — potential IndexError
|
||
**Severity:** MEDIUM
|
||
**File:** `routes/api_scans.py:145-147`
|
||
|
||
**Problem:** `dashboard["latest_flagged"][0]` — IndexError если `latest_flagged` пустой.
|
||
|
||
```python
|
||
"latest_scan_at": dashboard["latest_flagged"][0].started_at.isoformat()
|
||
```
|
||
|
||
**Fix:** Guard с `if dashboard.get("latest_flagged")`.
|
||
|
||
---
|
||
|
||
### M7. Error message HTML escaping
|
||
**Severity:** MEDIUM
|
||
**File:** `web/templates/scan_detail.html:30`
|
||
|
||
**Problem:** `scan.error_message` rendered в template — если содержит HTML/JS, может сломать UI.
|
||
|
||
**Fix:** Jinja2 autoescape handles this, но стоит добавить explicit escaping для `code` fields.
|
||
|
||
---
|
||
|
||
### M8. Unknown ecosystem defaults to pypi ✅ FIXED
|
||
**Severity:** MEDIUM
|
||
**Fix:** `_detect_ecosystem()` возвращает `None` → webhook reject с `"unknown_ecosystem"`.
|
||
**Duplicate:** L6.
|
||
|
||
---
|
||
|
||
## LOW (6)
|
||
|
||
### L1. Dockerfile grep hack ✅ FIXED (`uv pip install . --system`)
|
||
### L2. Health check without DB ✅ FIXED (`/health/dependencies`)
|
||
|
||
---
|
||
|
||
### L3. No backup strategy for SQLite
|
||
**Severity:** LOW
|
||
**Risk:** Crash → corrupted database → data loss.
|
||
|
||
**Fix:** Регулярные backups через cron или switch to PostgreSQL for production.
|
||
|
||
---
|
||
|
||
### L4. Dead code — `parse_package_path` unused in harvester
|
||
**Severity:** LOW
|
||
**File:** `core/nexus.py:93-99`
|
||
|
||
**Problem:** Функция определена но не используется в harvester pipeline.
|
||
|
||
**Fix:** Убрать или интегрировать.
|
||
|
||
---
|
||
|
||
### L5. Hardcoded LLM API base URL
|
||
**Severity:** LOW
|
||
**File:** `constants.py:139`
|
||
|
||
**Problem:** Default `https://api.openai.com/v1` — unexpected API calls для пользователей локальных моделей.
|
||
|
||
**Fix:** Better default или warning at startup.
|
||
|
||
---
|
||
|
||
### L6. Unknown ecosystem defaults to pypi (webhook)
|
||
**Severity:** LOW
|
||
**File:** `routes/webhooks.py:62`
|
||
|
||
**Problem:** Неизвестный format → fallback к pypi. Maven/NuGet webhooks будут сканироваться как PyPI пакеты.
|
||
|
||
**Fix:** Явно reject неизвестные ecosystems.
|
||
|
||
---
|
||
|
||
## Implementation Plan
|
||
|
||
### Phase 1 — P0 (Critical)
|
||
|
||
| # | Task | Status |
|
||
|---|------|--------|
|
||
| 1 | SSRF protection | ✅ FIXED |
|
||
| 2 | Mandatory WEBHOOK_SECRET | ❌ ACCEPTED |
|
||
| 3 | Remove default Nexus credentials | ✅ FIXED |
|
||
| 4 | LLM verdict whitelist + prompt injection | ⚠️ PARTIAL |
|
||
| 5 | Path traversal fix | ⚠️ LOW RISK |
|
||
|
||
### Phase 2 — P1 (High)
|
||
|
||
| # | Task | Status |
|
||
|---|------|--------|
|
||
| 6 | Rate limiting | ❌ REJECTED |
|
||
| 7 | API authentication | ❌ REJECTED |
|
||
| 8 | Memory leak fix for locks | ✅ FIXED |
|
||
| 9 | Race condition fix | ✅ FIXED |
|
||
| 10 | Remove source_ip from public API | ❌ REJECTED |
|
||
| 11 | CSV export auth + limit | ❌ REJECTED |
|
||
|
||
### Phase 3 — P2 (Medium)
|
||
|
||
| # | Task | Status |
|
||
|---|------|--------|
|
||
| 12 | LLM response validation (Pydantic) | ⬜ |
|
||
| 13 | CSRF protection | ⬜ |
|
||
| 14 | Security headers middleware | ⬜ |
|
||
| 15 | SQLite WAL mode | ⬜ |
|
||
| 16 | Scoped npm support | ⬜ |
|
||
| 17 | Dashboard None guard | ⬜ |
|
||
| 18 | `serialize_finding` вместо `**f.data` | ✅ FIXED |
|
||
| 19 | `_scan_component` try/except | ✅ FIXED |
|
||
| 20 | Reject unknown ecosystem | ✅ FIXED |
|
||
|
||
### Phase 4 — P3 (Low)
|
||
|
||
| # | Task | Status |
|
||
|---|------|--------|
|
||
| 21 | Dockerfile deps | ✅ FIXED |
|
||
| 22 | Health check DB ping | ✅ FIXED |
|
||
| 23 | Backup strategy docs | ⬜ |
|
||
| 24 | Reject unknown ecosystems | ✅ FIXED (duplicate) | |
|
||
|
||
---
|
||
|
||
## Test Coverage Gaps
|
||
|
||
The existing 85 tests do NOT cover:
|
||
|
||
- [ ] SSRF prevention (malicious downloadUrl)
|
||
- [ ] Webhook signature validation with empty secret
|
||
- [ ] Path traversal in download URLs
|
||
- [ ] Rate limiting on webhook endpoint
|
||
- [ ] Authentication on API endpoints
|
||
- [ ] LLM prompt injection
|
||
- [ ] LLM response schema validation
|
||
- [ ] CSRF protection
|
||
- [ ] Security headers presence
|
||
- [ ] Memory leak in lock dictionaries
|
||
- [ ] Race condition in URL locking
|
||
- [ ] Scoped npm package extraction
|
||
- [ ] Dashboard IndexError on empty data
|
||
|
||
---
|
||
|
||
## Recommendations
|
||
|
||
1. **Immediate:** Implement C1-C5 before any production deployment
|
||
2. **Short-term:** Implement H1-H7 within first sprint
|
||
3. **Medium-term:** Implement M1-M8 within first month
|
||
4. **Long-term:** Implement L1-L6 during routine maintenance
|
||
5. **Ongoing:** Add security-focused tests for all findings above
|