Skip to content

fix: pentest batch 2 — #13 #17 #18 #6#43

Merged
acartag7 merged 3 commits intomainfrom
worktree-ticklish-launching-dawn
Mar 11, 2026
Merged

fix: pentest batch 2 — #13 #17 #18 #6#43
acartag7 merged 3 commits intomainfrom
worktree-ticklish-launching-dawn

Conversation

@acartag7
Copy link
Copy Markdown
Collaborator

Summary

Fixes four pentest findings from the 2026-03-10 penetration test:

Review-caught regression fixed

Adversarial code review caught X-Edictum-Agent-Id missing from the new CORS allow_headers list — added before merge.

Test plan

  • 507 existing tests pass (0 failures)
  • TypeScript compiles clean (pnpm tsc --noEmit)
  • New tests: public health hides sensitive fields, /health/details requires auth, returns full info
  • Adversarial review completed — CORS header regression fixed
  • Verify on deployed instance: GET /health returns minimal JSON
  • Verify SSE without auth header returns 401, not 422
  • Verify CORS preflight includes X-Edictum-Agent-Id

Closes #13, closes #17, closes #18, closes #6

…e DoS (#13 #17 #18 #6)

- #17: Split /health into public (status + bootstrap only) and
  authenticated /health/details (version, db, redis, workers, agents).
  Dashboard settings page uses the authenticated endpoint.

- #18 L2: SSE stream returns 401 (not 422) when Authorization header
  is missing — require_api_key param changed to optional.
- #18 L3: CORS allow_methods/allow_headers use explicit lists instead
  of wildcards (includes X-Edictum-Agent-Id for agents).
- #18 L4: RequestValidationError handler strips ctx/type fields from
  Pydantic errors to avoid leaking framework internals.

- #13: Startup warning when base_url is HTTPS but trusted_proxies is
  empty — surfaces the redirect downgrade and rate-limit keying issue.

- #6: Evaluate endpoint wrapped in 5s timeout + semaphore (max 4
  concurrent). Pre-parse YAML document limit (10) and post-parse
  contract count limit (100).

All 507 existing tests pass. TypeScript compiles clean.
- Ruff: fix import sorting for RequestValidationError in main.py
- Ruff B904: add `from exc` to TimeoutError raises in evaluate.py
- login.tsx: remove health.version (no longer in public HealthResponse)
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: 2868fb4beb

ℹ️ 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".

)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
finally:
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 Keep semaphore slot until timed-out worker thread exits

The finally block always releases _EVALUATE_SEMAPHORE immediately, but asyncio.wait_for(asyncio.to_thread(...), timeout=5) only times out the awaiter and does not stop the underlying worker thread; that means expensive evaluations can keep running after a timeout while new requests are still admitted, so repeated timeout payloads can still exhaust the thread pool despite this guard.

Useful? React with 👍 / 👎.

allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
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 Add PATCH to CORS allow_methods

Restricting CORS methods to GET/POST/PUT/DELETE/OPTIONS drops PATCH, but the app exposes PATCH routes (for example assignment rules and agent registrations) and the dashboard client calls them (dashboard/src/lib/api/agents.ts), so those browser requests will fail preflight in any cross-origin deployment even though they previously worked with wildcard methods.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@edictum-reviewer edictum-reviewer bot left a comment

Choose a reason for hiding this comment

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

Inline comments posted. See the summary comment for the full review.

allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Warning: allow_methods omits PATCH.

Two existing routes use PATCH:

  • routes/assignment_rules.py:101PATCH /{rule_id}
  • routes/agent_registrations.py:60PATCH /{agent_id}

Cross-origin callers hitting these routes will fail CORS preflight — OPTIONS won't include PATCH in Access-Control-Allow-Methods and the browser will block the request before it reaches FastAPI.

Fix:

allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],

@edictum-reviewer
Copy link
Copy Markdown
Contributor

⚠️ Code Review

1 warning found. No critical issues, but one item needs attention.

🟡 Warnings

# File Issue Violates
1 src/edictum_server/main.py:408 allow_methods omits PATCH — two existing routes use it CLAUDE.md — Boring technology / no regressions
Details

1. src/edictum_server/main.py:408 — CORS allow_methods excludes PATCH

The PR tightens CORS from ["*"] to an explicit list, which is the right move. But the new list omits PATCH:

allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],

Two routes in the codebase respond to PATCH and were not touched by this PR:

  • src/edictum_server/routes/assignment_rules.py:101PATCH /{rule_id}
  • src/edictum_server/routes/agent_registrations.py:60PATCH /{agent_id}

Browser-based cross-origin callers (edictum-hub, or any third-party integration) sending a PATCH request will fail the CORS preflight (OPTIONS will not include PATCH in Access-Control-Allow-Methods). The actual request will be blocked before it reaches FastAPI.

Suggested fix:

allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],

Review details
Files reviewed 13
Checks applied Security boundaries (S1–S8) · Tenant isolation · Input bounds · Timing-safe comparisons · CORS/headers · Resource lifecycle · DDD layer rules · shadcn compliance · Code quality
Issues found 1
Criteria sources CLAUDE.md · code-reviewer.md

Files

  • ✏️ dashboard/src/lib/api/auth.ts (modified)
  • ✏️ dashboard/src/lib/api/index.ts (modified)
  • ✏️ dashboard/src/pages/login.tsx (modified)
  • ✏️ dashboard/src/pages/settings.tsx (modified)
  • ✏️ dashboard/src/pages/settings/notifications-section.tsx (modified)
  • ✏️ dashboard/src/pages/settings/system-section.tsx (modified)
  • ✏️ src/edictum_server/auth/dependencies.py (modified)
  • ✏️ src/edictum_server/main.py (modified)
  • ✏️ src/edictum_server/routes/evaluate.py (modified)
  • ✏️ src/edictum_server/routes/health.py (modified)
  • ✏️ src/edictum_server/services/evaluate_service.py (modified)
  • ✏️ tests/test_health.py (modified)
  • ✏️ tests/test_settings/test_health_enriched.py (modified)

- CORS: add PATCH to allow_methods (assignment rules + agent
  registrations use PATCH routes)
- Evaluate semaphore: use asyncio.shield + done callback so the
  semaphore slot stays acquired until the worker thread actually
  exits, not when the HTTP timeout fires
@edictum-reviewer
Copy link
Copy Markdown
Contributor

✅ Code Review

All checks passed. No issues found in this PR.

✅ Checks passed

Check Status
Auth bypass (S1, S2) ✅ Clean
Tenant isolation (S3) ✅ Clean
Input weaponization ✅ Clean
State manipulation / semaphore lifecycle ✅ Clean
Information leakage ✅ Clean
Privilege escalation ✅ Clean
Timing-safe comparisons ✅ Clean
Pydantic max_length bounds ✅ Clean
Identity from auth context ✅ Clean
Redis TTL ✅ Not applicable
DDD layer rules ✅ Clean
Code quality / file sizes ✅ Clean

Review details
Files reviewed 13
Checks applied Tenant isolation · Security boundaries · Auth bypass · Input weaponization · Information leakage · Pydantic bounds · DDD layers · Semaphore lifecycle · CORS · Code quality
Issues found 0
Criteria sources CLAUDE.md · code-reviewer.md

Files

  • ✏️ src/edictum_server/routes/health.py (modified)
  • ✏️ src/edictum_server/routes/evaluate.py (modified)
  • ✏️ src/edictum_server/services/evaluate_service.py (modified)
  • ✏️ src/edictum_server/auth/dependencies.py (modified)
  • ✏️ src/edictum_server/main.py (modified)
  • ✏️ dashboard/src/lib/api/auth.ts (modified)
  • ✏️ dashboard/src/lib/api/index.ts (modified)
  • ✏️ dashboard/src/pages/login.tsx (modified)
  • ✏️ dashboard/src/pages/settings.tsx (modified)
  • ✏️ dashboard/src/pages/settings/notifications-section.tsx (modified)
  • ✏️ dashboard/src/pages/settings/system-section.tsx (modified)
  • ✏️ tests/test_health.py (modified)
  • ✏️ tests/test_settings/test_health_enriched.py (modified)

Review notes (for context, not findings)

/health split is a net security improvement. The old public /health returned version, auth_provider, DB/Redis latencies, worker statuses, and agent counts with no auth. The new /health returns only status + bootstrap_complete. Everything else moves behind require_dashboard_auth on /health/details. Attack surface on the public endpoint is materially reduced.

require_api_key 401 fix is correct. Making authorization: str | None = Header(default=None) and raising 401 explicitly prevents FastAPI from returning a 422 validation error that leaks the header schema. The explicit early check is the right pattern.

Semaphore lifecycle in evaluate.py is airtight. All branches release the semaphore: success (else), ValueError, BaseException (including CancelledError from client disconnect), and timeout (via add_done_callback on the shielded thread task). asyncio.shield correctly prevents wait_for from cancelling the background thread, so the callback fires when the thread actually terminates rather than when the HTTP response is sent. The capacity-4 cap matches the comment's intent.

_check_yaml_complexity heuristic is acceptable. The \n--- string count is a fast pre-check that can false-positive on embedded --- in YAML string values, but false negatives don't matter — the real DoS protection is the 5-second timeout and semaphore cap. Pre-check just avoids spinning up a thread for obviously malformed input.

CORS tightening is correct. PATCH is now explicit (needed after recent route additions). Header allowlist covers all headers the frontend sends (Content-Type, X-Requested-With for CSRF, Authorization for API keys, X-Edictum-Agent-Id). Cookies with credentials: include are not in allow_headers — that's correct, credentialed cookies are handled by allow_credentials=True separately.

Validation error handler position. Registered after include_router calls — FastAPI exception handlers are stored globally in a dict; registration order relative to router inclusion doesn't affect which routes they apply to. All routes get the sanitized 422 response.

EvaluateRequest schema bounds verified. yaml_content has max_length=1_048_576 (1 MB), tool_name has 255, environment has 64, agent_id has 255. All string fields bounded. Pre-existing schema, not changed in this PR, but confirmed clean.

@acartag7 acartag7 merged commit 49c97e0 into main Mar 11, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant