From b99e3404a26bdd0e24768b902c307472a8360cfd Mon Sep 17 00:00:00 2001 From: aeonframework Date: Tue, 2 Jun 2026 08:57:12 +0000 Subject: [PATCH] fix(security): honor api_keys auth in monolithic gemini_web2api.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- gemini_web2api.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/gemini_web2api.py b/gemini_web2api.py index 7b1903d..fab9187 100644 --- a/gemini_web2api.py +++ b/gemini_web2api.py @@ -58,6 +58,7 @@ "log_requests": True, "cookie_file": None, "proxy": None, + "api_keys": [], } CONFIG = dict(DEFAULT_CONFIG) @@ -427,6 +428,14 @@ def send_json(self, data, status=200): self.end_headers() self.wfile.write(body) + def _authorized(self): + keys = CONFIG.get("api_keys") or [] + if not keys: + return True + auth = self.headers.get("Authorization", "") + key = auth[7:] if auth.startswith("Bearer ") else self.headers.get("x-api-key", "") + return key in keys + def do_OPTIONS(self): self.send_response(204) self.send_header("Access-Control-Allow-Origin", "*") @@ -436,6 +445,9 @@ def do_OPTIONS(self): def do_GET(self): try: + if self.path.startswith("/v1/") and not self._authorized(): + self.send_json({"error": {"message": "invalid api key"}}, 401) + return if self.path == "/v1/models": self.send_json({"object": "list", "data": [ {"id": n, "object": "model", "created": 1700000000, @@ -456,6 +468,9 @@ def do_GET(self): def do_POST(self): try: + if self.path.startswith("/v1/") and not self._authorized(): + self.send_json({"error": {"message": "invalid api key"}}, 401) + return length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) if length else b"" if self.path == "/v1/chat/completions":