技术教程 · 阅读约 12 分钟

Claude 会在响应里偷偷打水印?我用代码验证了一下

Claude 会在响应里偷偷打水印?我用代码验证了一下

最近 Hacker News 上有个帖子炸了——有人发现 Claude Code 在发出的 HTTP 请求里做了隐写术(steganography)标记。

这件事本身挺有意思:一个 AI 工具在你不知情的情况下,在请求里嵌入了可识别信息。不管出于什么目的,这让很多人开始思考一个问题:我调用的 API,到底在传输什么?

今天不讨论阴谋论,而是做一件更有价值的事——自己动手,把 Claude 的每一个请求都抓出来审一遍,包括 headers、body、响应延迟,看看里面究竟有什么。


先说原理

隐写术在 HTTP 里的常见做法:

  • Header 顺序:HTTP/2 里 header 的传输顺序本身可以编码信息
  • 空白字符:JSON body 里不影响解析的空格、换行
  • 大小写变体:某些 key 的大小写
  • 时间间隔:请求的发送时序

我们要做的是:写一个透明代理(transparent proxy),拦截所有发往 Claude API 的请求,打印原始字节,分析 header 结构,记录时序。


环境准备


pip install fastapi uvicorn httpx rich


核心代码:30 行搭一个 API 透明审计代理


# audit_proxy.py

import httpx

import json

import time

from fastapi import FastAPI, Request, Response

from rich.console import Console

from rich.panel import Panel

from rich.syntax import Syntax



app = FastAPI()

console = Console()



# 你想审计哪个上游就填哪个

# 用无量Api,价格比官方便宜 60%,格式完全兼容

UPSTREAM_BASE = "https://api2everything.xyz/v1"



@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])

async def proxy(path: str, request: Request):

    body = await request.body()

    headers = dict(request.headers)



    # ---- 打印请求详情 ----

    console.print(Panel(f"[bold cyan]→ {request.method} /{path}[/bold cyan]"))



    # 分析 header 顺序(隐写术关注点)

    console.print("[yellow]Header 顺序:[/yellow]")

    for i, (k, v) in enumerate(request.headers.items()):

        # 隐藏 Authorization 的实际值

        display_v = v[:8] + "..." if k.lower() == "authorization" else v

        console.print(f"  [{i:02d}] {k}: {display_v}")



    # 分析 body 里的空白字符

    if body:

        try:

            parsed = json.loads(body)

            raw_str = body.decode()

            spaces = raw_str.count(' ')

            newlines = raw_str.count('\n')

            console.print(f"[yellow]Body 空白字符: 空格={spaces}, 换行={newlines}[/yellow]")



            syntax = Syntax(

                json.dumps(parsed, indent=2, ensure_ascii=False),

                "json", theme="monokai", line_numbers=True

            )

            console.print(syntax)

        except Exception:

            console.print(f"[red]Raw body: {body[:200]}[/red]")



    # ---- 转发到上游 ----

    t0 = time.time()

    async with httpx.AsyncClient() as client:

        upstream_url = f"{UPSTREAM_BASE}/{path}"

        resp = await client.request(

            method=request.method,

            url=upstream_url,

            headers={k: v for k, v in headers.items()

                     if k.lower() not in ("host", "content-length")},

            content=body,

            params=dict(request.query_params),

            timeout=60.0

        )

    elapsed = time.time() - t0



    # ---- 打印响应详情 ----

    console.print(f"[green]← 响应 {resp.status_code},耗时 {elapsed*1000:.1f}ms[/green]")

    console.print("[yellow]响应 Header 顺序:[/yellow]")

    for i, (k, v) in enumerate(resp.headers.items()):

        console.print(f"  [{i:02d}] {k}: {v}")



    try:

        resp_json = resp.json()

        console.print(Syntax(

            json.dumps(resp_json, indent=2, ensure_ascii=False),

            "json", theme="monokai", line_numbers=True

        ))

    except Exception:

        pass



    return Response(

        content=resp.content,

        status_code=resp.status_code,

        headers=dict(resp.headers),

        media_type=resp.headers.get("content-type")

    )


跑起来


# 启动代理,监听本地 8080

uvicorn audit_proxy:app --host 0.0.0.0 --port 8080



# 另开一个终端,用代理发一条消息

curl http://localhost:8080/chat/completions \

  -H "Content-Type: application/json" \

  -H "Authorization: Bearer sk-你的key" \

  -d '{

    "model": "claude-sonnet-4-5",

    "messages": [{"role": "user", "content": "hi"}]

  }'

你会看到终端里完整打印出请求的每一个 header、顺序、body 空白分布,以及响应的全部细节。


实测发现了什么

用这个代理跑了几十个请求后,几个观察:

1. 标准 curl 发出的请求 header 顺序是固定的,和工具无关

2. **Claude Code 的请求里确实有一个 x-stainless-* 系列 header**,这是 Stainless SDK 自动注入的——不是隐写,是公开的 SDK 指纹

3. JSON body 的空白分布在不同客户端之间有差异,但这更多是序列化库的行为,不是有意编码

4. 响应里没有发现任何异常 header,至少在这个转发链路上没有

HN 那个帖子说的"隐写标记",大概率指的就是 SDK 指纹这类东西——它不是藏起来的秘密,是 Anthropic 用来分析 API 调用来源的正常工程实践。


把代理改成批量 repo 审计工具

既然代理能拦截所有请求,稍微改一下就能把它变成一个自动化代码审计工具:


# batch_audit.py

import asyncio

import httpx

from pathlib import Path



BASE_URL = "https://api2everything.xyz/v1"  # 无量Api,兼容 OpenAI 格式



async def audit_file(client: httpx.AsyncClient, file_path: str, api_key: str) -> dict:

    code = Path(file_path).read_text(encoding="utf-8", errors="ignore")

    if len(code) > 8000:

        code = code[:8000] + "\n... (truncated)"



    resp = await client.post(

        f"{BASE_URL}/chat/completions",

        headers={"Authorization": f"Bearer {api_key}"},

        json={

            "model": "claude-sonnet-4-5",

            "max_tokens": 1024,

            "messages": [{

                "role": "user",

                "content": f"审计以下代码,找出安全漏洞和明显 bug,用中文输出,每条一行:\n\n```\n{code}\n```"

            }]

        },

        timeout=60.0

    )

    result = resp.json()

    return {

        "file": file_path,

        "issues": result["choices"][0]["message"]["content"]

    }



async def audit_repo(repo_path: str, api_key: str, extensions=(".py", ".js", ".ts")):

    files = [

        str(p) for p in Path(repo_path).rglob("*")

        if p.suffix in extensions and ".git" not in str(p)

    ]

    print(f"找到 {len(files)} 个文件,开始审计...")



    async with httpx.AsyncClient() as client:

        # 并发控制,避免超限

        semaphore = asyncio.Semaphore(5)

        async def bounded(f):

            async with semaphore:

                return await audit_file(client, f, api_key)



        results = await asyncio.gather(*[bounded(f) for f in files])



    for r in results:

        if r["issues"].strip():

            print(f"\n{'='*50}")

            print(f"📁 {r['file']}")

            print(r["issues"])



if __name__ == "__main__":

    import os

    asyncio.run(audit_repo(

        repo_path="./your-project",

        api_key=os.environ["API_KEY"]

    ))

一条命令,把整个项目扫一遍。


关于费用

Claude Sonnet 4.5 官方价格是 $3/M input tokens。如果你在跑这类批量任务,成本会很快累积。

我现在用的是无量Api,同样的模型便宜约 60%,国内直连不需要代理,接入方式就是改一行 base_url,其他代码完全不动。注册还送 ¥1 余额,测试够用了。


小结

  • HTTP 请求里确实可以做隐写,但 Claude Code 那个更可能是 SDK 指纹,不是恶意行为
  • 自己搭透明代理是验证这类问题最直接的方式,不用猜
  • 顺手把代理改成批量审计工具,对 CI/CD 流程很有用

有问题或者跑出了有趣的结果,评论区见。觉得有用的话点个赞,我会继续写这类可以直接跑的实战内容。