agnes-the-ai-analyst/docs/archive/superpowers/plans/2026-04-08-security-hardening.md
ZdenekSrotyr a48524509a
docs: consolidate and de-clutter the documentation tree (#306)
CLAUDE.md rewritten (708 -> ~320 lines): four overlapping release
sections collapsed to one, stale v1->v35 schema history dropped (it
lives in CHANGELOG), marketplace endpoint internals and verbose
process sections moved out or tightened.

New focused docs:
- docs/RELEASING.md - release process, deploy workflows, CI quirks
  (RELEASE_TEMPLATE.md folded in as an appendix)
- docs/marketplace.md - marketplace ingestion + re-serving internals
- docs/README.md - documentation index by audience, linked from
  README.md and CLAUDE.md

Archived under docs/archive/: docs/superpowers/ (52 historical
planning artifacts), HACKATHON.md, pd-ps-comments.md,
security-audit-2026-04.md, future/NOTIFICATIONS.md.

Removed the docs/auto-install.md stub. Fixed dangling links in
connectors/jira/README.md and dev_docs/README.md, repointed
code/doc references to archived paths.
2026-05-14 18:54:22 +00:00

8.9 KiB

Security Hardening — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Fix all critical/high security vulnerabilities found by audit before merging to main. Also update Python version, sync deps, fix Docker prod config.

Architecture: 7 independent fix tasks targeting specific vulnerabilities. Each is self-contained.

Tech Stack: Python 3.13, FastAPI, DuckDB, Docker


Task 1: SQL Query Hardening

Files:

  • Modify: app/api/query.py

  • Modify: src/db.py (add read-only analytics getter)

  • Test: existing tests/test_security.py should still pass

  • Step 1: Add read-only analytics DB connection

In src/db.py, add after get_analytics_db():

def get_analytics_db_readonly() -> duckdb.DuckDBPyConnection:
    """Read-only connection to analytics DB. Blocks writes and external access."""
    db_path = _get_data_dir() / "analytics" / "server.duckdb"
    if not db_path.exists():
        db_path.parent.mkdir(parents=True, exist_ok=True)
    conn = duckdb.connect(str(db_path), read_only=True)
    conn.execute("SET enable_external_access = false")
    return conn
  • Step 2: Harden query endpoint

In app/api/query.py, replace get_analytics_db() with get_analytics_db_readonly(). Add to blocklist:

blocked = [
    "drop ", "delete ", "insert ", "update ", "alter ", "create ",
    "copy ", "attach ", "detach ", "load ", "install ",
    "export ", "import ", "pragma ",
    "read_csv", "read_json", "read_parquet(", "read_text",
    "write_csv", "write_parquet",
    "read_blob", "glob(", "read_ndjson",
    ";",
    "'/", '"/",  # Block absolute file paths in FROM clause
]
  • Step 3: Run tests
pytest tests/test_security.py tests/test_e2e_api.py -v --tb=short
  • Step 4: Commit
git commit -m "security: query endpoint — read-only DB + disable external access + block file paths"

Task 2: Upload Path Traversal Fix

Files:

  • Modify: app/api/upload.py

  • Step 1: Sanitize filenames

Replace lines 30-31 and 47-48 with:

# Sanitize filename — strip directory components to prevent traversal
raw_name = file.filename or f"session_{uuid.uuid4().hex[:8]}.jsonl"
filename = Path(raw_name).name  # strips ../../../ etc
if not filename or filename.startswith("."):
    filename = f"upload_{uuid.uuid4().hex[:8]}"
target = sessions_dir / filename

Same pattern for artifact upload.

  • Step 2: Commit
git commit -m "security: sanitize upload filenames — prevent path traversal"

Task 3: Script Sandbox Hardening

Files:

  • Modify: app/api/scripts.py

  • Step 1: Add AST-based validation

Add before the blocklist check:

import ast

# AST-based validation — catches obfuscated imports
try:
    tree = ast.parse(source)
except SyntaxError as e:
    raise HTTPException(status_code=400, detail=f"Script syntax error: {e}")

for node in ast.walk(tree):
    if isinstance(node, ast.Import):
        for alias in node.names:
            if alias.name.split(".")[0] in BLOCKED_MODULES:
                raise HTTPException(status_code=400, detail=f"Blocked import: {alias.name}")
    elif isinstance(node, ast.ImportFrom):
        if node.module and node.module.split(".")[0] in BLOCKED_MODULES:
            raise HTTPException(status_code=400, detail=f"Blocked import: {node.module}")
    elif isinstance(node, ast.Call):
        if isinstance(node.func, ast.Name) and node.func.id in BLOCKED_FUNCTIONS:
            raise HTTPException(status_code=400, detail=f"Blocked function: {node.func.id}")

BLOCKED_MODULES = {"os", "sys", "subprocess", "shutil", "ctypes", "importlib", "socket",
                   "requests", "urllib", "http", "signal", "pathlib", "builtins"}
BLOCKED_FUNCTIONS = {"exec", "eval", "compile", "open", "globals", "locals",
                     "getattr", "setattr", "delattr", "breakpoint", "__import__"}

Keep the string blocklist as a secondary check.

  • Step 2: Commit
git commit -m "security: AST-based script validation — catches obfuscated imports"

Task 4: Password Hashing + Auth Fixes

Files:

  • Modify: app/auth/providers/password.py (remove SHA256 fallback)

  • Modify: app/auth/providers/google.py (add secure cookie flag)

  • Modify: app/auth/dependencies.py (fix get_optional_user bug)

  • Modify: app/auth/jwt.py (add secret length validation)

  • Modify: pyproject.toml (add missing deps)

  • Step 1: Remove SHA256 fallback in password.py

Replace the try/except ImportError block with:

from argon2 import PasswordHasher
ph = PasswordHasher()
hashed = ph.hash(request.password)

Remove all except ImportError: import hashlib blocks. Same in app/auth/router.py if present.

  • Step 2: Add secure flag to Google OAuth cookie

In app/auth/providers/google.py, change set_cookie to:

is_https = os.environ.get("HTTPS", "").lower() in ("1", "true") or request.url.scheme == "https"
response.set_cookie(
    key="access_token", value=jwt_token,
    httponly=True, max_age=86400 * 30, samesite="lax",
    secure=is_https,
)
  • Step 3: Fix get_optional_user argument bug

In app/auth/dependencies.py, change line 68:

# Before (wrong):
return await get_current_user(authorization, conn)
# After (correct):
return await get_current_user(request=None, authorization=authorization, conn=conn)
  • Step 4: Add JWT secret validation

In app/auth/jwt.py, add after SECRET_KEY:

if len(SECRET_KEY) < 32 and not os.environ.get("TESTING"):
    import warnings
    warnings.warn("JWT_SECRET_KEY is less than 32 characters — insecure for production", stacklevel=2)
  • Step 5: Sync pyproject.toml deps

Add to pyproject.toml dependencies:

"authlib>=1.3.0",
"argon2-cffi>=23.1.0",
  • Step 6: Commit
git commit -m "security: fix password hashing, OAuth cookie, JWT validation, optional_user bug"

Task 5: CORS + Session Middleware

Files:

  • Modify: app/main.py

  • Step 1: Fix CORS

Replace wildcard CORS with environment-configured origins:

cors_origins = os.environ.get("CORS_ORIGINS", "http://localhost:3000,http://localhost:8000").split(",")
app.add_middleware(
    CORSMiddleware,
    allow_origins=[o.strip() for o in cors_origins],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
  • Step 2: Fix SessionMiddleware secret
session_secret = os.environ.get("SESSION_SECRET", os.environ.get("JWT_SECRET_KEY", ""))
if not session_secret:
    import secrets
    session_secret = secrets.token_hex(32)
app.add_middleware(SessionMiddleware, secret_key=session_secret)
  • Step 3: Commit
git commit -m "security: CORS from env config + session secret validation"

Task 6: Docker Production Config

Files:

  • Modify: docker-compose.yml (remove --reload)

  • Modify: Dockerfile (upgrade Python)

  • Step 1: Remove --reload from docker-compose.yml

# Before:
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
# After:
command: uvicorn app.main:app --host 0.0.0.0 --port 8000

Create docker-compose.override.yml for dev:

services:
  app:
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
    volumes:
      - .:/app
  • Step 2: Upgrade Dockerfile to Python 3.13
FROM python:3.13-slim
  • Step 3: Commit
git commit -m "chore: Docker production config — no reload, Python 3.13"

Task 7: Stale Docs Cleanup + datetime.utcnow Fix

Files:

  • Modify: CLAUDE.md (fix test count)

  • Delete or update: stale docs referencing Flask

  • Modify: app/api/jira_webhooks.py, src/profiler.py (utcnow → now(UTC))

  • Step 1: Fix CLAUDE.md test count

Update test count to current number.

  • Step 2: Delete stale docs
rm docs/superpowers/plans/*.md  # agent plans, not needed in repo
  • Step 3: Fix datetime.utcnow deprecation

Replace datetime.utcnow() with datetime.now(timezone.utc) in:

  • app/api/jira_webhooks.py

  • src/profiler.py (lines 1213, 1379)

  • Step 4: Commit

git commit -m "chore: fix stale docs, test count, datetime deprecation"

Execution order

Task 1 (SQL)        ─┐
Task 2 (Upload)      ├─ 3 parallel agents
Task 3 (Scripts)    ─┘
Task 4 (Auth)       ─┐
Task 5 (CORS)        ├─ 3 parallel agents
Task 6 (Docker)     ─┘
Task 7 (Docs)       ── sequential after above

Verification

# All tests pass
pytest tests/ --ignore=tests/test_cli.py -v

# Security spot checks
python3 -c "from app.api.query import *"  # no errors
curl -X POST http://localhost:8000/api/query -d '{"sql":"SELECT * FROM \"/etc/passwd\""}' # should fail

# Docker build
docker build -t test .