sgcIndy 2026.6 将目光投向了服务端。本次版本为 TLS、TCP 和 HTTP 服务端组件新增了一组可选启用的安全加固选项,封堵了底层 Indy 代码从未防范过的若干众所周知的攻击类别:会失败放行(fail-open)的证书验证回调、缓慢滴流的 Slowloris 连接、无限制的请求正文与请求头,以及 HTTP 请求走私。它还首次提供了 ML-KEM-768 后量子密钥封装原语。
每一项新防护都默认沿用以往的行为,因此在你将其打开之前,现有应用不会受到任何影响。本文将逐一介绍每项防护,并附上可直接复制粘贴的 Delphi 代码片段。
加固后的 TLS 服务端
最重要的修复在于 OpenSSL 对端验证回调。在原版 Indy 中,验证回调可能会失败放行:当服务端请求客户端证书时,最终决定由用户回调驱动,并忽略 OpenSSL 自身的判定结果,因此只要回调返回成功,就会接受一个已过期、自签名或不受信任的证书。新增的 TIdSSLOptions.StrictVerify 标志会强制采用 OpenSSL 的结果,因此自定义的 OnVerifyPeer 处理程序只能进一步收紧决定,而永远无法放宽它。
另有三个标志启用了一些头文件早已声明、但库从未真正应用过的 OpenSSL 选项:DisableCompression 缓解 CRIME,DisableRenegotiation 在 OpenSSL 1.1.0h 及更高版本上阻止客户端发起的重新协商(一种 CPU 不对称的 DoS),而 ServerCipherPreference 让服务端的密码套件顺序在协商中胜出,而不是客户端的。
uses
IdHTTPServer, IdSSLOpenSSL;
var
oServer: TIdHTTPServer;
oSSL: TIdServerIOHandlerSSLOpenSSL;
begin
oServer := TIdHTTPServer.Create(nil);
oSSL := TIdServerIOHandlerSSLOpenSSL.Create(oServer);
oSSL.SSLOptions.CertFile := 'server.pem';
oSSL.SSLOptions.KeyFile := 'server.key';
oSSL.SSLOptions.SSLVersions := [sslvTLSv1_2, sslvTLSv1_3];
// opt-in hardening, every flag defaults to False
oSSL.SSLOptions.StrictVerify := True; // enforce the OpenSSL verdict
oSSL.SSLOptions.DisableCompression := True; // CRIME
oSSL.SSLOptions.DisableRenegotiation := True; // renegotiation DoS
oSSL.SSLOptions.ServerCipherPreference := True; // server picks the cipher
oServer.IOHandler := oSSL;
oServer.Active := True;
end;
当你希望实现真正会拒绝未知客户端证书的双向 TLS 时,请将 VerifyMode 设置为 [sslvrfPeer, sslvrfFailIfNoPeerCert],并与 StrictVerify 一起使用。
阻止 Slowloris
Slowloris 客户端会打开大量连接,并一次只挤出一个字节,永远不完成请求,以此占满每一个工作线程。最直接的防御看起来像是读取超时,但有一个值得了解的细节:Indy 的 ReadTimeout 是一种非活动超时。每到达一个字节都会将其重置,因此每隔几秒发送一个字节的客户端就能让连接永远保持存活。
2026.6 通过 SetReadDeadline 为 TIdIOHandler 新增了一个真正的总读取截止时间。它会在每次读取时检查,因此即便字节仍在持续滴入也会触发。HTTP 服务端通过新的 RequestReadTimeout 属性自动接入这一机制,该属性限定了接收请求行和请求头的时间,并在每个 keep-alive 请求时重置。
uses
IdHTTPServer;
var
oServer: TIdHTTPServer;
begin
oServer := TIdHTTPServer.Create(nil);
oServer.DefaultPort := 8080;
// close any client that has not finished sending the request
// line and headers within 5 seconds, even if it keeps dripping bytes
oServer.RequestReadTimeout := 5000;
oServer.Active := True;
end;
对于自定义的 TIdTCPServer,你可以在读取循环周围手动应用相同的截止时间。传入 0 即可清除它。
procedure TForm1.ServerExecute(AContext: TIdContext);
var
vLine: string;
begin
// bound the whole request to 5 seconds of total read time
AContext.Connection.IOHandler.SetReadDeadline(5000);
try
vLine := AContext.Connection.IOHandler.ReadLn;
// ... handle the request ...
finally
AContext.Connection.IOHandler.SetReadDeadline(0);
end;
end;
HTTP 服务端的请求限制
一个会读取客户端声明的任意大小的 HTTP 服务端,是一个轻而易举的内存耗尽攻击目标。单个请求可以声明 Content-Length: 2000000000,服务端就会尝试缓冲两个吉字节,或者持续传输无尽的分块正文,或者发送数百万字节的请求头。2026.6 为 TIdCustomHTTPServer 新增了三项上限和一项走私检查。
MaxRequestBodySize 限制 Content-Length 以及累加的分块正文,超出时回复 413 Payload Too Large。MaxHeaderTotalSize 限制请求头字节总量,超出时回复 431 Request Header Fields Too Large。StrictRequestParsing 会拒绝那些故意制造歧义的请求,例如同时携带 Content-Length 和 Transfer-Encoding: chunked 的消息(经典的请求走私手段)或负数的分块大小,并回复 400 Bad Request。分块尾部头(trailer-header)循环现在也设有上限,因此攻击者无法再用无穷无尽的尾部行将连接保持打开。
oServer.MaxRequestBodySize := 10 * 1024 * 1024; // 10 MB, else 413
oServer.MaxHeaderTotalSize := 64 * 1024; // 64 KB, else 431
oServer.StrictRequestParsing := True; // reject CL+TE smuggling, else 400
在原始 TCP 层面,TIdIOHandler.MaxInputBufferSize 为构建在该 IOHandler 之上的任何协议限定累积输入缓冲区,从而阻止以长度为前缀的读取或超大的行让缓冲区无限制增长。
// inside OnConnect / OnExecute of any Indy server
AContext.Connection.IOHandler.MaxInputBufferSize := 1024 * 1024; // 1 MB cap
设计上即为可选启用
这些选项都不会改变默认行为。所有字段都默认关闭(大小和超时限制为 0,布尔标志为 False),因此升级到 2026.6 的项目其行为与在 2026.5 上完全一致。你只需精确启用部署所需的那些防护即可,而同一份代码可在 Delphi 7 到 RAD Studio 13 以及 Free Pascal 上编译。
本次版本的其他内容
2026.6 首次引入了 ML-KEM-768 后量子封装与解封装原语,可在 OpenSSL 3.5 及更高版本上使用。它们提供了一个简单的 TBytes API,让你可以在经典 ECDH 交换之外,将后量子密钥封装步骤叠加进混合握手中。
在构建方面,当 RAD Studio 安装在包含空格的路径中时,C++Builder 包编译不再因 MSBuild 错误 MSB1008 而失败。DCC_BpiOutput 参数现在已加上引号。
升级
2026.6 是一次直接替换式升级。没有破坏性变更,也无需迁移任何内容,因为每一项新防护都是可选启用的。请查看上面的代码片段,并启用适合你服务端的选项。
sgcIndy 是免费的。请从 esegece.com/products/sgcindy/download 下载最新版本。
有疑问、反馈或需要帮助加固你的服务端?联系我们,你会收到编写这些代码的人的回复。
