Content-Length 说正文有多少字节,必须与实际一字不差。
写小了会被当成报文截断,写大了会让接收方一直等,写错位就是请求走私的入口。
前面几个字段都是「描述」,这个字段是「边界」。 整个 HTTP 报文能不能被正确切开,全靠它和分块传输两个机制。
一、四种写法,三种出错
Demo 里正文固定 50 字节,声明值分别写成 50、0、20、120,客户端老老实实按声明值读: | 声明 | 实发 | 客户端收到 | 结果 | |---:|---:|---:|---| | 50 | 50 | 50 | 正常 | | 0 | 50 | 0 | 正文被当成 0 字节 | | 20 | 50 | 20 | 只收前 20 字节 | | 120 | 50 | 50 | 等不全,超时收场 | 只有第一行是对的。第二行声明 0 却发了 50 字节——客户端一个字节都不读,那 50 字节就留在连接里了。 在持久连接上这比「丢数据」更糟:剩下的 50 字节会被当成下一个响应的开头, 于是整条连接上后续所有报文全部错位。这就是请求走私最土的一种做法。
二、和分块传输的关系
HTTP/1.1 里有两个机制能定出正文边界:Content-Length 给绝对长度,
Transfer-Encoding: chunked 给一组带长度的块。
两个同时出现时,规范要求接收方以 Transfer-Encoding 为准,并忽略 Content-Length。
D064 实测过:故意把 Content-Length 写成两倍,接收方照旧按块读完 840 字节。
这条规定本身就是安全防线。
历史上很多走私漏洞,就出在「前端认 CL,后端认 TE,
两边对同一个报文的边界理解不同」——中间夹进去的那段字节,就成了攻击者的通道。
三、哪些情况不能有它
用了分块传输就不能发;204 和 304 这类明确没有正文的响应也不该有;
HEAD 请求的响应可以带,但带的是「正文本该有多长」,实际不发正文。
HEAD 这个例外值得单独记:客户端拿到 Content-Length: 12345 但一个字节都没有,这是正常的,
因为 HEAD 约定的就是「只要首部」。
还有一条容易忽略:Content-Length 数的是编码之后的字节数。
gzip 压过的正文,这里写的是压缩后的长度,不是原文长度。
四、动手 Demo:正文 50 字节,声明值写四种
服务端正文固定 50 字节,只改 Content-Length 的声明值;
客户端严格按声明值读——不按连接关闭判断,只按那个数字判断。这样才能看出「声明错了」到底会怎样。
def read_by_cl(port, raw, declared, timeout=1.2):
sk = socket.create_connection(('127.0.0.1', port), timeout=timeout)
sk.sendall(raw)
buf = b''
while b'\r\n\r\n' not in buf:
buf += sk.recv(4096)
head, rest = buf.split(b'\r\n\r\n', 1)
body = rest
while len(body) < declared: # 只认声明值,不认连接关闭
try:
body += sk.recv(4096)
except socket.timeout:
break # 等不全就超时收场
return head, body[:declared]
实跑输出:
--------------------------------------------------------------------
Content-Length:说多少就得是多少
--------------------------------------------------------------------
正文固定 50 字节,声明值分别写成 50、0、20、120:
声明 实发 客户端收到 结果
50 50 50 正常
0 50 0 正文被当成 0 字节,剩下的没人要
20 50 20 只收前 20 字节,剩下的是垃圾
120 50 50 等不全,超时收场
四行里只有第一行是对的。第二行声明 0 却发了 50 字节正文,
在持久连接上后果更糟:那 50 字节会被当成下一个响应的开头,
整条连接上的后续报文全部错位。这就是请求走私(request smuggling)的土办法。
第三行声明 20 但发了 50,客户端收 20 就停,剩下的 30 字节污染连接;
第四行声明 120 实际只有 50,客户端会一直等,等到超时或者连接被关。
所以有一条硬规矩:用了分块传输(Transfer-Encoding: chunked)就不能再发
Content-Length,两个同时出现时接收方该以 TE 为准——D064 已经实测过这一点。
四行结果:50 收 50、0 收 0、20 收 20、120 收 50 后超时。数字完全由声明值决定,与实发多少无关。 第二行最危险:声明 0,客户端压根不读那 50 字节,它们留在接收缓冲区里,下一次读会先读到这些垃圾。 第四行则是另一种体验:客户端一直等,等到 1.2 秒超时才放弃。 真实场景里如果服务端不主动关连接,这可能挂很久。 所以「多发」和「少发」都是错的,而且错法不同:少发是数据被丢,多发是连接被污染。
30 秒实验
- 把超时从 1.2 秒改到 0.2 秒,看第四行还收不收得到那 50 字节。
- 加上
Transfer-Encoding: chunked同时保留Content-Length,看客户端认哪个。 - 想一想:为什么
HEAD的响应可以有Content-Length却没有正文?
常见误区
- 「Content-Length 是可选的,靠连接关闭也能判断边界」——HTTP/1.0 才那样,持久连接下必须给。
- 「多说一点没关系,客户端会自己停」——会污染连接,是走私的入口。
- 「它可以和 Transfer-Encoding 同时用」——同时出现时 CL 必须被忽略。
自测三问(点开对答案)
1. 声明 20、实发 50,客户端收几个字节? —— 20 个,多出来的 30 个留在连接里,会污染后续报文。 2. `Content-Length` 和 `Transfer-Encoding` 同时出现,认哪个? —— 认 Transfer-Encoding,Content-Length 必须被忽略。 3. gzip 压缩过的正文,这个字段写哪个长度? —— 压缩后的长度,因为数的是实际发出去的字节。边界定好了,接下来是「这份内容和别的地址是什么关系」——Content-Location。
- ☐ (本节完,下一节 6.6.5 Content-Location)