CRLF注入漏洞的核心问题在于攻击者利用回车符(CR,即\r,ASCII 13)和换行符(LF,即\n,ASCII 10)来篡改HTTP响应头或日志记录,而修复它最直接有效的手段就是在所有用户输入进入系统之前,对\r和\n进行严格的过滤、转义或替换。简单来说,你需要在代码层面找到所有接收用户输入的地方,把其中的\r和\n字符替换成空字符串或者安全的转义形式,同时配合输入验证和输出编码,形成多层防御。下面我会从漏洞原理、具体修复方案、代码示例、最佳实践几个维度把这件事讲透。

CRLF注入漏洞到底是什么

CRLF是Carriage Return和Line Feed的缩写,也就是回车和换行。在HTTP协议中,响应头和响应体之间用一个空行(即\r\n\r\n)来分隔。如果攻击者能在用户输入中插入\r\n字符,就可以伪造HTTP响应头,实现HTTP响应拆分、XSS攻击、日志注入等危害。比如用户在评论框输入"test%0d%0aSet-Cookie:malicious=1",服务器如果没有过滤,就会把这段内容当作新的响应头返回给浏览器,导致Cookie被篡改。

为什么单纯过滤换行符不够

很多开发者只过滤了\n(LF,0x0A),却忽略了\r(CR,0x0D)。攻击者可以单独使用\r来绕过,比如URL编码%0d。还有一些编码方式如%0d%0a、\x0d\x0a、0x0D0x0A等,都需要一并处理。更隐蔽的是UTF-8编码下的多字节表示,比如%e5%98%8a%e5%98%8d这种过长编码在某些解析器中也会被还原为\r\n。所以修复方案不能只盯着一种字符,必须全面覆盖。

修复方案一:输入过滤——最基础也最关键的一步

在所有用户输入进入业务逻辑之前,统一做一层过滤。核心思路是把\r和\n全部替换掉。以下是几种常见语言的实现方式:

// PHP 实现
function sanitizeCRLF($input) {
    $input = str_replace(array("\r", "\n"), '', $input);
    // 同时处理URL编码形式
    $input = str_replace(array('%0d', '%0a', '%0D', '%0A'), '', $input);
    return $input;
}
// Python 实现
def sanitize_crlf(input_str):
    input_str = input_str.replace('\r', '').replace('\n', '')
    input_str = input_str.replace('%0d', '').replace('%0a', '')
    input_str = input_str.replace('%0D', '').replace('%0A', '')
    return input_str
// Java 实现
public static String sanitizeCRLF(String input) {
    return input.replace("\r", "")
                .replace("\n", "")
                .replace("%0d", "")
                .replace("%0a", "")
                .replace("%0D", "")
                .replace("%0A", "");
}

修复方案二:使用正则表达式进行更全面的清洗

正则表达式可以一次性匹配多种变体,包括十六进制编码、Unicode编码等。这是比简单替换更健壮的方式:

// PHP 正则方式
function sanitizeCRLFRegex($input) {
    // 匹配 \r, \n, \r\n, %0d, %0a, %0D, %0A 以及各种编码变体
    $input = preg_replace('/[\r\n]+/', '', $input);
    $input = preg_replace('/%0[dD]/i', '', $input);
    $input = preg_replace('/%0[aA]/i', '', $input);
    return $input;
}
// JavaScript 正则方式
function sanitizeCRLF(input) {
    return input
        .replace(/[\r\n]+/g, '')
        .replace(/%0[dD]/gi, '')
        .replace(/%0[aA]/gi, '');
}

修复方案三:在HTTP响应层面加固

除了过滤输入,还需要在输出HTTP响应时确保响应头的安全性。具体做法包括:对所有放入HTTP响应头的用户数据进行URL编码或Base64编码;设置Content-Security-Policy头限制脚本执行;使用HttpOnly和Secure标志保护Cookie。如果你的框架支持,直接使用框架自带的响应头安全处理机制。

// Node.js Express 中的响应头安全处理
app.use((req, res, next) => {
    // 确保响应头中不包含用户可控的换行符
    const originalSetHeader = res.setHeader;
    res.setHeader = function(name, value) {
        if (typeof value === 'string') {
            value = value.replace(/[\r\n]/g, '');
        }
        return originalSetHeader.call(this, name, value);
    };
    next();
});

修复方案四:框架层面的统一防护中间件

如果你的项目使用了Web框架,最好的做法是写一个全局中间件,在请求进入任何业务逻辑之前统一清洗。这样可以避免每个接口单独处理遗漏的问题。比如在Spring Boot中可以写一个Filter,在Django中写一个Middleware,在Laravel中写一个全局中间件。

// Java Spring Boot Filter 示例
@Component
public class CRLFFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        // 清洗所有参数
        Map<String, String[]> sanitizedParams = new HashMap<>();
        for (Map.Entry<String, String[]> entry : httpRequest.getParameterMap().entrySet()) {
            String[] values = entry.getValue();
            String[] sanitized = new String[values.length];
            for (int i = 0; i < values.length; i++) {
                sanitized[i] = values[i]
                    .replace("\r", "").replace("\n", "")
                    .replace("%0d", "").replace("%0a", "")
                    .replace("%0D", "").replace("%0A", "");
            }
            sanitizedParams.put(entry.getKey(), sanitized);
        }
        chain.doFilter(new SanitizedRequestWrapper(httpRequest, sanitizedParams), response);
    }
}

修复方案五:日志记录层面的防护

CRLF注入不仅影响HTTP响应,还会污染日志文件。攻击者可以通过注入\r\n来伪造日志条目,干扰安全审计。修复方法是在写入日志之前,对所有用户输入进行同样的CRLF过滤:

// Python 日志安全写入示例
import logging

def safe_log(logger, message):
    clean_message = message.replace('\r', '').replace('\n', '')
    clean_message = clean_message.replace('%0d', '').replace('%0a', '')
    logger.info(clean_message)

修复方案六:WAF和安全网关的辅助防护

在应用层修复的基础上,部署WAF(Web应用防火墙)可以作为第二道防线。大多数商业WAF都内置了CRLF注入的检测规则,能够在请求到达应用之前拦截包含\r\n的恶意参数。但需要注意,WAF只是辅助手段,不能替代代码层面的修复,因为WAF规则可能被绕过,而代码层面的过滤是根本性的。

需要特别注意的边界情况

在实际修复过程中,有几个容易被忽略的点:第一,某些业务场景确实需要用户输入换行,比如富文本编辑器。这种情况下不能简单删除\r\n,而是应该用HTML实体编码(如&#10;)或转义后存储,输出时再解码显示。第二,文件上传功能中,文件名也可能包含\r\n,需要同样处理。第三,API接口返回JSON数据时,如果用户输入被原样返回,也存在CRLF注入风险,必须在序列化之前清洗。

测试验证修复是否生效

修复完成后,必须进行安全测试来验证。可以使用以下测试用例:在输入框中输入%0d%0aSet-Cookie:test=1、%0d%0aContent-Length:0、\r\n\r\n<script>alert(1)</script>等payload,观察服务器响应是否还包含被注入的内容。同时检查日志文件,确认没有被伪造的条目。建议使用Burp Suite、OWASP ZAP等工具进行自动化扫描。

从架构层面建立长期防御机制

CRLF注入的修复不应该是一次性的补丁,而应该纳入安全开发生命周期(SDL)。具体包括:在代码审查阶段增加输入验证检查项;在CI/CD流水线中集成静态代码分析工具(如SonarQube、Semgrep)自动检测未过滤的用户输入;定期进行渗透测试;建立安全编码规范文档,明确所有涉及用户输入的地方都必须做CRLF过滤。只有把防护变成制度化、流程化的操作,才能从根本上杜绝此类漏洞反复出现。

总结:多层防御才是正道

CRLF注入漏洞的修复核心就是过滤\r和\n字符,但真正有效的防护需要多层叠加:输入层过滤、输出层编码、框架中间件统一处理、日志层清洗、WAF辅助拦截,再加上完善的测试和制度化的安全流程。不要指望单一手段解决所有问题,也不要觉得过滤了\n就万事大吉。把每一层都做扎实,才能让攻击者无机可乘。这篇文章给出的代码示例和方案可以直接应用到实际项目中,根据你使用的技术栈选择对应的实现方式即可。