Skip to content

Unauthenticated POST /api/query invokes paid Gemini 2.5 Pro on live deployment; wildcard CORS on all routes #1

@tg12

Description

@tg12

Summary

The FastAPI endpoint POST /api/query invokes Gemini 2.5 Pro with zero authentication. The application also uses wildcard CORS (allow_origins=["*"]) on all routes. The service is deployed live at sentinel.osintnet.uk. Any unauthenticated request from any origin can trigger an AI inference call at the repository owner's Gemini billing account.

Evidence

main.py line 82 — wildcard CORS on all routes:

app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

main.py lines 387-395 — unauthenticated Gemini endpoint:

@app.post("/api/query")
async def query_agent(req: QueryRequest):
    if not GEMINI_API_KEY:
        return {"answer": "GEMINI_API_KEY not configured.", "engine": "none"}
    # No auth dependency, no rate limiting on this endpoint
    # ... proceeds to call Gemini 2.5 Pro

.env.example confirms GEMINI_API_KEY is a real credential, not a placeholder. The README references the live deployment at sentinel.osintnet.uk.

No authentication dependency (Depends(...)) is applied to /api/query, /api/contracts, /api/stats, or any other route. There is no API key header check, no session validation, and no per-IP rate limit on the /api/query endpoint specifically.

Why this matters

Any anonymous user can POST arbitrary questions to /api/query and consume Gemini API tokens at the owner's expense. Depending on the Gemini tier and query complexity, this can generate significant billing charges within minutes of discovery. Wildcard CORS additionally allows any malicious website to trigger cross-origin requests from a visitor's browser.

Attack or failure scenario

  1. Attacker finds the live deployment at sentinel.osintnet.uk
  2. Scripts a loop: while true; do curl -X POST https://sentinel.osintnet.uk/api/query -d '{"question": "<long prompt>"}'; done
  3. Each request invokes Gemini 2.5 Pro with the attacker's prompt — billing accumulates per token
  4. No rate limiting stops the loop; the endpoint continues serving until the API quota is exhausted or the billing limit is hit
  5. Alternatively, any webpage can iframe or fetch the endpoint cross-origin (wildcard CORS allows it)

Root cause

The endpoint was built for hackathon demonstration without adding authentication. Rate limiting exists on other routes but was not applied to /api/query. The wildcard CORS is a common copy-paste default that was never restricted to the expected frontend origin.

Recommended fix

  1. Add a Depends check on /api/query — at minimum verify an X-API-Key header against a secret stored in the environment:
    async def require_key(x_api_key: str = Header(...)):
        if x_api_key != os.environ["API_SECRET"]:
            raise HTTPException(status_code=401)
    
    @app.post("/api/query", dependencies=[Depends(require_key)])
  2. Add per-IP rate limiting on /api/query (e.g., slowapi or a Redis-backed counter)
  3. Replace allow_origins=["*"] with an explicit allow-list of production frontend origins
  4. Set a Gemini API quota limit in the Google Cloud console to cap maximum spend

Acceptance criteria

  • POST /api/query returns 401 without a valid credential
  • Per-IP rate limit applied to /api/query (e.g., max 10 req/min)
  • CORS allow_origins restricted to the production frontend domain
  • Gemini API quota configured in Google Cloud to cap spend

Suggested labels

security, bug

Priority

P0

Severity

Critical — live unauthenticated endpoint invoking a paid AI API with no rate limiting or origin restriction; immediately exploitable for billing abuse.

Confidence

Confirmed — main.py read directly from committed source; live deployment documented in README.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions