可信客户端证书与私钥持有证明通向独立的应用授权分支:reader 获准访问并返回 HTTP 200,合法 guest 被拒绝并返回 HTTP 403。软件私钥可以被复制到别处使用,硬件身份需要额外机制。

客户端证书认证私钥控制能力

双向 TLS 在验证服务器身份的同时,也认证客户端是否能够使用证书对应的私钥。但它并不天然证明某台物理设备的身份。可复制的软件私钥可以被带到别处使用;硬件绑定和设备证明需要额外机制。注册和签发策略可以把密钥关联到设备,但 TLS 本身不会自动建立或验证这项硬件关联。 mTLS 认证私钥控制能力。应用根据已验证身份另行授权,因此有效凭据仍可能得到 403。物理设备身份与凭据生命周期还需要额外控制。 完成认证仍然需要授权 TLS 1.3 的证书式客户端认证中,服务器请求证书,客户端通过 CertificateVerify 证明自己能够使用对应私钥。只发送一份公开证书文件并不够。双方都必须应用各自的信任规则,检查证书链和适当用途;客户端还要验证服务器名称,不能因为自己提交了凭据就放弃验证对端。 服务端信任哪些客户端 CA,与客户端信任哪些服务器 CA,是两个方向的决定。应只接受预期签发者,并明确使用哪个已验证证书字段表示工作负载身份。URI SAN 可以存放应用标识,但谁有资格拿到这个标识,取决于签发者的注册规则。证书里出现了名字,不代表它自动拥有所有 API 权限。 “要求必须提供证书”也不同于“请求对方有的话就提供”。Go 的 RequireAndVerifyClientCert 要求有效客户端证书,而可选模式可能允许没有证书的连接通过。认证之后仍需根据已验证身份执行授权,因此合法访客可以得到 HTTP 403,而获准读取的客户端得到 HTTP 200,这两个结果不矛盾。 ...

九月 6, 2027 · 3 分钟 · XB Field Notes
用户控制的 URL 进入服务端可达边界,经抓取器指向内部目标;此图表示网络位置委托,并非已验证的联系。

SSRF 让服务器替你去开门

服务端请求伪造会让服务器发出由攻击者选择或影响的请求。缩略图服务、导入器、Webhook 测试器或文档渲染器,可能到达请求用户无法直接访问的目标。服务器贡献的是自己的网络位置,以及这条具体抓取路径实际附带的凭据;取一个地址并不意味着进程可见的所有账号凭据都会自动加入请求。评估风险应从真实连接与认证方式开始。 先看清委托了什么权限 内部服务、回环管理端点和云元数据服务,在可达时都是重要目标。后果取决于认证、响应是否可见、允许的方法和网络策略。盲 SSRF 即使不返回目标正文,也可能触发动作或暴露时间信息。不能只凭功能名称给严重程度排位,而应确认它能联系什么,以及一次联系可以产生什么结果,尤其注意有副作用的内部接口。 在 EC2 上,实例附加了角色且相关访问被允许时,角色凭据可能通过元数据提供。角色名称列表与具体角色的凭据文档是不同请求。这些临时凭据代表该角色获得的权限,并不等于无限 AWS 访问,也不保证是实例上每个应用实际采用的凭据。目标没有角色、端点不可达或权限不足时,风险形态会不同,需要依照部署事实判断。 IMDSv2 要求先用 PUT 获取令牌,再在后续元数据请求中提供令牌。强制使用它可以挡住简单、没有认证头的单次 GET 路径,但不能解决能够控制必要方法和请求头的任意 SSRF。应结合最小权限角色与有效网络限制。元数据还可能通过 IPv4,以及启用并受支持时的 IPv6 到达;检查时不能只处理一种地址族。 解析、解析域名,并保留已检查的目标 允许列表应描述功能真正需要的目标。如果可以,接收资源标识而不是任意 URL。否则,应使用与 HTTP 客户端解释一致的解析器,限制协议、主机和端口。不需要的 userinfo 和歧义语法应拒绝,不能因为字符串某处出现了允许的域名,就认定目的地已经获准。解析函数有返回值,也不等于输入符合业务策略。 例如,位于 @ 前面的 trusted.example 是用户信息,不证明目标主机可信。旧式数字 IPv4 写法在不同解析器和解析库中可能有不同解释。IPv6 回环是 ::1,并不是 127.0.0.1,所以搜索一个固定字符串远远不够。一个运行时的解析结果,也不能直接成为另一个客户端行为的可移植保证。验证应围绕实际使用的组件版本建立。 解析允许的域名后,要按照预期网络策略检查返回的全部地址。只允许公网抓取时,应拒绝回环、私有、链路本地、多播以及其他禁止范围。地址库和登记规则会演进,因此要测试真正部署的版本。全球可路由也不代表目标是获准的、可信的或无害的服务;地址分类与业务允许列表解决的是不同问题,应同时保留。 坏掉的样子 先检查 DNS,再让客户端自行重新解析,会留下检查与使用之间的空隙。应通过受支持的传输机制固定经过验证的连接地址,同时保留原本的 TLS 主机名、证书校验和 HTTP authority。代理可能独立解析;因此须检查真实出口路径、重试与回退连接,不能假设一次本地预检控制了它们。固定地址也不能以关闭证书验证为代价。 ...

九月 5, 2027 · 2 分钟 · XB Field Notes
浏览器可能自动携带会话 Cookie;文章中的服务端关卡同时检查 POST、与会话绑定的 CSRF 令牌和可信来源。

CSRF 检查请求到底来自哪里

浏览器可能为攻击者触发的请求附带会话 Cookie。CSRF 防御检查请求上下文是否可信,却不能证明用户真的看过或有意批准某个动作。 本图描述文章的服务端夹具,不是浏览器实测。SameSite 只减少 Cookie 发送;可信上下文不证明用户意图,XSS 与认证仍需独立防护。 Cookie 无意识地跟随请求 浏览器判别是否带上 Cookie,依据的是目标域名、路径、Secure、SameSite 及其他浏览器规则,而不是「这次请求是由用户先前的什么意图触发的」。于是,来自攻击者网站的页面,也能驱动受害者的浏览器发出一条改变状态的请求——改密、改资料——并且这条请求会带上已登录的会话 Cookie。服务器看到这样一条请求,会以为它来自合法用户。这正是关键:请求带了凭据,并不能证明这是用户的本意;浏览器只是在执行它的 Cookie 规则,而不是用户的意志。 令牌把动作与来源绑定 对策是让「改变状态的动作」带一个攻击者无法预料的参数。服务端为每个会话生成一个不可预测的反 CSRF 令牌,在它自己的表单里带上,提交时再比对;攻击者的跨站页面既没有这个令牌,校验就会失败。配合严格的方法约束——不要让 GET 去改变状态——让「带令牌」成为改变状态的唯一途径。比对必须严格:缺失或不匹配就拒绝,而且绝不接受攻击者自带的 origin。 SameSite 是浏览器护栏 SameSite=Strict 不在跨站请求中发送该 Cookie;显式设置 Lax 时,使用安全方法的顶层导航仍可携带它。这些限制减少凭据被跨站运送的机会,但本设计仍要求服务端校验令牌。同站范围大于同源,浏览器支持和请求上下文也会影响结果。示例采用服务端保存的同步令牌,而非双提交 Cookie;两种模式不能仅因为都叫令牌,就混为同一种实现。源内脚本注入可以替用户发出合法上下文中的请求,所以还需单独防御 XSS。 把 Origin 当作来源信号 请求存在 Origin 时先检查它;缺失时,服务端可以检查 Referer 的来源部分。两个字段都可能缺失,Origin 也可能是字面值 null,不能把这些情况当作同源证据。本夹具有意同时要求有效令牌和可信来源信号,缺一即拒绝;实际兼容策略也应明确写出,不能默默跳过整个保护。应比较完整的协议、主机与端口,不能只匹配域名前缀,防止相似域名通过。 ...

九月 4, 2027 · 2 分钟 · XB Field Notes
概念命名空间中,Pod 创建者的直接 get 被拒绝,但能创建消费数据的 Pod。另一条挂载箭头将 Secret 数据交给 Pod。读取权限和工作负载创建权限要一起审查。

Kubernetes Secret 需要明确的访问边界

Kubernetes Secret 是用于保存工作负载所需机密值的 API 对象,提供专门的类型、消费路径和访问控制目标。但它不会让所有其他参与者都无法读取内容。设计边界时,需要同时考虑谁能取回对象、谁能创建消费者、谁能查看消费者输出,以及谁能管理运行它们的节点。仅区分配置对象与秘密对象,还不足以完成权限设计。 Secret 的边界既包括直接读取者,也包括能够创建消费者的主体。窄投射只限制这一次交付,不一定限制创建者。轮换还需要交付、重载和签发方验证。此图是概念示意,本轮没有执行集群实验。 编码与保护处于不同层次 在 JSON API 表示中,Secret 的 data 使用 base64;stringData 则便于在写入时提交字符串。Base64 无需密钥就能还原,并不是加密。这种表示也不能证明某个具体集群怎样保护底层存储。Kubernetes 文档说明,若未配置相应加密,秘密数据默认不在存储层加密。应核对实际 API 服务器和存储设置,不能看到编码文字就推断保密措施已经生效。 存储加密同样不会阻止获得授权的 API 读取者拿到数据。RBAC 中的 get、list 和 watch 权限都可能暴露 Secret 内容,广泛的列表权限并不只是允许查看对象名称。备份和诊断输出也需要保护。正常应用最终仍要获得可用值,因此,一旦有权消费数据的程序被控制,攻击者就已经进入这份数据的信任边界,不能靠编码再次隔离它。 ...

八月 21, 2027 · 3 分钟 · XB Field Notes
历史双目录示例中,只做编译器路径映射会使目标文件一致,但最终可执行文件仍不同。目标文件相同不能证明发布二进制相同。

可复现构建把源码与产物对应起来

可复现构建的含义是:另一方依据声明的源码、构建步骤和相关环境,能够逐字节重建指定产物。这个承诺必须说明比较哪些输出;如果交付的可执行文件不同,只比较目标文件相同是不够的。复现结果为源码与分发内容之间的联系提供证据,而源码审阅、来源证明和签名分别回答其他问题。两次相同的恶意构建仍然有害,被污染的工具链也可能稳定复现自己的异常行为。 图示为文章记录的 Apple 工具链比较,包含仅做编译器处理时最终可执行文件仍不一致的情况。平台专用重跑仍受阻,本次绘图也未重跑构建。本地一致不等于独立复现,也不证明源码安全。 记录真正影响字节的输入 源码版本与编译器版本只是输入的一部分。依赖、目标架构、SDK、链接器、参数、文件遍历顺序、目录路径、时间戳、区域设置和打包工具,都可能进入最终结果。应固定实际相关的输入,并说明比较边界。容器镜像有助于描述环境,却不会自动消除网络下载、宿主机挂载或变化的时间戳。一个镜像标签也不必然代表永远不变的镜像内容。 时间宏和调试信息是常见差异来源。SOURCE_DATE_EPOCH 为支持该约定的工具提供稳定时间,并不会改写所有程序对当前时间的理解。编译器的前缀映射能够规范化嵌入的源文件路径;链接器还可能独立记录目标文件位置或其他元数据。因此,编译阶段消除了差异,仍应继续检查实际链接输出,不能在中间产物相同时就停止验证。 坏掉的样子 只比较挑选出的某个段,可能掩盖真正发布文件中的不同。应明确可复现性覆盖可执行文件、调试符号、压缩归档还是带签名安装包。如果签名过程加入会变化的数据,就说明并论证可复现负载的边界,同时单独校验签名后的分发包。还应记录 SDK 和链接器的构建标识;仅有编译器版本,无法重建另外两者的输入。不能悄悄删除难以处理的字节,再宣称原始文件完全相等;比较范围本身也是需要审阅的契约。 同一台机器上的两次摘要一致,是这些输入条件下本地重复构建的证据,不等于多个独立构建方已经验证,也不能证明源码与工具正确。反过来,摘要不一致首先意味着需要调查,不能立即断定发生了攻击。应定位差异来自哪个组件,再判断是不是有输入没有纳入环境声明。没有记录 SDK、链接参数或打包规则,可能就足以让相同源码生成不同文件。 快速验证 此前记录的实验使用 arm64 macOS、Apple clang 21.0.0 及其本机链接器;本轮没有重新执行这个平台专用构建。保存为 repro-build.py,安装命令行开发工具后运行 python3 repro-build.py。链接参数属于 Apple 工具链,不应假设 Linux 链接器也接受。脚本创建两个临时构建目录,用三种处理方式编译同一小段 C 程序;目录名差异是刻意保留的输入扰动。 from hashlib import sha256 from pathlib import Path from tempfile import TemporaryDirectory import json import os import subprocess source = '#include <stdio.h>\nint main(void) { puts(__FILE__); puts(__DATE__ " " __TIME__); }\n' with TemporaryDirectory() as directory: base = Path(directory) hashes = {mode: {'objects': [], 'executables': []} for mode in ('unmapped', 'compiler-only', 'complete')} output = [] for name in ('one', 'two'): root = base / name root.mkdir() src = root / 'main.c' src.write_text(source) env = {'PATH': os.environ['PATH'], 'LC_ALL': 'C', 'SOURCE_DATE_EPOCH': '1700000000'} for mode in hashes: obj = root / (mode + '.o') binary = root / mode flags = [] if mode == 'unmapped' else [f'-ffile-prefix-map={root}=/src'] subprocess.run(['clang', '-g', '-O0', *flags, '-c', str(src), '-o', str(obj)], cwd=root, env=env, check=True, capture_output=True) os.utime(obj, (1700000000, 1700000000)) linker = ['-Wl,-reproducible'] if mode == 'complete': linker.append(f'-Wl,-oso_prefix,{root}/') subprocess.run(['clang', *linker, str(obj), '-o', str(binary)], cwd=root, env=env, check=True, capture_output=True) hashes[mode]['objects'].append(sha256(obj.read_bytes()).hexdigest()) hashes[mode]['executables'].append(sha256(binary.read_bytes()).hexdigest()) if mode == 'complete': output.append(subprocess.check_output([str(binary)], text=True).splitlines()) equality = {mode: {kind: pair[0] == pair[1] for kind, pair in records.items()} for mode, records in hashes.items()} assert equality == {'unmapped': {'objects': False, 'executables': False}, 'compiler-only': {'objects': True, 'executables': False}, 'complete': {'objects': True, 'executables': True}} assert output == [['/src/main.c', 'Nov 14 2023 22:13:20']] * 2 print(json.dumps({'sha256': hashes, 'equal_between_directories': equality, 'complete_program_output': output, 'same_host_only': True})) 所有处理都使用时间值 1700000000,并固定目标文件的修改时间。没有路径映射时,目标文件和可执行文件都不同。只做编译器映射后,目标文件摘要已经相等,最终可执行文件仍然不同:链接器调试映射中还保留着各自的目标文件路径。这个中间失败说明,编译阶段通过的检查不能直接为实际发布的二进制文件背书。 ...

八月 15, 2027 · 2 分钟 · XB Field Notes
TLS 1.3 完整与恢复握手卡片都标明:无早期数据时通常需要一次 TLS 往返。完整握手使用证书认证,恢复接受 PSK 认证并更新密钥。可选零往返数据仍可被重放。

TLS 1.3 会话恢复复用认证状态

“省一次往返”需要结合协议版本和 early data 模式理解。TLS 1.3 的普通完整握手,以及不携带 early data 的恢复握手,通常都需要一次 TLS 往返。恢复仍然可以减少证书传输和身份认证开销,但开启票据并不意味着每个 HTTPS 请求都能提前一个 RTT 开始。 TLS 1.3 会话恢复在新连接上复用认证状态,并派生新的流量密钥。普通恢复不保证省一次往返;可选早期数据还需要单独处理重放、前向保密与拒绝边界。 新连接仍要派生新的流量密钥 TLS 会话恢复使用先前握手派生的 PSK,为一条新连接建立安全上下文。NewSessionTicket 提供的是与恢复状态关联的不透明标识,不是让后续连接直接沿用原来的流量密钥。新的握手仍会根据自身上下文派生密钥。服务器可以用密码学方法保护票据状态,也可以在不透明标识后保存服务端状态;应用不应把票据解析成一种通用登录凭据。 服务器接受 PSK 认证时,不再发送自己的 Certificate 和 CertificateVerify 消息。恢复可以结合一次新的临时 Diffie-Hellman 交换,获得相应的前向保密;协议也允许仅使用 PSK 的模式,后者没有这项保护。因此,“拿到了票据”既不能证明实际选用了哪种密钥交换,也不能独自证明节省了多少网络等待。 ...

八月 8, 2027 · 2 分钟 · XB Field Notes
两条 POST 路径:JSON 预检被拒绝后不发送 POST;符合条件的 text/plain POST 可以发出,但响应仍可能不可读。

CORS 预检在浏览器发送请求之前检查许可

CORS 预检询问浏览器是否可以发送某个具体的跨源请求,内容包括发起页面的源、计划使用的方法和相关请求头名称。浏览器会先判断预检响应是否允许,再发送实际请求;已有有效预检缓存时,则可能省去这次询问。CORS 同时约束脚本能否读取实际响应。发送许可与响应读取是相互关联的两次检查,但任何一次都不能代替业务身份认证和权限控制。 预检由请求形式触发 源由协议、主机和端口组成,任一部分变化都可能使请求跨源。浏览器不会猜测服务器是否修改数据,再据此决定要不要预检。符合条件的 text/plain 类型 POST 可以直接发送;反而一个仅仅读取数据、却带有 Authorization 头的请求通常需要预检。方法、头部取值以及 Fetch 定义的其他条件共同决定请求是否属于安全列出的范围,“读取不用、写入必用”不是正确规则。 JSON POST 通常需要预检,因为 application/json 不是安全列出的内容类型。浏览器先向目标发出 OPTIONS,携带 Origin、值为 POST 的 Access-Control-Request-Method,以及包含 content-type 的 Access-Control-Request-Headers。这里发送的是未来头部的名称,不是其中可能包含的凭据值。服务器需要允许对应源、方法和相关字段,浏览器才会继续发送这一次实际请求。 两个响应都需要正确配置 受限接口应按完整源进行精确白名单匹配,并在 Access-Control-Allow-Origin 中返回一个允许的源。不能返回逗号分隔的源列表,也不能无条件照抄任意输入。公开给所有来源、且请求不带凭据的资源可以使用星号;携带凭据的访问则需要明确的源和 Access-Control-Allow-Credentials: true。浏览器自己的 Cookie 限制仍然独立生效,CORS 放行不会自动突破这些限制。 实际响应也必须包含适用的 CORS 字段,希望脚本能读取的错误响应同样如此。预检通过,并不能授权脚本读取一个后来缺少许可字段的响应。若返回内容或头部依赖 Origin,还应添加 Vary: Origin,让共享缓存区分不同来源的变体。Access-Control-Max-Age 控制专用预检缓存,并受到浏览器上限约束;不能仅靠普通 HTTP 缓存规则解释为什么某次请求没看到 OPTIONS。 ...

七月 23, 2027 · 2 分钟 · XB Field Notes
客户端中的 Cookie 旁列有 Secure、HttpOnly 与 SameSite。箭头仅把 Cookie 名称和值送往服务端;服务端仍需授权。

Cookie 标记约束支持它们的客户端

Secure、HttpOnly 和 SameSite 分别描述支持这些规则的浏览器如何限制 Cookie。服务端收到的是最终的名称与值,而不是一张证明这些限制已经执行的证书。浏览器行为与服务端授权必须分开:Cookie 完全可能被正确发送,但当前请求的业务动作仍然不被允许,不能把出现 Cookie 头当成全部安全条件已经满足。 分开看设置、发送与脚本读取 Secure 限制 Cookie 通过安全连接发送,通常就是 HTTPS。现代浏览器 Cookie 规则还会拒绝从不安全语境设置 Secure Cookie;localhost 等开发例外应在目标浏览器中核对,而不能推广为任意明文主机都可信。这不仅是发送阶段的标记。一次 HTTPS 重定向无法撤回此前明文请求已经泄露的令牌,因此应结合 HTTPS 与适当的 HSTS 策略,而不是让两者互相替代。 HttpOnly 阻止通过 document.cookie 等非 HTTP 脚本接口读取 Cookie,但不会禁止浏览器把它附到符合条件的脚本请求上。被注入的同源 JavaScript 即使读不到令牌,也仍可能以用户身份发起操作。因此必须单独预防和限制 XSS;HttpOnly 减少的是其中一条窃取路径,并不是把脚本注入漏洞消除了。 SameSite 增加请求语境限制。Strict 通常允许同站请求,Lax 还允许使用安全方法的跨站顶层导航。显式 Lax 不会仅因为某次跨站 POST 是从顶层页面发起,就让 Cookie 自动附上。None 去掉这一项限制,但在支持规则的浏览器中必须搭配 Secure。第三方 Cookie 阻止、存储分区以及 Fetch 的凭据模式,仍可能阻止发送,不能把 None 理解成任何请求都无条件携带。 ...

六月 17, 2027 · 2 分钟 · XB Field Notes