老旧Java项目中CSRF Token掩码处理方案咨询
问题描述
我维护着一个老旧Java项目,最近外部代码审计提出两项CSRF Token优化要求:一是定期轮换Token,避免会话全程使用同一个;二是对Token做掩码处理,防止在BurpSuite这类工具中直接被识别出来。目前Token轮换的实现思路已经确定并在推进,但还没找到Token掩码处理的有效方案,想请教相关实现思路。注:该Token会在报告下载场景的URL中暴露,示例请求如下:
GET /xyz/testcontroller/begin?event=Y&CSRF_TOKEN=-653632r324354546432tret455364&id=3576632446475442342 Host: xyz.com Cookie: JSESSIONID=452455535455762565; FileDownload=null; JSESSIONID=452455535455762565; < other security parameters> User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.75 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 <some more parameters> Referer: xxxxxx Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.9 Connection: close
CSRF Token掩码处理实现思路
1. 对称加密可逆掩码(适合快速改造,兼容性强)
- 用AES这类对称加密算法对原始CSRF Token加密,加密结果做URL安全的Base64编码后作为URL参数传递。
- 后端接收请求时,先解码并解密得到原始Token,再执行正常校验逻辑。
- 关键细节:
- 加密密钥从配置文件/环境变量加载,禁止硬编码;密钥长度符合算法要求(如AES需16/24/32字节)。
- 采用URL安全的Base64编码,避免
+//等字符在URL中被转义导致解析失败。 - 示例代码:
import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; public class CsrfMaskUtil { // 从配置读取密钥,示例为16字节AES密钥 private static final String SECRET_KEY = "your-secure-16byte-key"; public static String maskToken(String rawToken) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec); byte[] encryptedBytes = cipher.doFinal(rawToken.getBytes(StandardCharsets.UTF_8)); return Base64.getUrlEncoder().encodeToString(encryptedBytes); } public static String unmaskToken(String maskedToken) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, keySpec); byte[] decodedBytes = Base64.getUrlDecoder().decode(maskedToken); return new String(cipher.doFinal(decodedBytes), StandardCharsets.UTF_8); } }
2. 哈希加盐掩码(不可逆,依赖会话存储)
- 原始Token存在用户会话中,URL传递的是
原始Token+固定盐值的哈希结果(如SHA-256)。 - 后端校验时,从会话取出原始Token,用相同盐值计算哈希,与URL中的掩码值比对。
- 关键细节:
- 盐值需固定且保密,避免泄露后被逆向破解。
- 适合已在会话中存储Token的场景,无需额外加密逻辑。
- 示例代码:
import java.security.MessageDigest; import java.nio.charset.StandardCharsets; public class CsrfMaskUtil { private static final String FIXED_SALT = "your-confidential-salt"; public static String generateMaskedToken(String rawToken) throws Exception { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hashBytes = digest.digest((rawToken + FIXED_SALT).getBytes(StandardCharsets.UTF_8)); // 转十六进制字符串 StringBuilder sb = new StringBuilder(); for (byte b : hashBytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } public static boolean validateMaskedToken(String maskedToken, String rawTokenFromSession) throws Exception { if (rawTokenFromSession == null) return false; String expectedMask = generateMaskedToken(rawTokenFromSession); return expectedMask.equals(maskedToken); } }
3. 字符规则替换(轻量方案,快速上线)
- 对原始Token的字符做固定规则的替换混淆,比如字母移位、数字映射,后端接收后反向还原。
- 关键细节:
- 规则需具备一定复杂度,避免被轻易逆向,但无需过度设计。
- 确保替换后的字符均为URL安全字符,无特殊符号。
- 示例代码:
public class CsrfMaskUtil { // 掩码:字母+5循环移位,数字+5循环移位 public static String maskToken(String rawToken) { StringBuilder masked = new StringBuilder(); for (char c : rawToken.toCharArray()) { if (Character.isLowerCase(c)) { masked.append((char) ((c - 'a' + 5) % 26 + 'a')); } else if (Character.isUpperCase(c)) { masked.append((char) ((c - 'A' + 5) % 26 + 'A')); } else if (Character.isDigit(c)) { masked.append((char) ((c - '0' + 5) % 10 + '0')); } else { masked.append(c); } } return masked.toString(); } // 解掩码:反向移位 public static String unmaskToken(String maskedToken) { StringBuilder raw = new StringBuilder(); for (char c : maskedToken.toCharArray()) { if (Character.isLowerCase(c)) { raw.append((char) ((c - 'a' - 5 + 26) % 26 + 'a')); } else if (Character.isUpperCase(c)) { raw.append((char) ((c - 'A' - 5 + 26) % 26 + 'A')); } else if (Character.isDigit(c)) { raw.append((char) ((c - '0' - 5 + 10) % 10 + '0')); } else { raw.append(c); } } return raw.toString(); } }
通用注意事项
- 统一处理逻辑:通过过滤器(Filter)拦截所有包含CSRF_TOKEN参数的请求,先完成解掩码/校验,再传递给业务控制器,减少业务代码修改量。
- URL安全验证:确保掩码后的Token无特殊字符,避免URL编码导致解析失败。
- 测试验证:抓包确认BurpSuite无法直接识别掩码后的Token为CSRF Token(工具通常识别格式规整的Token字符串,掩码后格式改变即可规避)。
内容的提问来源于stack exchange,提问作者Sam
相关产品推荐
相关产品推荐

