JAVA:CRLF 注入漏洞详解与预防实践

admin
1
2025-07-30

🛡️ 1、简述

在日常开发中,Web 安全不仅仅是 SQL 注入、XSS 攻击那样“显眼”的问题,CRLF 注入(Carriage Return Line Feed Injection) 也是一种常被忽视但危险的安全漏洞。尤其在 HTTP 响应头处理中,若不加以防范,极有可能造成严重后果。

本文将深入分析 CRLF 注入的原理、在 Java 中的攻击方式,并结合实际代码示例给出预防方案。

image-dzcx.png


2、什么是 CRLF 注入?

CRLF(Carriage Return Line Feed) 是 HTTP 协议中用于标识“换行”的两个字符,分别是:

🔹 \r(Carriage Return, 回车)
🔹 \n(Line Feed, 换行)

在 HTTP 协议中,响应头与响应体之间通过一行空行 \r\n 分隔:

HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: userId=admin

<html>...</html>

📌 CRLF 注入的本质

如果攻击者在参数中注入了 \r\n 字符,并且这些内容未经过滤就拼接进 HTTP 响应头,就可能造成如下后果:

🔹 插入恶意的响应头;
🔹 伪造多个响应;
🔹 诱导浏览器缓存敏感内容;
🔹 配合 XSS 实现 Cookie 劫持。


3、攻击示例(Java 场景)

下面是一个存在 CRLF 注入风险的代码片段:

@GetMapping("/download")
public void downloadFile(@RequestParam String filename, HttpServletResponse response) {
    response.setHeader("Content-Disposition", "attachment; filename=" + filename);
}

🧨 输入模拟攻击:

访问地址:

http://localhost:8080/download?filename=test.txt%0D%0ASet-Cookie:%20token=malicious

结果响应头被注入:

Content-Disposition: attachment; filename=test.txt
Set-Cookie: token=malicious

攻击者成功伪造了一个 Cookie,造成安全隐患。

Java 中常见的易受害场景

场景 说明
response.setHeader() 响应头拼接用户输入
重定向(如 sendRedirect() URL 参数拼接不当
设置 Cookie(如 addCookie() Cookie 名称、值未校验
日志记录/文件输出 文件名、内容中包含非法换行符

4、实际解决方案

✅ 方案一:使用工具类过滤 CRLF 字符

public static String sanitizeHeaderValue(String value) {
    if (value == null) return null;
    return value.replaceAll("[\\r\\n]", "");
}

使用示例:

String safeFilename = sanitizeHeaderValue(filename);
response.setHeader("Content-Disposition", "attachment; filename=" + safeFilename);

✅ 方案二:Apache Commons Text 工具推荐

Apache 提供了 StringEscapeUtils 工具进行安全转义:

import org.apache.commons.text.StringEscapeUtils;

String encoded = StringEscapeUtils.escapeJava(filename);

注意:该方法并不会直接去除 CRLF,而是对其编码,可以避免注入问题。

✅ 方案三:限制输入类型和格式(白名单)

if (!filename.matches("^[a-zA-Z0-9_.-]+$")) {
    throw new IllegalArgumentException("文件名不合法");
}

✅ 方案四:使用标准 API 设置文件名(Spring 推荐)

String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFilename + "\"");

虽然 URLEncoder.encode() 本意是转义 URL,但也能规避 CRLF。

✅ 方案五:统一参数过滤(可选)

你可以通过一个全局 Filter,对所有参数做一次性清洗:

@WebFilter("/*")
public class CRLFSanitizerFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(request) {
            @Override
            public String getParameter(String name) {
                String value = super.getParameter(name);
                return sanitize(value);
            }

            private String sanitize(String input) {
                return input == null ? null : input.replaceAll("[\\r\\n]", "");
            }
        };

        chain.doFilter(wrappedRequest, res);
    }
}

这样你在任何 Controller 使用 request.getParameter("xxx") 时都已经是安全的。

✅ 方案六:通过 Nginx 防止 CRLF 注入(源头拦截)

🔹 拦截请求 URL、Header、参数中含有非法 \r(%0D)或 \n(%0A)的内容。
🔹 防止恶意注入响应头、绕过跨域、防止缓存污染等攻击。

🔥 使用 ngx_http_rewrite_module 进行 CRLF 拦截

核心思路:如果请求的 URI 或参数中出现 %0d%0a,直接返回 400 Bad Request

在 Nginx 配置里加上:

server {
    listen 80;
    server_name yourdomain.com;

    location / {
        # 拦截URL中的%0d或%0a
        if ($request_uri ~* "%0d|%0a") {
            return 400;
        }
      
        # 拦截Header中有CRLF(比较少见,防护第二层)
        if ($http_cookie ~* "%0d|%0a") {
            return 400;
        }
      
        # 拦截Referer、Origin等常被利用的头
        if ($http_referer ~* "%0d|%0a") {
            return 400;
        }
      
        if ($http_origin ~* "%0d|%0a") {
            return 400;
        }
      
        proxy_pass http://backend_server;
    }
}

说明
🔹 $request_uri:完整的请求 URI(包含 path 和 query string)。
🔹 $http_cookie$http_referer$http_origin:都是可以直接访问的请求头字段。
🔹 ~* 是大小写不敏感正则匹配。
🔹 检测 %0d%0a 出现,就直接拒绝请求。

🔥 升级版:使用 map 简洁拦截

如果想写得更优雅,可以这样:

map $request_uri $is_crlf_attack {
    default 0;
    ~*"%0d|%0a" 1;
}

server {
    listen 80;
    server_name yourdomain.com;

    location / {
        if ($is_crlf_attack) {
            return 400;
        }

        proxy_pass http://backend_server;
    }
}

更好维护,后续加字段也方便,比如加 $http_referer$http_origin

🔥 防止响应头注入(防止后端带回异常 header)

如果后端返回头部中出现非法字符(很少见但更高阶),可以:

proxy_pass_request_headers off;

或者手动指定允许哪些 header:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 不转发其他 header

不过一般只在后端不可信时(比如第三方 API)才需要这么严格。

防护点 配置建议
请求URI含CRLF `$request_uri ~* "%0d
Header参数含CRLF $http_referer, $http_origin 检查
后端返回头混乱 关闭 proxy_pass_request_headers 或只允许白名单头
返回状态 直接 return 400

📌 最佳实践建议

🔹 Nginx 前端拦截非法请求。
🔹 后端 Java Controller 再做一次输入清洗(安全双保险)。
🔹 定期通过扫描器(如 Burp、Nuclei)测试是否存在 CRLF 注入点。


✍️ 5、总结

虽然 CRLF 注入的出现频率不如 SQL 注入那样高,但它的破坏力不容小觑。开发者必须养成 “输入即验证” 的安全意识,特别是在处理 HTTP 响应头、重定向等接口时,务必小心。

内容 建议
风险 可注入响应头、篡改 Cookie、诱发缓存问题等
原因 未过滤或未转义 CRLF 控制字符
最佳实践 参数校验 + CRLF 清理 + 编码输出

如你有其他关于 Web 安全或 Java 安全框架相关问题,欢迎留言讨论 😄


是否需要我补充 Spring Boot 项目中集成全局防御、统一拦截策略、或者添加单元测试样例?可以继续帮你完善。

动物装饰