在Web应用中处理用户上传的文件时,最安全的做法是在文件真正写入磁盘或对象存储之前,先对其内容做病毒检测。ICAP(Internet Content Adaptation Protocol)是一种基于HTTP风格的标准协议,专门用于让代理服务器把内容交给外部服务处理,比如防病毒扫描。C#完全可以通过原生Socket或TcpClient实现ICAP客户端,在保存文件前完成拦截。

一、ICAP协议基础与扫描场景
ICAP运行在TCP之上,默认端口是1344。它定义了几种服务类型,其中REQMOD用于在请求到达源站前修改或检查内容,RESPMOD用于响应返回前处理。文件上传场景通常使用REQMOD:客户端把HTTP请求体(也就是文件字节)封装进ICAP消息,发给防病毒网关,网关用ClamAV等引擎扫描后,回传一个ICAP响应,告知是否安全。
相比在C#程序里直接启动clamscan进程,ICAP方式把扫描逻辑放在独立服务中。这样Web服务器不会因为一次大文件扫描而阻塞线程池,也方便集中升级病毒库。很多企业级防病毒设备(如Symantec、McAfee以及开源的ClamAV加c-icap)都自带ICAP接口,C#只要会拼协议就能对接。
二、构造ICAP请求的核心步骤
一个ICAP请求看起来很像HTTP,但起始行是REQMOD icap://host/service ICAP/1.0。之后是ICAP头,比如Host、Preview。如果文件很大,可以使用分块传输,先在Preview里发一小段,等服务端返回100 Continue后再发剩余部分。下面代码展示如何用TcpClient发送一个带Preview的简单ICAP扫描请求。
using System;
using System.Net.Sockets;
using System.Text;
public class IcapScanner
{
public static bool ScanFile(byte[] fileData)
{
string host = "127.0.0.1";
int port = 1344;
string service = "avscan";
using (TcpClient client = new TcpClient(host, port))
using (NetworkStream stream = client.GetStream())
{
// 构造ICAP头与Preview内容
StringBuilder sb = new StringBuilder();
sb.AppendLine("REQMOD icap://" + host + "/" + service + " ICAP/1.0");
sb.AppendLine("Host: " + host);
sb.AppendLine("Preview: " + Math.Min(fileData.Length, 512));
sb.AppendLine("Allow: 204");
sb.AppendLine();
byte[] header = Encoding.ASCII.GetBytes(sb.ToString());
// 发送ICAP头
stream.Write(header, 0, header.Length);
// 发送Preview数据(最多512字节)
int previewLen = Math.Min(fileData.Length, 512);
stream.Write(fileData, 0, previewLen);
// 若还有剩余数据,可在此继续发送,简化示例仅发Preview
// 读取响应判断是否含病毒
byte[] buffer = new byte[1024];
int read = stream.Read(buffer, 0, buffer.Length);
string response = Encoding.ASCII.GetString(buffer, 0, read);
return !response.Contains("X-Infection-Found");
}
}
}
</code>上面的代码把文件前512字节作为Preview发出,服务端若支持Allow: 204,对干净文件会直接回204 No Content,表示放行。若发现病毒,响应里会出现X-Infection-Found头并带病毒名。真实项目里应当循环读取流直到ICAP头结束,再决定是否继续传文件体。
需要注意,ICAP消息里封装的原始HTTP请求也要按格式写,比如POST /upload HTTP/1.1和对应的Content-Length。如果省掉内部HTTP头,部分c-icap模块会拒绝处理。开发时建议先用Telnet手动连1344端口观察交互,再写C#代码。
三、在ASP.NET上传管道中集成
在ASP.NET Core里,可以在自定义中间件或控制器动作中,把IFormFile的OpenReadStream读成字节数组,调用上面的扫描方法。只有返回安全时才调用File.CopyTo保存到本地或转发到OSS。这样即使攻击者传了带毒文件,也绝不会落盘。
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Threading.Tasks;
[ApiController]
[Route("api/upload")]
public class UploadController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Post(IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest("空文件");
using (var ms = new MemoryStream())
{
await file.CopyToAsync(ms);
byte[] data = ms.ToArray();
// 保存前扫描
if (!IcapScanner.ScanFile(data))
return StatusCode(422, "文件包含病毒,已拒绝保存");
// 确认安全后保存
string path = Path.Combine("uploads", file.FileName);
await System.IO.File.WriteAllBytesAsync(path, data);
}
return Ok("上传成功");
}
}
</code>这段代码把扫描放在写盘之前,符合最小暴露原则。不过把整个文件读进内存对大文件不友好,生产环境可改成流式分块发送ICAP,边读上传流边转发给ICAP服务,收到威胁立刻断开。
另一个常见做法是把ICAP扫描做成后台服务,Web层只发消息队列,由独立 worker 拉取并扫描,再回调通知。这样Web接口响应更快,但牺牲了同步拦截能力,适合非实时批量导入。
四、错误处理与性能注意
ICAP服务不可用时,C#客户端会抛出SocketException。业务上应明确策略:是拒绝上传(fail-closed)还是暂时放行并告警(fail-open)。金融类系统通常用fail-closed,普通论坛可fail-open避免影响体验。
性能方面,每个上传都新建TcpClient会有握手开销。可以用Connection: keep-alive复用ICAP连接,或者维护一个连接池。同时设置合理的SendTimeout和ReceiveTimeout,防止防病毒服务卡死拖垮站点。下表列出两种集成方式对比:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 同步ICAP拦截 | 文件绝不会落盘,实时防护 | 上传线程等待扫描,增加延迟 |
| 异步队列扫描 | 接口响应快,易横向扩展 | 毒文件可能短暂存在,需补偿删除 |
总体来看,C#调用ICAP做保存前病毒扫描,是兼顾安全与架构清晰的可行方案。只要处理好协议头和分块逻辑,就能无缝接入现有上传接口。