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.
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.
// 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
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.
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.
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.