Skip to content

fix(auth): add startup-generated API key authentication to all routes#278

Open
advikdivekar wants to merge 2 commits into
utksh1:mainfrom
advikdivekar:security/zero-api-authentication
Open

fix(auth): add startup-generated API key authentication to all routes#278
advikdivekar wants to merge 2 commits into
utksh1:mainfrom
advikdivekar:security/zero-api-authentication

Conversation

@advikdivekar
Copy link
Copy Markdown
Contributor

What is the problem

Closes #199

All /api/v1/* endpoints were publicly accessible with no authentication. Any process on localhost — or any attacker who can reach the port — could trigger scans, read results, and interact with the credential vault without supplying any credentials.

What was changed

File Change
backend/secuscan/auth.py New module: generates a random 64-char hex key at startup, persists it to <data_dir>/.api_key (mode 0600), exposes require_api_key FastAPI dependency
backend/secuscan/main.py Calls init_api_key(settings.data_dir) in the lifespan startup block
backend/secuscan/routes.py Adds dependencies=[Depends(require_api_key)] to the APIRouter; fixes is_filesystem_target() CIDR false-positive
testing/backend/conftest.py test_client fixture now calls init_api_key and passes the key via X-Api-Key header
testing/backend/integration/test_task_cleanup.py app_client fixture initialises API key and passes it in AsyncClient headers
testing/backend/test_task_pagination.py Converted module-level client to test_client fixture to avoid key state bleed
testing/backend/unit/test_api_auth.py 22 new unit tests covering key init, both header formats, wrong-key rejection, unprotected endpoints

Why this approach

  • Startup-generated, file-persisted key — no hardcoded secret, survives restarts, rotatable by deleting .api_key
  • secrets.compare_digest — constant-time comparison prevents timing oracle attacks
  • Router-level dependency — single declaration protects every existing and future route with no per-route boilerplate
  • /api/v1/health and / are defined directly on app, not on the router, so they remain unauthenticated for health checks

How to test

# Start backend
cd backend && python -m secuscan

# Unauthenticated — should return 401
curl http://localhost:8000/api/v1/plugins

# Read generated key
API_KEY=$(cat backend/data/.api_key)

# Authenticated via X-Api-Key — should return 200
curl -H "X-Api-Key: $API_KEY" http://localhost:8000/api/v1/plugins

# Authenticated via Bearer — should return 200
curl -H "Authorization: Bearer $API_KEY" http://localhost:8000/api/v1/plugins

Edge cases covered

  • Key file created with 0o600 permissions on first run; reloaded on subsequent starts
  • _api_key is None (service not yet initialised) returns 503, not 401
  • Both Authorization: Bearer and X-Api-Key headers accepted
  • CIDR notation (8.8.8.8/32) no longer incorrectly flagged as a filesystem target
  • All pre-existing tests updated to pass the API key; test isolation maintained via fixtures

Verification checklist

  • pytest testing/backend/ — 261 passed (1 pre-existing failure in test_route_rejects_task_when_limiter_full due to rate-limiter state bleed across the full suite, also failing on main)
  • 22 new auth unit tests — all pass
  • Unauthenticated requests return 401
  • Correct key via X-Api-Key returns 200
  • Correct key via Authorization: Bearer returns 200
  • is_filesystem_target("8.8.8.8/32") returns False

All API routes now require a valid key via Authorization: Bearer or
X-Api-Key header. The key is generated at startup using secrets.token_hex
and persisted to <data_dir>/.api_key (mode 0o600) so it survives restarts.

Fixes utksh1#199 (zero API authentication — all endpoints publicly accessible).

Also fixes is_filesystem_target() to match only real filesystem roots,
preventing CIDR notation like 8.8.8.8/32 from bypassing safe-mode target
validation.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1456d51047

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

from sse_starlette.sse import EventSourceResponse

router = APIRouter(prefix="/api/v1")
router = APIRouter(prefix="/api/v1", dependencies=[Depends(require_api_key)])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude SSE stream route from global API-key dependency

Applying Depends(require_api_key) at router scope also protects /api/v1/task/{task_id}/stream, but the browser clients open that endpoint with native EventSource (frontend/src/api.ts and frontend/src/pages/TaskDetails.tsx), which cannot attach the required auth headers. As a result, scan progress streams will consistently fail with 401 in the UI after this change, even if non-stream API calls are later updated to send credentials.

Useful? React with 👍 / 👎.

"""Best-effort detection for path-based targets that should bypass host validation."""
if target.startswith(("/", "./", "../", "~")):
# Absolute or relative filesystem roots only — not CIDR notation (e.g. 8.8.8.8/32)
if target.startswith(("/", "./", "../", "~/")):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat slash-containing relative paths as filesystem targets

The new is_filesystem_target() logic no longer recognizes relative paths like artifacts/memdump.raw (it only accepts /, ./, ../, ~/, or Windows-drive prefixes), so these values now fall into validate_target() and are rejected as invalid hostnames. This regresses non-code plugins that accept file/directory targets because previously slash-containing paths bypassed host validation; now users must rewrite existing inputs to add ./.

Useful? React with 👍 / 👎.

@advikdivekar
Copy link
Copy Markdown
Contributor Author

@utksh1 please review it and I have asked to claim few issues, please assign those issues to me under GSSoC 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Zero API Authentication — All Endpoints Publicly Accessible Without Credentials

1 participant