观察、决策、行动三个方框,旁边强调验证客户端之后仍需授权。

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

双向 TLS 在验证服务器身份的同时,也认证客户端是否能够使用证书对应的私钥。但它并不天然证明某台物理设备的身份。可复制的软件私钥可以被带到别处使用;硬件绑定和设备证明需要额外机制。注册和签发策略可以把密钥关联到设备,但 TLS 本身不会自动建立或验证这项硬件关联。 完成认证仍然需要授权 TLS 1.3 的证书式客户端认证中,服务器请求证书,客户端通过 CertificateVerify 证明自己能够使用对应私钥。只发送一份公开证书文件并不够。双方都必须应用各自的信任规则,检查证书链和适当用途;客户端还要验证服务器名称,不能因为自己提交了凭据就放弃验证对端。 服务端信任哪些客户端 CA,与客户端信任哪些服务器 CA,是两个方向的决定。应只接受预期签发者,并明确使用哪个已验证证书字段表示工作负载身份。URI SAN 可以存放应用标识,但谁有资格拿到这个标识,取决于签发者的注册规则。证书里出现了名字,不代表它自动拥有所有 API 权限。 “要求必须提供证书”也不同于“请求对方有的话就提供”。Go 的 RequireAndVerifyClientCert 要求有效客户端证书,而可选模式可能允许没有证书的连接通过。认证之后仍需根据已验证身份执行授权,因此合法访客可以得到 HTTP 403,而获准读取的客户端得到 HTTP 200,这两个结果不矛盾。 坏掉的样子 没有证书、签发者不受信任、凭据过期或扩展用途不合适,都可能阻止客户端认证。把服务端改为任意证书都接受,或者关闭客户端对服务器的验证,只是隐藏边界,不是在修复凭据问题。应区分握手失败和应用层拒绝,让排查人员能定位到实际负责的那一层。 私钥需要受控存储、签发、轮换和事件响应。续签证书并不必然更换私钥;被盗且仍被接受的私钥可能用于冒充,不能把等待过期当作唯一处置方式。Go 的证书 Verify 不执行吊销检查,因此发布了一条吊销记录,也不等于每个调用方已经应用它,接受策略必须另行核实。 TLS 终止位置还决定了身份在哪里被建立。后端收到调用方自行填写的身份 Header,不代表后端就验证了调用方证书。受信任的身份传递需要自己的认证边界和防伪造规则;凭据或权限变化时,既有连接、恢复会话如何处理,也必须有明确的生命周期策略。 快速验证 保存为 mtls_probe.go,使用 Go 1.23 或更新版本运行 go run mtls_probe.go。程序在内存中生成客户端 CA 和凭据,把 notBefore 回拨一分钟,并把 notAfter 设为生成时刻之后一小时。回环服务端使用 httptest 自带的测试服务器证书,配套测试客户端信任这张证书。两侧信任库都不安装到系统全局,也不打印或写出私钥。 ...

九月 6, 2027 · 3 分钟 · XB Field Notes
服务器被诱导去访问内网地址。

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
文章标题旁的三个相连面板,依次标注观察、决策与行动。

CSRF 检查请求到底来自哪里

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

九月 4, 2027 · 2 分钟 · XB Field Notes
文章标题旁是标有 OBSERVE、DECIDE、ACT 的三个通用面板,以及箭头和圆形仪表。

Kubernetes Secret 需要明确的访问边界

Kubernetes Secret 是用于保存工作负载所需机密值的 API 对象,提供专门的类型、消费路径和访问控制目标。但它不会让所有其他参与者都无法读取内容。设计边界时,需要同时考虑谁能取回对象、谁能创建消费者、谁能查看消费者输出,以及谁能管理运行它们的节点。仅区分配置对象与秘密对象,还不足以完成权限设计。 编码与保护处于不同层次 在 JSON API 表示中,Secret 的 data 使用 base64;stringData 则便于在写入时提交字符串。Base64 无需密钥就能还原,并不是加密。这种表示也不能证明某个具体集群怎样保护底层存储。Kubernetes 文档说明,若未配置相应加密,秘密数据默认不在存储层加密。应核对实际 API 服务器和存储设置,不能看到编码文字就推断保密措施已经生效。 存储加密同样不会阻止获得授权的 API 读取者拿到数据。RBAC 中的 get、list 和 watch 权限都可能暴露 Secret 内容,广泛的列表权限并不只是允许查看对象名称。备份和诊断输出也需要保护。正常应用最终仍要获得可用值,因此,一旦有权消费数据的程序被控制,攻击者就已经进入这份数据的信任边界,不能靠编码再次隔离它。 Secret 适合小规模值,单对象有一 MiB 限制。应只挂载需要的键,并只交付给真正需要它的容器。文件模式可以缩小工作负载内部的读取范围,却不能替代 API 授权或对创建工作负载权限的控制。环境变量注入也有暴露范围和生命周期方面的取舍,应根据应用需求选择,不能默认它一定比文件更安全。 坏掉的样子 禁止直接 get Secret,不一定禁止间接取得内容。主体如果可以在命名空间创建 Pod,通常就能在自己提交的规范中指定一个 Secret 卷,由 kubelet 把数据交给该工作负载。创建相应控制器的权限也可能产生类似效果。因此,除了直接读取数据的规则,还必须一起审查创建工作负载的权限及适用的准入限制,否则边界会漏掉实际消费路径。 ...

八月 21, 2027 · 3 分钟 · XB Field Notes
以观察、判断和行动三个面板呈现构建差异的定位。

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

可复现构建的含义是:另一方依据声明的源码、构建步骤和相关环境,能够逐字节重建指定产物。这个承诺必须说明比较哪些输出;如果交付的可执行文件不同,只比较目标文件相同是不够的。复现结果为源码与分发内容之间的联系提供证据,而源码审阅、来源证明和签名分别回答其他问题。两次相同的恶意构建仍然有害,被污染的工具链也可能稳定复现自己的异常行为。 记录真正影响字节的输入 源码版本与编译器版本只是输入的一部分。依赖、目标架构、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 会话恢复复用认证状态

“省一次往返”需要结合协议版本和 early data 模式理解。TLS 1.3 的普通完整握手,以及不携带 early data 的恢复握手,通常都需要一次 TLS 往返。恢复仍然可以减少证书传输和身份认证开销,但开启票据并不意味着每个 HTTPS 请求都能提前一个 RTT 开始。 新连接仍要派生新的流量密钥 TLS 会话恢复使用先前握手派生的 PSK,为一条新连接建立安全上下文。NewSessionTicket 提供的是与恢复状态关联的不透明标识,不是让后续连接直接沿用原来的流量密钥。新的握手仍会根据自身上下文派生密钥。服务器可以用密码学方法保护票据状态,也可以在不透明标识后保存服务端状态;应用不应把票据解析成一种通用登录凭据。 服务器接受 PSK 认证时,不再发送自己的 Certificate 和 CertificateVerify 消息。恢复可以结合一次新的临时 Diffie-Hellman 交换,获得相应的前向保密;协议也允许仅使用 PSK 的模式,后者没有这项保护。因此,“拿到了票据”既不能证明实际选用了哪种密钥交换,也不能独自证明节省了多少网络等待。 可选的零往返模式允许符合条件的应用数据跟随 ClientHello 一起发送,这与普通会话恢复是两件事。early data 不具有前向保密,并可能跨连接被重放;某条连接后来完成握手,也不能消除另一条连接上已经发生或可能发生的重放。HTTP 服务需要按资源的实际副作用决定能否接受重放风险,并正确处理拒绝,包括规范要求的 425 Too Early 响应。 坏掉的样子 客户端可能根本没有启用票据缓存,可能在处理握手后票据之前就关闭连接,也可能每次请求都重建并丢弃缓存。服务器重启、密钥轮换或票据过期后,也可能不再接受原来的恢复状态。一次恢复尝试最终走完整握手,不自动等于 TLS 服务不可用;恢复比例和建连成功率应该分别观察。 ...

八月 8, 2027 · 2 分钟 · XB Field Notes
一项只放行可信跨源读者的头部约定。

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
两行标题旁边的三个相连方块,依次标注 Observe、Decide 与 Act。

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