可复用访问令牌会把某种约定访问能力交给持有它的人。把令牌放进地址,会让这项能力进入平时就会复制地址的系统,例如访问日志、监控记录、浏览器导航历史、书签和分享链接。具体暴露范围取决于请求及策略,但查询参数的名称看起来像秘密,并不会自动让它受到保护。传输位置本身就是凭据披露边界的一部分,应当结合它会经过哪些记录和分享环节判断。
对于 OAuth 持有者令牌请求,通常应采用授权请求头机制。HTTPS 仍然不可缺少,因为把值从查询移动到请求头并不会加密它;TLS 保护的是到终止位置为止的传输数据。传输结束后,应用和中间组件仍然能够检查字段。请求头的直接收益,是让令牌离开请求地址,而不是保证它永远不会被日志或诊断工具捕获。不能把两种不同的保护作用混为一谈。
分清减少暴露与绝对保密 某些访问日志只记录方法、请求目标和状态。在这种格式里,请求头令牌不会出现在目标字段中,而查询令牌会出现。但其他系统可能采集请求头,因此授权字段仍需在日志、链路和错误报告配置中被明确处理。应验证真实管线究竟保存了什么,不能假定请求头可以绕过全部日志层。检查时使用无效的合成值即可,不应为了演示而把真正凭据再次写入更多记录。
引用来源策略能够减少源地址向后续目标披露,却不会删除已经发给目的地的查询内容,也不会抹去浏览器记录或改写已复制的链接。同样,禁止缓存响应并不是要求清除全部访问日志和导航历史的通用指令。把令牌移到片段,会改变 HTTP 请求发送哪些内容,但值仍可被页面脚本和浏览器地址状态访问,也不是适合所有场景的凭据存储方案。
坏掉的样子 团队可能把接口认证从查询参数改成请求头,却让旧的带令牌地址继续留在报告和共享书签中。传输方式改变并不会撤销此前暴露,需要调查受影响的副本,并在系统支持时使已经暴露的可复用凭据失效。另一种误判是把有效期很短的链接称为泄露也无所谓;在有效期间,任何取得这项能力的人仍然可能使用它。缩短时间窗口只能减少风险,不能取消披露的后果。
当实际客户端流程需要时,签名地址可以是有意设计的、范围很窄的资源访问方式,但允许访问的资源、操作、期限和处理规则都应明确审阅。到期失效与只能使用一次并不相同,要阻止到期之前重复使用,需要真正强制执行消费状态的机制。简单的回调接口或浏览器导航,也不会自动构成把无限制可复用接口凭据放进查询的理由,应根据真实客户端选择合适的认证流程。
快速验证 保存代码为 token-placement.py,执行 python3 token-placement.py –serve,打开打印出的页面。本次使用 Python 3.14.4 和 Chrome 152,全部演示值都是明确无效的固定标记,不能替换为真实凭据。回环服务只是观察实验,不是认证服务;它记录请求目标以及示例授权头是否存在,不记录该请求头的值。结束后按控制键加 C 关闭服务,临时事件仅留在本次进程的内存中。
import json import sys from http.server import BaseHTTPRequestHandler, HTTPServer from threading import Event, Thread from urllib.parse import urlsplit from urllib.request import Request, urlopen # Synthetic demonstration value only; this server is not an authentication service. demo = "DEMO-NOT-A-SECRET" events = [] class Handler(BaseHTTPRequestHandler): def log_message(self, *args): pass def do_GET(self): path = urlsplit(self.path).path if path == "/events": body = json.dumps(events).encode() kind = "application/json" elif path == "/echo": entry = {"request_target": self.path, "authorization_present": self.headers.get("Authorization") == "Bearer " + demo, "referer": self.headers.get("Referer")} events.append(entry) body = json.dumps(entry).encode() kind = "application/json" else: events.append({"request_target": self.path, "document": True}) body = ("""<!doctype html><meta charset="utf-8"><title>Token placement lab</title> <h1>A clean referrer does not erase the request URL</h1><pre id="result">Running...</pre><script> const demo=""" + json.dumps(demo) + """; async function run() { const query=await (await fetch('/echo?access_token='+demo)).json(); const header=await (await fetch('/echo',{headers:{Authorization:'Bearer '+demo}})).json(); await new Promise(resolve=>requestAnimationFrame(resolve)); const resourceUrls=performance.getEntriesByType('resource').map(entry=>entry.name).filter(name=>name.includes('/echo')); window.labResult={page:location.href,query,header,resourceUrls}; document.getElementById('result').textContent=JSON.stringify(window.labResult,null,2); } run().catch(error=>{window.labError=String(error);}); </script>""").encode() kind = "text/html; charset=utf-8" self.send_response(200) self.send_header("Content-Type", kind) self.send_header("Content-Length", str(len(body))) self.send_header("Referrer-Policy", "no-referrer") self.send_header("Cache-Control", "no-store") self.end_headers() self.wfile.write(body) server = HTTPServer(("127.0.0.1", 0), Handler) thread = Thread(target=server.serve_forever, daemon=True) thread.start() origin = f"http://127.0.0.1:{server.server_port}" try: with urlopen(origin + "/echo?access_token=" + demo, timeout=5) as response: query = json.load(response) with urlopen(Request(origin + "/echo", headers={"Authorization": "Bearer " + demo}), timeout=5) as response: header = json.load(response) assert demo in query["request_target"] and query["authorization_present"] is False assert demo not in header["request_target"] and header["authorization_present"] is True events.clear() print(json.dumps({"app_url": origin + "/?access_token=" + demo, "wire_placement_checks": "PASS", "real_credentials_used": False}), flush=True) if "--serve" in sys.argv: Event().wait() except KeyboardInterrupt: pass finally: server.shutdown() thread.join(timeout=5) server.server_close() 页面先通过 access_token 查询发出一次请求,再通过授权请求头发出另一次请求。因为页面设置了不发送引用来源,两次接收结果中的 Referer 都为空。然而查询请求的目标仍然包含演示标记,请求头方案的目标则只是 echo 路径。浏览器资源计时条目中的地址也体现同样差异。页面自身的地址同样带有演示标记,因此当前导航记录仍保留这条完整地址,不能因为后续请求没有来源头就认为它消失了。
...