Skip to content

Case Studies & ProjectsSecurity Researcher

Hacking AI: DeepSeek & Gemini Frontend APIs Cracked

By Burhan Moin

Overview

Both DeepSeek Chat and Google Gemini's web UIs enforce "unlimited messages" only through the browser — there is no equivalent public API tier with that limit. I suspected the actual API calls were standard HTTP requests with some client-side signing or encryption layer. By intercepting and decrypting that layer I was able to replicate the full request/response cycle from a local CLI — giving me programmatic access to both models at whatever rate the frontend allowed.

DeepSeek — Decrypting the Request Signing Scheme

Interception setup

I proxied DeepSeek Chat's browser traffic through Burp Suite with SSL interception enabled. The initial capture showed that chat message payloads were not plain JSON — the message body was encrypted before being sent to /api/chat/completions.

text·burp_capture.txt
POST /api/chat/completions HTTP/2
Host: chat.deepseek.com
Content-Type: application/json

{
  "model": "deepseek-chat",
  "messages": [...],
  "x-ds-sign": "a3f9c...",
  "x-ds-ts":   "1748901234"
}

The x-ds-sign header was the signing token. Its value changed with every request, ruled out a static key.

JavaScript source analysis

I pulled the minified frontend bundle from the browser's network tab and ran it through a JS beautifier. Searching for the string "x-ds-sign" led me directly to the signing function. It was a simple HMAC-SHA256 over a concatenation of the timestamp, a fixed nonce derived from the user's session token, and a hash of the request body.

javascript·signing_logic.js
// Deobfuscated signing logic (DeepSeek)
function buildSign(sessionToken, body, timestamp) {
  const nonce   = sha256(sessionToken).slice(0, 16);
  const payload = `${timestamp}:${nonce}:${sha256(body)}`;
  return hmacSHA256(payload, STATIC_SECRET);
}

STATIC_SECRET was a hardcoded string in the bundle — not rotated per-user, not fetched from a server. Once I had that, I could sign any request myself.

Python client

python·deepseek_cli.py
import hashlib, hmac, time, json, requests

STATIC_SECRET = "..."          # extracted from bundle
SESSION_TOKEN = "..."          # from browser cookies

def sha256_hex(s: str) -> str:
    return hashlib.sha256(s.encode()).hexdigest()

def sign(body: str) -> dict:
    ts    = str(int(time.time()))
    nonce = sha256_hex(SESSION_TOKEN)[:16]
    data  = f"{ts}:{nonce}:{sha256_hex(body)}"
    sig   = hmac.new(STATIC_SECRET.encode(), data.encode(), hashlib.sha256).hexdigest()
    return {"x-ds-sign": sig, "x-ds-ts": ts}

def chat(messages: list) -> str:
    body    = json.dumps({"model": "deepseek-chat", "messages": messages, "stream": False})
    headers = {**sign(body), "Content-Type": "application/json",
               "Cookie": f"session={SESSION_TOKEN}"}
    r = requests.post("https://chat.deepseek.com/api/chat/completions",
                      headers=headers, data=body)
    return r.json()["choices"][0]["message"]["content"]

print(chat([{"role": "user", "content": "Hello from CLI"}]))

Gemini — Replicating the Frontend API

Traffic analysis

Gemini's web app (gemini.google.com) uses a different approach: requests go to a Google API endpoint with a batchexecute-style RPC format. The payload is a deeply nested, URL-encoded JSON structure — not an obvious REST call. The signing is simpler: a session cookie from Google auth plus a per-page CSRF-like token (f.sid) embedded in the page HTML.

text·gemini_capture.txt
POST /u/0/_/BardChatUi/data/batchexecute HTTP/2
Host: gemini.google.com

f.req=%5B%5B%5B%22oPEP8c%22%2C%22...encoded_payload...%22...

Payload reconstruction

I decoded several captured f.req values to understand the envelope format. The inner payload is a JSON array: [prompt_text, null, conversation_id, ...]. I wrote a builder that constructs and URL-encodes the full RPC envelope.

python·gemini_cli.py
import urllib.parse, json, re, requests

def build_freq(prompt: str, conv_id: str = "") -> str:
    inner = json.dumps([prompt, None, conv_id, None, None, None, []])
    outer = json.dumps([[["oPEP8c", inner, None, "generic"]]])
    return urllib.parse.quote(f"[{outer}]")

def chat(prompt: str, session_cookies: dict, fsid: str, conv_id: str = "") -> str:
    data = (
        f"f.req={build_freq(prompt, conv_id)}"
        f"&at={fsid}&"
    )
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    r = requests.post(
        "https://gemini.google.com/u/0/_/BardChatUi/data/batchexecute",
        headers=headers, cookies=session_cookies, data=data
    )
    # Response is a streaming JSON envelope — extract text
    raw = r.text.split("\n")
    for line in raw:
        try:
            parsed = json.loads(line)
            text = parsed[0][2]
            if text:
                return json.loads(text)[4][0][1][0]
        except Exception:
            pass
    return ""

Impact

With both clients working locally I can send programmatic requests to either model at the full rate the frontend session allows — no API key required, no per-token billing, no request-count cap visible to the UI rate limiter. I integrated both into personal automation scripts for summarisation, code review, and research pipelines.

[ Code output screenshots and Burp Suite capture images to be added here ]

Key Takeaways

Frontend "security" enforced only in JavaScript is not security — it is obfuscation. Static secrets in bundled JS, HMAC signatures computed client-side, and RPC envelope formats that are "hidden" through encoding are all fully recoverable. Proper rate-limiting must be enforced server-side with authenticated API keys and per-token accounting.