Skip to content

Latest commit

 

History

History
61 lines (51 loc) · 4.21 KB

File metadata and controls

61 lines (51 loc) · 4.21 KB

Security Policy

Scope: this repo's REST API + MCP server (src/idc_api/).

Threat model, in one paragraph

This service serves the public NCI Imaging Data Commons index — an open, de-identified dataset, not secret data. The DuckDB backend is opened read-only, so no request can modify or delete it. The realistic risk is therefore abuse of the server (cost, availability, resource exhaustion) rather than data disclosure. Full rationale and the guarded-SQL threat model live in dev/api_v3_plan.md → "Safety for the guarded SQL tool".

What's already in place

  • Read-only, hardened DuckDB connectionenable_external_access=false, autoload/autoinstall_known_extensions=false, lock_configuration=true (frozen at connect time). See DuckDBBackend._hardening_config in duckdb_backend.py. Regression-tested in tests/test_backend_guards.py: non-SELECT statements, multi-statement SQL, local/remote file access, and extension-loading/export statements (INSTALL, LOAD, COPY ... TO, SET) are all rejected.
  • Row/response capsrun_sql and manifest endpoints clamp max_rows to a hard server ceiling (IDC_API_SQL_MAX_ROWS_CAP, IDC_API_MANIFEST_HARD_CAP); a caller cannot request an unbounded dump.
  • Parameterized SQL everywhere we author it — values are always bound (? placeholders); identifiers that can't be bound (table/column names) are validated against allow-lists before being interpolated. This is an architecture invariant — see CLAUDE.md — enforced in code review and spot-checked by bandit in CI (SQL-construction findings are individually annotated with why the identifier is trusted, not blanket-suppressed).
  • Structured audit logging — every REST request and MCP tool call emits one JSON log line (path/tool, status/outcome, duration, row count where applicable) to stdout, which Cloud Run ships to Cloud Logging automatically. For the guarded SQL endpoint/tool, a rendering of the query is included — by default the first 200 chars (IDC_API_SQL_LOG_MODE=snippet), or a short digest instead (IDC_API_SQL_LOG_MODE=hash) if you'd rather correlate repeated queries without putting query text in logs. Either way this is public-schema SQL the caller wrote themselves, not sensitive data — the cap is about log-line hygiene (one pathological query can't inflate a line), not confidentiality. Client IPs are not logged at the application level (Cloud Run's own request log already has caller IP, correlatable by timestamp).
  • CI checks on every PR: ruff (lint), bandit (static security lint), pip-audit (dependency CVEs), and the tests suite.
  • Non-root containerDockerfile drops to an unprivileged user before serving.

Known residual risks (public deployment)

Risk Status
No per-IP rate limiting in front of the service Mitigated by Cloud Run --max-instances/--concurrency caps (see dev/deployment.md); a dedicated edge rate limit (Cloud Armor / API Gateway) is an infra decision outside this repo.
CORS allows all origins (*) Intentional — the API serves only public, read-only data. Revisit if private data or auth is ever added.
MCP DNS-rebinding protection defaults off Documented trade-off for the unauthenticated hosted transport; operators who want it set IDC_API_MCP_DNS_REBINDING_PROTECTION=true plus an allowed-hosts list.
DuckDB sandbox escape (0-day in DuckDB itself) No code-level mitigation beyond the hardening config above and re-running the guard tests on every DuckDB upgrade; the container itself holds no secrets and reaches no internal network.

If you run your own fork or self-hosted deployment, you own its network exposure, auth (if you add non-public data), and update cadence — this document describes the project's own hosted service.

Reporting a vulnerability

Please use GitHub's private "Report a vulnerability" flow so it isn't publicly disclosed before a fix ships. For non-sensitive hardening suggestions (e.g. a missing test case), a regular GitHub issue is fine.