上一章“Web 应用安全与渗透测试”先把边界说清楚了:渗透测试不是“找到一个站就试试看”,而是在书面授权、明确范围和可控影响下完成的安全验证。这一章我们先不追漏洞,也不急着背工具选项。我们要做一件看起来很基础、实际上会贯穿整门课的事:把浏览器里一次普通操作,沿着真实处理链拆开。
你点下“查看订单”,眼前只是页面切换了一下。背后却可能发生了 DNS 查询、TLS 握手、HTTP 请求、边缘代理转发、应用路由匹配、会话查询、权限判断、数据库读取、模板或 JSON 序列化,最后浏览器再解析和绘制结果。后续测试中的大多数线索,都藏在这条链的某个接缝里。
为什么一定要先把正常链路弄懂?因为浏览器显示 403,不代表一定是应用拒绝了请求;脚本读不到跨源响应,不代表请求没有到服务器;把参数从查询字符串搬到 JSON 正文,也不会让它突然变可信。看到现象就套漏洞名称,很容易把缓存、代理、浏览器策略或业务规则误判成安全问题。
本章所有观察只在本地靶场、隔离教学环境,或书面授权明确覆盖的系统中进行。示例里的域名和账号都是教学占位符。即使动作只是重放一个读取请求,也要服从授权中的目标、账号、时间窗口、速率和停止条件。
这一章的目标不是让你背下整个 Web 技术栈,而是建立一张可靠的“请求地图”。拿到一条请求时,你应该能说明它从哪里发出、经过哪些边界、在哪里被解析、谁做了身份与权限判断、数据去了哪里,以及每层能留下什么证据。
假设你在测试商城里点开“我的订单”。浏览器地址栏看起来只是从账户首页切换到订单页,但浏览器并不是把一整张“页面”从服务器搬回来。它先处理导航,再取得主文档,然后根据主文档继续请求样式、脚本、图片和接口数据。一个看似简单的页面,通常对应一组请求,而不是一个请求。
我们按发生顺序走一遍。
浏览器先解析地址,分出协议、主机、端口、路径、查询和片段。片段主要留在客户端使用,常规请求不会把它发给服务器。浏览器还会考虑缓存、代理配置、已有连接和安全策略。
客户端需要把主机名对应到可连接的位置,并与目标建立传输连接。使用 HTTPS 时,还要完成 TLS 协商并验证服务器身份。只有安全连接成功建立,HTTP 消息才有机会在这条连接上交换。
浏览器发出主文档请求。请求可能先到 CDN、负载均衡器、Web 应用防火墙或反向代理,再被转发到某个应用实例。每一层都可能终止连接、增加字段、重写目标、命中缓存或直接返回响应。
应用框架解析路径、查询、请求头、Cookie 和正文,选择路由与中间件。认证组件识别当前账号,授权组件判断该账号能否对目标对象执行当前动作。业务代码随后读取缓存、数据库或内部服务。
响应沿着链路返回。代理可能压缩内容、增加缓存字段或统一错误页。浏览器收到 HTML 后构造 DOM,加载 CSS,执行允许执行的 JavaScript,再经过样式计算、布局与绘制,把结果放到屏幕上。

这里最值得建立的习惯,是把“页面”和“响应”分开。页面是浏览器综合许多响应、缓存内容和本地运行结果后呈现的状态;响应是某个请求对应的协议消息。页面上出现一张用户卡片,不等于用户数据一定写在 HTML 里,它可能来自随后发起的 Fetch 请求,也可能已经在脚本状态中,甚至来自浏览器旧缓存。
同理,响应也不一定来自业务代码。边缘缓存能直接返回旧内容,反向代理能生成 502,WAF 能生成拦截页,应用框架能在业务函数运行前返回格式错误。测试记录只写“服务器返回了某状态码”还不够,最好进一步回答“哪一层最可能生成了它,有什么证据”。
浏览器开发者工具给你客户端视角:页面发起了哪些请求、谁触发了它们、浏览器是否命中缓存、脚本是否因同源策略无法读取结果。测试代理给你线上视角:经由代理发送和接收了哪些 HTTP 消息。服务端日志给你处理视角:入口接收了什么、路由如何匹配、身份和权限判断结果怎样、下游调用是否成功。
这三份记录并不天然一致。浏览器可能从缓存恢复资源,代理因而看不到新请求;代理会解码或重新编码消息,展示形式不一定是线上原始字节;应用日志通常只记录解析后的字段,而不是完整原始请求。真正可复核的结论,需要明确证据来自哪个视角。
测试代理安装的根证书,会让代理具备解密经它转发的 HTTPS 流量的能力。只能在自己控制、项目批准的测试浏览器或隔离设备中安装。测试结束后移除证书与代理配置,也不要把含有真实口令、完整令牌或个人数据的代理项目文件随意上传和分享。
第一次看 URL,很多人会把问号前面叫“地址”,问号后面全叫“参数”。这在日常交流里勉强够用,做技术分析时就太粗了。不同组件承担不同职责,也会进入不同的解析器。
看这个教学地址:
https://shop.example.test:8443/account/orders?state=open&page=2#recent
\____/ \___________________/\_____________/\_________________/\____/
协议 主机与端口 路径 查询 片段协议决定客户端采用哪类通信规则。主机用于定位目标,并常常参与虚拟主机或租户路由。端口决定连接入口;没有显式端口时,客户端按协议使用默认端口。路径通常参与代理和应用的路由匹配。查询由应用自行解释,常见用途是筛选、分页和搜索。片段用于客户端在文档内部定位或保存前端状态,普通 HTTP 请求不会携带它。

因此,记录一个可控值时要说清位置。orderId 可以出现在路径段、查询、表单正文、JSON 字段、请求头或 Cookie 中。即使名字完全相同,这些位置也可能由不同组件解析,接受不同的数据类型,并受到不同的校验规则约束。
浏览器和服务器面对的不是一张截图,而是一串要被解析的字符。默认端口可以被省略,主机名不区分 ASCII 大小写,国际化域名需要转换,点号段可能被规范化,某些字符在不同 URL 组件中有不同语法意义。两个地址看起来很像,不代表解析结果相同;两串文字不同,也可能被规范化到同一个目标。
安全判断不要依赖简单的“以某字符串开头”或“包含某域名”。处理跳转地址、回调地址、跨源许可或服务端取址功能时,应使用统一的 URL 解析器取得协议、主机、端口、路径等结构化结果,再按明确规则比较。
证据也应保留两个层次:原始输入说明测试者实际提供了什么,解析结果说明组件把它理解成什么。若入口代理继续改写目标,还要单独记录转发后的值。把三者都写成“请求 URL”,后面很难定位差异产生在哪一层。
百分号编码用 % 和两位十六进制数字表示一个字节。它解决的是字符如何安全进入 URL 语法的问题,不是加密,也不是访问控制。看到 %2F、%E8 之类的形式,任何接收方都可以按规则还原相应字节。
更容易踩坑的是:不同组件使用不同编码集合。路径、查询、片段和 application/x-www-form-urlencoded 表单正文不是完全相同的语境。表单编码经常把空格序列化成 +,真正的加号则需要另行编码。代理先解码一次、框架又解码一次,或一层按 UTF-8 解释而另一层使用不同字符集,都可能让路由、校验、业务代码与日志看到不同内容。
在授权靶场里观察解析差异时,一次只改变一个无害字符,记录原始表示、代理转发值、框架解析值和最终路由。响应发生变化只能说明“处理路径可能不同”,不能直接写成“已绕过”。只有安全决策确实因解析不一致而失效,并且影响得到验证,才形成漏洞结论。
查询字符串常出现在浏览器历史、书签、访问日志、分析系统和复制出来的链接中,页面跳转时还可能影响来源信息。口令、长期令牌和敏感个人数据不适合放在查询里。把它们移到 POST 正文可以减少一部分暴露面,却不会自动带来可信性:正文依然由客户端提供,服务端照样要校验。
HTTP 的核心模型很朴素:客户端发送请求,服务器返回响应。请求用方法和目标表达意图,用字段补充元数据,也可以带内容;响应用状态码说明处理结果类别,用字段描述内容与策略,再附上可选的响应内容。
为了方便阅读,下面用 HTTP/1.1 的文本形态展示消息:
POST /api/profile HTTP/1.1
Host: app.example.test
Content-Type: application/json
Accept: application/json
Cookie: __Host-session=已遮盖的会话标识
X-Request-ID: req-lab-2048
{"displayName":"测试学员"}第一行有方法、请求目标和协议版本。接下来是请求字段,空行之后才是消息内容。Content-Type 描述发送内容的媒体类型,Accept 表达客户端希望收到的表示类型。两者经常被初学者混淆。前者像是在说“我递过去的是 JSON”,后者像是在说“如果可以,请给我 JSON”。
HTTP/2 和 HTTP/3 的线上编码方式不同,开发者工具中还会看到 :method、:scheme、:authority、:path 等伪首部。不要把 HTTP/1.1 的文本请求行误认为所有版本在线上都以同样的纯文本出现。对应用层分析来说,方法、目标、字段、内容和响应状态这些语义仍然成立。

HTTP 里的“安全方法”,说的是客户端没有请求改变服务端状态的标准语义,不是在说这个方法天然具备网络安全。HTTP 里的“幂等”,说的是同一意图重复执行后,服务端预期最终状态与执行一次相同,也不保证每次响应字节、时间戳和日志完全一样。
更重要的一点是,方法从来不负责身份和权限。把路由从 POST 改成 GET,不会自动变安全;某个 DELETE 接口返回 204,也不证明请求者有权删除那个对象。认证、对象级授权和业务校验必须在服务端按具体操作执行。
来看一个响应:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
Set-Cookie: __Host-session=新的已遮盖标识; Path=/; Secure; HttpOnly; SameSite=Lax
X-Request-ID: req-lab-2048
{"result":"资料已更新"}2xx、3xx、4xx、5xx 提供的是结果类别。具体含义仍要结合请求方法、字段、响应内容和业务状态理解。200 可以包着业务错误对象;404 可以是应用为了避免暴露对象存在性而统一返回;403 可能来自边缘防护;500 只表示某层处理失败,不能单凭它证明存在注入或代码执行。
测试报告若只写“修改参数后返回 500,所以存在漏洞”,证据链几乎是空的。更稳妥的记录是:基线请求是什么、只改变了什么、响应的结构和时间如何变化、哪个组件生成响应、服务端是否出现对应异常、是否造成了未经授权的数据访问或状态改变。
Host 或 :authority 常参与虚拟主机与路由;Cookie 和 Authorization 常携带身份材料;Origin、Referer 与 Fetch Metadata 能帮助判断请求上下文;Content-Type 决定正文走哪个解析器。它们很有用,但“出现在请求头里”并不等于可信。
客户端可以自行构造大多数普通请求字段。经过代理后,还可能出现 Forwarded、X-Forwarded-For、X-Forwarded-Proto 或 X-Forwarded-Host。后端若要依赖这些信息做安全决策,入口必须先定义可信代理链:清除外部伪造值,再写入自己确认的值,后端只接受来自受控入口的数据。
这也是为什么“改了一个头,页面变了”仍只是线索。变化可能来自缓存键、路由选择、调试功能或错误处理。要确认问题,必须找到具体安全决策,并证明它信任了攻击者可控的信息。
HTTPS 可以理解成在 TLS 保护的通道中交换 HTTP。TLS 主要解决传输中的机密性、完整性和服务器身份验证,让路径上的旁观者更难读取或篡改消息。它不会自动修复越权、业务逻辑错误、错误的会话生命周期,也不会让进入应用的数据天然可信。
很多系统并不是浏览器直接连到业务进程。常见链路是“浏览器—CDN 或负载均衡器—反向代理—应用实例”。TLS 可能在边缘终止,边缘再与源站建立另一条加密连接,也可能在受控内网中按架构要求转发。于是“这个站用了 HTTPS”至少还要追问:哪一段用了、证书验证发生在哪里、边缘到源站是否受保护、应用如何得知外部原始协议。
地址栏主机名、TLS 验证的证书身份、HTTP 路由所用的主机信息,在普通访问中通常一致,但它们是不同层的对象。多租户、反向代理和服务网格会让这种区别更明显。连接失败时也要按层判断:DNS 解析失败、TCP 或 QUIC 连接失败、TLS 证书错误、HTTP 状态码和应用业务错误不是一类现象。

反向代理可以重写路径、补充转发字段、限制正文大小、解压内容、合并重复字段、选择上游,也能直接从缓存返回结果。应用框架随后还会做 URL 解码、字符集转换、Cookie 解析、表单或 JSON 反序列化。测试代理看到的请求,不必然等于应用对象中的值。
想把差异找出来,可以给每层列一份小账本:
如果你无法接触服务端日志,就直接说明结论只来自客户端或代理观察,不要把推测写成确定事实。证据边界写清楚并不会削弱报告,反而让复测者知道还需要在哪一层补证。
浏览器端的 required、长度限制和下拉选项能改善体验,却不是安全边界。用户可以关闭脚本,也可以用测试代理或普通 HTTP 客户端直接构造请求。因此,格式、长度、对象归属、当前状态和操作权限都要在服务端重新判断。
一条典型的服务端处理链可以拆成下面几步:
框架先根据方法、主机和路径选择路由,并按声明的内容类型解析查询、表单或 JSON。解析失败应该得到一致、可控的错误,而不是继续带着半成品对象进入业务逻辑。
认证中间件从会话或其他凭据识别当前主体。认证回答“这是谁”,但此时还没有回答“他能不能读取这张订单”。
授权逻辑把当前主体、目标对象和动作放在一起判断。对象标识来自客户端时,服务端仍要检查对象是否属于当前租户或账号,不能因为页面只展示了自己的对象就省略校验。
业务层检查状态机和不变量,例如已退款订单不能再次退款、价格由服务端商品记录决定、普通成员不能提交管理员角色。客户端字段是意图,不是事实来源。
应用常同时使用关系数据库、文档数据库、缓存、搜索引擎和消息队列。同一个订单页可能先查缓存,未命中再查数据库,然后调用库存服务。出现陈旧数据、权限差异或响应时延时,不能默认原因就在数据库。
数据库驱动通常提供参数化查询,让 SQL 结构与数据值分开传递。它解决的是数据不应被当成查询语法解释的问题,但不会替你做对象级授权,也不会判断“折扣 99%”是否符合业务规则。输入校验、授权、参数化访问和输出编码分别位于不同边界,不能拿其中一个替代其他几个。
还有一个常见误会:使用 ORM 就等于不可能出现注入。ORM 的普通查询接口通常会参数化值,但动态拼接排序字段、原生查询、过滤表达式或不安全的字符串模板,仍可能重新把数据混进语法。测试时不要只问“用了什么框架”,要沿数据流确认可控值最终进入了哪个接口。
假设客户端传来 JSON:
{
"page": "2",
"includeArchived": false,
"filter": null
}网关可能检查正文大小,框架把字节解析成对象,校验器把字符串 "2" 转成数字,业务层区分 null 与字段缺失,数据访问层再把条件映射为查询。若每层对空值、重复字段、数字范围或 Unicode 的理解不同,最后执行的逻辑就可能偏离入口校验时的逻辑。
面对这类问题,先问四句话通常比先猜漏洞类型更有效:谁第一次解析它?谁做了规范化?谁基于它做安全决策?谁把结果写进响应或存储?答案构成了后续最小验证的路线图。

HTTP 请求之间默认没有“记忆”。应用要连续识别同一位用户,通常会建立会话。最常见的实现是:登录成功后,服务端创建会话状态和一个难以预测的标识,通过 Set-Cookie 交给浏览器;浏览器在满足作用域条件的后续请求中自动附带该 Cookie;服务端拿标识查找当前账号、权限和会话状态。

Cookie 是浏览器保存并按规则发送的一小段数据,Session 是应用对一段交互状态的组织方式。两者经常一起出现,但不能画等号。Cookie 可以只保存语言偏好;会话标识也可能通过其他机制传递。反过来,即使 Cookie 里存放签名后的状态,服务端仍要验证签名、有效期、账号状态和当前操作权限。
Cookie 位于客户端,用户能够查看和修改它。把 role=admin 放在 Cookie 里,不会因为它由浏览器自动发送就变可信。若确实保存客户端状态,需要防篡改;若只保存随机会话标识,真正的身份与权限应由服务端记录决定。
名字以 __Host- 开头的 Cookie,把一组约束交给支持的浏览器检查:它需要 Secure、Path=/,并且不能设置 Domain。这适合只属于当前主机的敏感 Cookie。它能缩小配置错误空间,但应用仍要检查完整 Cookie 名称和服务端会话状态。
会话是一段生命周期:创建、认证、轮换、持续使用、权限变化、超时、退出和撤销。只检查登录响应里的 Cookie 属性,远远不够。
认证前后应更换会话标识,避免攻击者预先固定一个标识后等待用户登录。权限提升、修改关键认证信息等敏感变化,也适合轮换标识。会话应同时考虑空闲超时和绝对有效期。退出登录时,浏览器删除 Cookie 只是客户端动作,服务端也必须让旧会话失效。
测试时用自己控制的两个专用账号就足够建立对照。记录登录前后标识是否变化、权限提升后是否轮换、超时后服务端是否拒绝旧标识、退出后旧标识是否失效。会话值必须遮盖,只保留能关联前后变化的最小片段或安全摘要。
浏览器里同时运行着许多来源的文档和脚本。如果任何页面都能直接读取用户已经登录的邮箱、后台或内网接口,打开一个陌生页面就可能泄露大量数据。同源策略因此限制一个来源的脚本如何读取或操控另一个来源的资源。
比较来源时,看协议、主机和端口。以 https://app.example.test 为基准:

“同站”是 Cookie SameSite 等机制使用的另一套边界,通常围绕方案和可注册域理解,粒度比同源更宽。两个子域可能不同源,却仍然同站。把这两个词混用,会同时误判脚本读取权限、Cookie 发送条件和 CSRF 风险。
同源策略不是一堵“任何跨域请求都过不去”的墙。浏览器允许许多跨源行为,例如导航、表单提交,以及按规则嵌入图片、样式或脚本。Fetch 或 XMLHttpRequest 发起的跨源请求,则受到 CORS 等机制约束。
分析一个跨源现象时,至少拆成三个问题:
脚本读不到响应,不代表请求没发送;浏览器携带了 Cookie,不代表发起页面能读到 Cookie;控制台显示 CORS 错误,也不代表服务端没有执行状态改变。服务端必须独立完成认证、授权和用户意图校验。
CORS 通过 HTTP 响应字段声明:哪些外部来源的页面脚本可以读取响应。对于不满足简单条件的跨源请求,浏览器通常先发送预检,说明计划使用的方法与字段。服务器允许后,浏览器才继续发真正的业务请求。
预检成功只表示跨源协议条件满足,不表示业务请求已获授权。真正请求仍要检查当前身份、目标对象和操作权限。反过来,CORS 配置再严格,也挡不住不受浏览器同源策略约束的普通 HTTP 客户端,它从来不是 API 的访问控制。
需要携带凭据的跨源场景尤其要谨慎。允许来源应按精确清单匹配,不能把客户端提交的 Origin 不加判断地原样放回允许字段。浏览器端的凭据模式、Cookie 的 SameSite 属性、服务器允许凭据的响应字段和第三方 Cookie 策略会共同影响结果,不能只盯着一行 CORS 配置。
看到“跨域报错”时先打开网络面板:请求是否发出、预检是否发生、真正请求是否发生、服务端返回了什么。控制台描述的是浏览器端读取结果,不是整个服务端处理过程。
客户端看到的现象有限,代理只看到经过自己的流量,单个服务的日志又只知道本服务发生了什么。要把请求从边缘一路对到业务和数据库,通常需要一个贯穿链路的关联标识。
入口可以生成请求 ID,并把它传递给应用和下游服务。应用日志围绕一次安全相关事件记录时间、路由、主体、目标对象、动作、结果和原因;下游调用继续携带关联信息。这样,测试者才能区分“代理拒绝”“应用权限拒绝”“数据库超时”和“业务返回空结果”。
日志不是越多越好。口令、完整 Cookie、访问令牌、支付数据和大段个人信息不应进入普通日志。会话关联可以记录带盐摘要或另一个无敏感性的内部标识。请求与响应正文只有在确有调试或审计需要、完成脱敏并控制访问与保存周期时才考虑记录。
日志还要考虑输入本身不可信。换行符、控制字符和过长字段如果未经处理直接写入文本日志,可能破坏结构或制造伪造条目。日志系统应使用结构化字段、长度限制和安全编码,并限制查询、导出和删除权限。
对测试者来说,日志的价值不是“证明系统记录了我”,而是缩小结论的不确定性。比如代理返回 403,入口日志显示请求被边缘规则拦截,应用侧没有同一请求 ID,那么就不该把这次响应描述成业务权限控制有效。相反,如果应用日志记录了主体、对象和明确的授权拒绝,结论就更扎实。
代理历史、截图、日志导出和测试笔记本身都可能成为敏感资产。项目开始前要约定保存位置、可访问人员、加密方式和销毁时间;交付物只保留证明结论所需的最小片段。
现在把前面的机制落到一次练习里。请选择本地靶场或书面授权的测试环境,使用专用账号打开一个只读取自己资料的页面。目标不是制造异常,而是把浏览器、代理和服务端证据尽量对齐。
先写范围卡片:测试目标、专用账号、允许时间、频率限制、紧急联系人和停止条件。为这次观察分配记录编号,例如 CH02-OBS-01。
打开浏览器网络面板,重新加载页面。区分主文档、样式、脚本、图片和 Fetch/XHR 请求,记录每个请求的发起者、方法、目标、状态、内容类型、耗时与缓存情况。
选择一个主业务请求,拆解协议、主机、端口、路径、查询、字段和正文。标出哪些值由浏览器自动附带,哪些由前端代码写入,哪些可能由代理增加。
保存基线后,只改变一个无副作用的值,例如把自己列表的页码从 1 改成 。不要改变账号、对象归属或业务动作。比较响应状态、内容结构、缓存字段、耗时与请求 ID。
一份简短但可复核的记录,可以是这样:
这个练习可能没有任何“刺激”的结果,却非常接近真实测试的日常。大量时间会花在确认基线、排除缓存、辨认统一错误页、校准账号状态、控制变量和整理证据上。如果正常请求都没看懂,后面任何异常都可能被夸大。
如果这六个问题能回答清楚,你已经开始从“会抓包”走向“会分析处理链”。
这一章从一次点击出发,把正常 Web 交互拆成了浏览器、URL、HTTP、TLS 与代理、应用、数据库、会话、同源策略和日志。它们不是互相独立的知识点,而是一条连续的数据与决策链:URL 描述目标,HTTP 表达意图,TLS 保护某段传输,代理选择和改写上游,应用解析输入并判断身份与权限,数据层保存或读取状态,浏览器按来源和响应策略呈现结果,日志再把各层证据关联起来。
下一章会进入渗透测试方法论。到那里,我们会把这张技术地图变成工作步骤:怎样从范围建立资产和功能地图,怎样提出假设,怎样一次只验证一个变量,怎样设置停止条件,以及怎样把现象整理成防守方可复现、可修复的结论。
先记住一个简单的落点:真正可靠的测试记录,不只说“我发了什么、页面显示什么”,还会说清“请求在哪一层被怎样理解,安全决定由谁做出,证据从哪里来”。这就是后续所有测试方法的地基。
数据访问层通过参数化接口调用数据库,或访问缓存与内部服务。查询结果回到业务层后,还要按输出上下文进行序列化或转义,最后生成 HTML、JSON 等响应表示。
2如果授权允许查看日志,用请求 ID 对齐边缘、应用和下游记录,确认哪个路由处理、会话是否命中、授权结果怎样、数据来自缓存还是数据库。无法查看时,明确写下证据边界。
删除或遮盖代理历史中的敏感值,按“观察—假设—最小验证—结果”整理记录。未验证的猜测留在工作笔记里,不写成漏洞发现。