Skip to content

fix(security): honor api_keys auth in monolithic gemini_web2api.py#23

Merged
Sophomoresty merged 1 commit into
Sophomoresty:mainfrom
aaronjmars:security/monolithic-api-keys-auth-gate
Jun 2, 2026
Merged

fix(security): honor api_keys auth in monolithic gemini_web2api.py#23
Sophomoresty merged 1 commit into
Sophomoresty:mainfrom
aaronjmars:security/monolithic-api-keys-auth-gate

Conversation

@aaronjmars

Copy link
Copy Markdown

Summary

The README's documented contract says: configure api_keys in config.json and /v1/* requires Authorization: Bearer <key> or x-api-key: <key>. The modular package (gemini_web2api/server.py) implements that. The monolithic script the README's Quick Start invokes (python gemini_web2api.py) does not — DEFAULT_CONFIG omits api_keys, and do_GET / do_POST never check it. Any request to /v1/chat/completions, /v1/responses, or /v1/models is served regardless of what's configured.

Impact

  • A user follows the README, sets api_keys: ["sk-secret"] in config.json, and believes their proxy is gated. It is not. curl http://host:8081/v1/chat/completions -d '...' with no Authorization header is served.
  • Default host is 0.0.0.0, so the bound socket is reachable from any device on the same LAN (or wider, if a router or container exposes the port).
  • If the operator has configured a cookie file for gemini-3.1-pro routing, every request is signed with that cookie and SAPISIDHASH. Whoever hits the socket consumes the operator's Google account: prompts land in the operator's Gemini activity history, rate-limit / abuse signals attribute back to the operator's IP and Google account, and a Pro subscription cookie hands free Pro access to anyone within reach.

Location

gemini_web2api.py — entrypoint shown in the README's Quick Start:

python gemini_web2api.py
  • DEFAULT_CONFIG (lines 48–61 in HEAD): no api_keys key.
  • GeminiHandler.do_GET (line 437) and do_POST (line 457): no auth check.

The README's Configuration section claims the opposite:

When api_keys is [], authentication is disabled. When one or more keys are set, /v1/* endpoints require Authorization: Bearer <key> or x-api-key: <key>.

That sentence is true for the modular package (gemini_web2api/server.py's _authorized()) but not for the monolithic script.

Fix

Bring the monolithic script to parity with the modular package — same _authorized() shape, same gate location, same documented contract:

  • Add "api_keys": [] to DEFAULT_CONFIG so the config knob is present and serializable.
  • Add _authorized() to GeminiHandler — empty list → allow (anonymous mode, matches docs); non-empty list → require Authorization: Bearer <key> or x-api-key: <key>.
  • Call _authorized() at the top of do_GET / do_POST for /v1/* paths, returning 401 {"error": {"message": "invalid api key"}} on rejection.

No behavior change for users running anonymous (api_keys: []). The change is invisible to them. Users who set api_keys now actually get the gating the README promises.

/ and /v1beta/* remain ungated — same parity choice the modular package makes (the /v1beta path is for the Google-native protocol used by gemini-cli and similar tools, which speak GEMINI_API_KEY=none).

Out of scope for this PR:

Verification

No test suite ships with the repo. A targeted harness (verify-gemini-auth.cjs) exercises the patch by instantiating GeminiHandler against mock rfile / wfile / headers and dispatching do_GET / do_POST directly:

Case Expected Result
api_keys: []GET /v1/models 200 PASS
api_keys: ["sk-1"] → no header 401 PASS
api_keys: ["sk-1"]Authorization: Bearer sk-bad 401 PASS
api_keys: ["sk-1"]Authorization: Bearer sk-1 200 PASS
api_keys: ["sk-1"]x-api-key: sk-1 200 PASS
api_keys: ["sk-1"]POST /v1/chat/completions no key 401 (body not parsed) PASS
api_keys: ["sk-1"]GET / (not under /v1/) 200 PASS
api_keys: ["sk-1"]GET /v1beta/models (Google native) 200 PASS
DEFAULT_CONFIG['api_keys'] [] PASS

python3 -c 'import ast; ast.parse(open("gemini_web2api.py").read())' exits clean.

Detected by

Aeon + Semgrep + manual review (threat-model-claims axis — the README's documented api_keys contract didn't hold at the script the Quick Start points at).

  • Severity: high (silent auth-bypass against documented behavior)
  • CWE-287 (Improper Authentication)
  • CWE-1188 (Insecure Default Initialization of Resource)

Filed by Aeon.

The README documents that setting api_keys in config.json gates /v1/*
behind Bearer / x-api-key auth. The modular package
(gemini_web2api/server.py) implements this via _authorized(). The
monolithic script the README's Quick Start runs
(`python gemini_web2api.py`) does not — DEFAULT_CONFIG omits api_keys,
do_GET / do_POST never check it, so /v1/chat/completions and
/v1/responses accept any request regardless of config.

Combined with the default host of 0.0.0.0, anyone reachable on the
operator's network can use the operator's Google account (and configured
cookie) to drive Gemini, with the activity attributed to the operator's
IP and Google account.

Fix:
- Add api_keys: [] to DEFAULT_CONFIG (defaults to no-auth, matches docs).
- Add _authorized() to GeminiHandler — same logic as the modular package.
- Gate /v1/* in do_GET and do_POST behind _authorized(), returning 401
  with {"error": {"message": "invalid api key"}} on rejection.

Behavior matches the modular package and the README's documented contract:
- api_keys empty → /v1/* open (unchanged for anonymous users).
- api_keys non-empty → /v1/* require Bearer or x-api-key.
- / and /v1beta/* not gated (Google-native path parity with modular).

Detected by Aeon + Semgrep + manual review.
Severity: high (silent auth-bypass against documented behavior).
CWE-287 (Improper Authentication), CWE-1188 (Insecure Default Initialization).
@Sophomoresty Sophomoresty merged commit 75fe411 into Sophomoresty:main Jun 2, 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.

3 participants