【Bug已解决】Allow custom `http_headers` / `env_http_headers` on the built-in `amazon-bedrock` model p... 【Bug已解决】Allow customhttp_headers/env_http_headerson the built-inamazon-bedrockmodel provider 解决方案原始需求线索Allow customhttp_headers/env_http_headerson the built-inamazon-bedrockmodel provider希望在内置的模型提供方上允许用户自定义额外的 HTTP 请求头和环境变量以满足企业网关、签名、路由等需求。一、背景为什么要允许用户自定义头与环境企业接入大模型提供方时常在「客户端」和「提供方」之间插入一层企业网关 / 代理 / 签名中间件。这类中间件往往要求额外的 HTTP 头如X-Gateway-Key、X-Route-To、X-Request-Id特定的环境变量如AWS_REGION覆盖、私有 CA 路径SSL_CERT_FILE自定义签名头如X-Amz-*变体。 内置提供方如果不开放「自定义头 / 环境」的口子用户就被卡死只能放弃内置、自己裸写客户端。但开放这个口子也意味着注入攻击面随之而来。二、安全风险盲目注入会出事2.1 覆盖鉴权头用户自定义的Authorization头若被框架「无脑合并」可能覆盖框架自己算好的正确鉴权头 → 请求被服务端拒或更糟把用户填的明文密钥发到错误的地方。2.2 泄露密钥到日志自定义头里若含密钥X-Api-Key: sk-xxx框架若把完整请求头打进日志密钥就泄露了。2.3 污染全局环境env_http_headers若直接用os.environ.update(...)改了进程全局环境会影响同进程里其他提供方 / 其他请求造成串扰。2.4 注入到不该去的请求自定义头若被应用到「所有提供方」而非「指定提供方」会把 A 网关的头发到 B 提供方引发混乱。三、为什么实现会踩坑根因3.1 合并策略错误头合并用「用户优先」而非「框架鉴权头优先」导致用户误覆盖关键头。3.2 环境作用域错误环境变量直接改全局os.environ而非「仅对该次请求 / 该子进程」生效。3.3 缺少头白/黑名单不做任何校验什么头都能塞包括Content-Length、Host这类框架必须自己管的头。3.4 日志未脱敏把含密钥的头原样打印。四、最小可运行复现错误合并覆盖鉴权头下面演示「用户头无脑覆盖框架头」如何破坏鉴权def merge_headers_bad(framework_headers, user_headers): # 错误用户头直接覆盖可能覆盖 Authorization return {**framework_headers, **user_headers} framework {Authorization: Bearer 正确的框架令牌, Content-Type: application/json} user {Authorization: Bearer 用户瞎填的} # 用户误填 merged merge_headers_bad(framework, user) print(merged[Authorization]) # 变成用户的 - 鉴权失败/泄露运行后Authorization被用户值覆盖框架算好的正确令牌丢失。五、解决方案一受保护的头合并框架优先鉴权 / 框架关键头必须「只读」用户自定义头只能补充、不能覆盖它们# 框架必须自己管的头用户不可覆盖 PROTECTED_HEADERS { authorization, content-length, host, content-type, x-amz-content-sha256, x-amz-date, } def merge_headers_safe(framework_headers, user_headers): merged dict(framework_headers) # 先放框架头含鉴权 for k, v in (user_headers or {}).items(): lk k.lower() if lk in PROTECTED_HEADERS: # 受保护头忽略用户值保留框架值 continue merged[k] v # 其余用户头补充进来 return merged if __name__ __main__: fw {Authorization: Bearer 框架, Content-Type: application/json} usr {Authorization: Bearer 用户, X-Gateway-Key: gw-123} m merge_headers_safe(fw, usr) print(Authorization:, m[Authorization]) # 仍是框架的 print(X-Gateway-Key:, m[X-Gateway-Key]) # 用户补充的头生效框架鉴权头始终优先用户只能加「额外」头如X-Gateway-Key。六、解决方案二环境变量的作用域隔离env_http_headers不应污染全局os.environ而只在该提供方的请求/子进程作用域内生效import os import subprocess def run_with_scoped_env(base_env, user_env, cmd): 合并环境但只作用于本次子进程不改全局 os.environ。 env dict(os.environ) # 复制当前环境不修改原对象 env.update(base_env or {}) for k, v in (user_env or {}).items(): if k.upper() in {PATH, HOME, USER}: continue # 禁止覆盖关键环境防串扰 env[k] str(v) # subprocess 用这份独立 env父进程 os.environ 不受影响 return subprocess.run(cmd, envenv, capture_outputTrue, textTrue) if __name__ __main__: origin os.environ.get(AWS_REGION) run_with_scoped_env({AWS_REGION: us-east-1}, {SSL_CERT_FILE: /custom/ca.pem}, [env]) # 演示子进程看到自定义环境 print(父进程 AWS_REGION 未变:, os.environ.get(AWS_REGION) origin)env只传给子进程父进程环境原封不动——杜绝全局污染根因 3.3。七、解决方案三头的白/黑名单校验开放自定义头前先做结构校验禁止危险头、限制格式、脱敏日志import re HEADER_NAME_RE re.compile(r^[A-Za-z][A-Za-z0-9-]*$) SENSITIVE_PREFIX (x-api-key, x-auth, authorization, cookie, proxy-authorization) def validate_user_headers(user_headers): problems [] sanitized {} for k, v in (user_headers or {}).items(): lk k.lower() if lk in PROTECTED_HEADERS: problems.append(f头 {k} 受保护忽略) continue if not HEADER_NAME_RE.match(k): problems.append(f头名非法: {k}) continue if len(str(v)) 4096: problems.append(f头值过长: {k}) continue sanitized[k] v return sanitized, problems def redact_for_log(headers): 日志脱敏敏感头只显示前缀。 out {} for k, v in headers.items(): if k.lower() in SENSITIVE_PREFIX: out[k] f{str(v)[:4]}***(已脱敏) else: out[k] v return out if __name__ __main__: h, p validate_user_headers({X-Gateway-Key: gw, Authorization: x, Bad;Header: y}) print(校验问题:, p) print(脱敏日志:, redact_for_log({X-Api-Key: sk-secret, X-Gateway-Key: gw}))校验 脱敏双保险非法/受保护头被拦敏感头打日志时自动掩码。八、跨提供方隔离自定义头/环境必须绑定到具体提供方不能泄漏给其他提供方class ProviderConfig: def __init__(self, name): self.name name self.extra_headers {} self.extra_env {} def request_headers(self, framework_headers): # 只合并「本提供方」的自定义头 return merge_headers_safe(framework_headers, self.extra_headers) # 用法bedrock 提供方的头只给 bedrock bedrock ProviderConfig(bedrock) bedrock.extra_headers {X-Gateway-Key: gw-bedrock} openai ProviderConfig(openai) openai.extra_headers {X-Gateway-Key: gw-openai} # 互不影响每个提供方持自己的extra_headers合并时只取自己的绝不会串到别家。九、排查清单「自定义头/环境」功能出问题按下面排查用户头是否覆盖了鉴权头合并策略是否框架优先第五节是否污染全局环境env_http_headers应只作用于子进程/请求第六节是否做头校验非法名、超长、受保护头是否被拦第七节日志是否脱敏含密钥的头是否掩码是否绑定具体提供方自定义头有没有泄漏给其他提供方第八节环境变量是否覆盖关键项PATH/HOME应禁止失败是否明确报错不合法的头应告知用户而非静默忽略是否有默认值回退用户没设时不报错、用框架默认。十、小结「在内置提供方上开放自定义 http_headers / env_http_headers」的通用工程要点受保护头优先合并时用「框架鉴权头优先、用户头只补充」杜绝覆盖Authorization等第五节环境作用域隔离env_http_headers只注入子进程/请求环境不碰全局os.environ且禁改PATH/HOME第六节校验 脱敏头名正则、超长拦截、受保护头忽略日志对敏感头掩码提供方隔离自定义头/环境绑定具体提供方互不串扰第八节。 一句话给用户开「自定义注入」的口子等于在请求链上开了后门——必须用「框架关键头只读 环境作用域隔离 校验脱敏 提供方隔离」四把锁把它变成可控的前门。灵活性与安全性从不矛盾矛盾的是「只顾灵活忘了边界」。这与第 86 篇路径边界、第 100 篇 scope 校验、第 121 篇登录态隔离共同体现「任何用户可控的注入点都必须有白名单与边界」的工程纪律。