合规的双向加密数据的传输方案:
1)后端生成非对称算法(国密SM2、RSA2048)的公钥B1、私钥B2,前端访问后端获取公钥B1。
2)前端每次发送请求前,随机生成对称算法(国密SM4、AES256)的密钥A1。
3)公钥、私钥可以全系统固定为一对,前端可以储存公钥,但私钥不能保存在后端数据库中。
4)前端用步骤2的密钥A1加密所有业务数据生成en_data,用步骤1获取的公钥B1加密密钥A1生成en_key。
5)前端用哈希算法对en_data + en_key的值形成一个校验值check_hash。
6)前端将en_data、en_key、check_hash三个参数包装在同一个http数据包中发送到后端。
7)后端获取三个参数后先判断哈希值check_hash是否匹配en_data + en_key以验证完整性。
8)后端用私钥B2解密en_key获取本次请求的对称算法的密钥A1。
9)后端使用步骤8获取的密钥A1解密en_data获取实际业务数据。
10)后端处理完业务逻辑后,将需要返回的信息使用密钥A1进行加密后回传给前端。
11)加密数据回传给前端后,前端使用A1对加密的数据进行解密获得返回的信息。
12)步骤2随机生成的密钥A1已经使用完毕,前端应将其销毁。
合规的防篡改和防重放攻击的传输方案:
1)客户端获取公钥时应同时获取后端服务器时间,保证客户端和服务器时间一致。
2)前端每次发送请求前,应在header请求头中添加时间戳字段。
3)通过 url + 时间戳 + 用户token + http请求体(可以为空) 产生 sign 签名。
4)将 sign 签名添加到header请求头中后发送请求。
5)后端校验 sign 签名是否正确,如果签名校验不通过应提示"数据被篡改"并丢弃当前请求。
6)如果时间戳的时间和服务器时间相差大于10秒,应丢弃当前请求。
7)后台应缓存每一个sign值10秒,在10秒内如果出现包含同一个sign值的请求,应丢弃当前请求。
8)用户token中部分数据(禁止全部)应存储在sessionStorage中,以保证页面关闭后登录失效。
如果应用程序未正确校验用户输入的数据,则恶意用户可能会破坏应用程序的逻辑以执行针对客户端或服务器端的攻击。
脆弱代码 1:
// 攻击者可以提交 lang 的内容为:
// en&user_id=1#
// 致使攻击者可以随意篡改 user_id 的值
String lang = request.getParameter("lang");
GetMethod get = new GetMethod("http://www.host.com");
// 攻击者提交 lang=en&user_id=1#&user_id=123 可覆盖原始 user_id 的值
get.setQueryString("lang=" + lang + "&user_id=" + user_id);
get.execute();
解决方案 1:
// 参数化绑定
URIBuilder uriBuilder = new URIBuilder("http://www.host.com/viewDetails");
uriBuilder.addParameter("lang", input);
uriBuilder.addParameter("user_id", userId);
HttpGet httpget = new HttpGet(uriBuilder.build().toString());
脆弱逻辑 2:
订单系统计算订单的价格
步骤1:
订单总价 = 商品1单价 * 商品1数量 + 商品2单价 * 商品2数量 + ...
步骤2:
钱包余额 = 钱包金额 - 订单总价
当攻击者将商品数量都篡改为负数,导致步骤1的订单总价为负数。而负负得正,攻击者不仅买入了商品并且钱包金额也增长了。
解决方案 2:
应在后台严格校验订单中每一个输入参数的长度、格式、逻辑、特殊字符。
整体解决方案:
查询字符串是 GET 参数名称和值的串联,可以传入非预期参数。
风险:
例如 URL 请求 /app/servlet.htm?a=1&b=2
则对应查询字符串提取为 a=1&b=2
那么 HttpServletRequest.getParameter() HttpServletRequest.getQueryString()
获取的值都可能是不安全的。
解决方案:
攻击者可以恶意篡改或伪造所有 http 请求头中的参数,达到破坏应用程序的逻辑以执行针对客户端或服务器端的目的。
GET /somePage HTTP/1.1
Host: yourwebsite.com
User-Agent: Mozilla/5.0
Cookie: JSESSIONID=Any value of the user's choice!!??'''">
脆弱代码:
Cookie[] cookies = request.getCookies();
for (int i =0; i< cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("authenticated") && Boolean.TRUE.equals(c.getValue())) {
authenticated = true;
}
}
以上代码直接从 cookie 中而不是 session 中提取了参数作为登录状态的判断,导致攻击者可以伪造登录状态。
解决方案:
HTTP 请求头 Content-Type 可以由恶意的攻击者控制。因此,HTTP 的 Content-Type 值不应在任何重要的逻辑流程中使用。
ServletRequest.getServerName() 和 HttpServletRequest.getHeader("Host") 具有相同的逻辑,即提取 Host 请求头。
GET /testpage HTTP/1.1
Host: www.example.com
因为恶意的攻击者可以伪造 Host 请求头,所以 HTTP 的 Host 值不应在任何重要的逻辑流程中使用。
请求头 User-Agent 很容易被客户端伪造,不建议基于 User-Agent 的值采用不同的安全校验逻辑。
以下 IP 请求头,很容易被客户端伪造,可能导致 IP 地址欺骗。
解决方案 1:
应用程序与用户间无代理时, 应使用 getRemoteAddr 函数获取用户 ip。
解决方案 2:
应用程序与用户间存在代理时, 应使用代理头获取用户 ip。
private String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
System.out.println("x-forwarded-for ip: " + ip);
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if( ip.indexOf(",")!=-1 ){
ip = ip.split(",")[0];
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
System.out.println("Proxy-Client-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
System.out.println("WL-Proxy-Client-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
System.out.println("HTTP_CLIENT_IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
System.out.println("HTTP_X_FORWARDED_FOR ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
System.out.println("X-Real-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
System.out.println("getRemoteAddr ip: " + ip);
}
System.out.println("获取客户端ip: " + ip);
return ip;
}
风险 1:
脆弱代码 2:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用 csrf 保护会导致 csrf 攻击的产生
http.csrf().disable();
}
}
解决方案:
当 Web 应用程序将用户重定向并转发到其他页面或其他外部网站,如果不验证这些页面的可信度,攻击者可以将受害者重定向到网络钓鱼或恶意软件站点,或者恶意利用转发来访问未经授权的页面。
脆弱代码 1:
@RequestMapping("/redirect")
public String redirect(@RequestParam("url") String url) {
// 不执行校验就直接跳转
return "redirect:" + url;
}
脆弱代码 2:
protected void doGet(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
// 不执行校验就直接跳转
resp.sendRedirect(req.getParameter("redirectUrl"));
}
解决方案:
完整 URL 格式: protocol://hostname[:port]/path/[;parameters][?query]#fragment
当 Web 应用程序根据用户请求对数据资源发起请求,如果不对该数据资源执行安全校验,攻击者可能获取敏感数据资源。
脆弱代码:
@WebServlet( "/downloadServlet" )
public class downloadServlet extends HttpServlet {
protected void doPost( HttpServletRequest request,
HttpServletResponse response ) throws ServletException, IOException{
this.doGet( request, response );
}
protected void doGet( HttpServletRequest request,
HttpServletResponse response ) throws ServletException, IOException{
String filename = "1.txt";
// 没有校验 url 变量的安全性
String url = request.getParameter( "url" );
response.setHeader( "content-disposition", "attachment;fileName=" + filename );
int len;
OutputStream outputStream = response.getOutputStream();
// 直接使用 url 变量导致任意文件读取
URL file = new URL( url );
byte[] bytes = new byte[1024];
InputStream inputStream = file.openStream();
while ( (len = inputStream.read( bytes ) ) > 0 )
{
outputStream.write( bytes, 0, len );
}
}
}
使用以下请求可以下载服务器硬盘上的文件
http://localhost:8080/downloadServlet?url=file:///c:\1.txt
解决方案:
完整 URL 格式: protocol://hostname[:port]/path/[;parameters][?query]#fragment
如果将未经过滤的命令参数传递给执行命令的 API,可以导致任意命令执行。
脆弱代码:
import java.lang.Runtime;
Runtime r = Runtime.getRuntime();
r.exec("/bin/sh -c some_tool" + input)
如果将 input 的内容从 1.txt
篡改为 1.txt && reboot
,则可以导致服务器重启。
解决方案:
& | ; $ > < ` ' " ! ? * #
攻击者通过将恶意数据传递到反序列化 API,可导致:读写任意文件、执行系统命令、探测或攻击内网等危害。
解决方案:
确保使用安全的组件和安全的编码执行反序列化操作。
所有 java 应用程序应配置全局反序列化白名单,保证只有必要的类才能被反序列化。
// 针对每一次反序列化的输入数据,配置安全限制
maxdepth=value // 每一个内置对象的最大深度(一次反序列化可能会有多个内置对象)
maxrefs=value // 对象引用的最大上限
maxbytes=value // 输入数据的字节数上限
maxarray=value // 反序列化数组时的最大上限
// 以下示例介绍了限制反序列化的类名称的配置方法
// 允许输入字节数最大为100,允许唯一类 org.example.Teacher ,并阻止其它一切的类
jdk.serialFilter=maxbytes=100;org.example.Teacher;!*
// 允许输入字节数最大为100,允许 org.example. 下的所有类,并阻止其它一切的类
jdk.serialFilter=maxbytes=100;org.example.*;!*
// 允许输入字节数最大为100,允许 org.example. 下的所有类和子类,并阻止其它一切的类
jdk.serialFilter=maxbytes=100;org.example.**;!*
// 允许一切类
jdk.serialFilter=*;
; 作为表达式的分隔符
.* 代表当前包下的所有类
.** 代表当前包下所有类和所有子类
! 代表取反,禁止匹配符号后的表达式被反序列化
* 通配符
jdk11+:%JAVA_HOME%\conf\security\java.security
jdk8: %JAVA_HOME%\jre\lib\security\java.security
java -Djdk.serialFilter=maxbytes=100;org.example.**;!*
Properties props = System.getProperties();
props.setProperty("jdk.serialFilter", "maxbytes=100;org.example.**;!*");
参考链接:
// jackson白名单过滤
ObjectMapper om = new ObjectMapper();
BasicPolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
// 信任 com.xxxx. 包下的类
.allowIfBaseType("com.xxxx.")
.allowIfSubType("com.xxxx.")
// 信任 Collection、Map 等基础数据结构
.allowIfSubType(Collection.class)
.allowIfSubType(Number.class)
.allowIfSubType(Map.class)
.allowIfSubType(Temporal.class)
.allowIfSubTypeIsArray()
.build();
om.activateDefaultTyping(validator,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
ParserConfig.getGlobalInstance().addAutoTypeCheckHandler((typeName, expectClass, features) -> {
if (null != typeName && typeName.contains("java.net.")) {
throw new JSONException("not support autoType : " + typeName);
}
return null;
});
1. 在代码中配置
ParserConfig.getGlobalInstance().setSafeMode(true);
如果使用new ParserConfig的方式,需要注意单例处理,否则会导致低性能full gc
2. 加上JVM启动参数
-Dfastjson.parser.safeMode=true
3. 通过fastjson.properties文件配置。
通过类路径的fastjson.properties文件配置,配置方式如下:
fastjson.parser.safeMode=true
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
static final Filter autoTypeFilter = JSONReader.autoTypeFilter(
// 按需加上需要支持自动类型的类名前缀,范围越小越安全
"org.springframework.security.core.authority.SimpleGrantedAuthority"
);
private Class<T> clazz;
public FastJsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) {
if (t == null) {
return new byte[0];
}
return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
}
@Override
public T deserialize(byte[] bytes) {
if (bytes == null || bytes.length <= 0) {
return null;
}
return JSON.parseObject(bytes, clazz, autoTypeFilter);
}
}
-Dfastjson2.parser.safeMode=true
参考地址: https://github.com/alibaba/fastjson2/wiki/fastjson2_autotype_cn
// 使用默认解析器
XStream xStream = new XStream();
// 必须开启安全模式,安全模式采用白名单限制输入的数据类型
XStream.setupDefaultSecurity(xStream);
// 在白名单内添加一些基本数据类型
xstream.addPermission(NullPermission.NULL);
xstream.addPermission(PrimitiveTypePermission.PRIMITIVES);
xstream.allowTypeHierarchy(Collection.class);
// 在白名单中添加可信任包内所有的子类
xstream.allowTypesByWildcard(new String[] {
Blog.class.getPackage().getName()+".*"
});
官方参考:
脆弱代码:
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie加密的密钥
// 默认AES算法,密钥可选长度(128 192 256 位)
// 不应使用外泄的128位密钥
cookieRememberMeManager.setCipherKey(Base64.decode("1QWLxg+NYmxraMoxAXu/Iw=="));
return cookieRememberMeManager;
}
解决方案:
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie加密的密钥,建议每个项目都不一样
// 应重新生成 192 位或 256 位密钥,或从配置中心统一获取密钥
cookieRememberMeManager.setCipherKey(GenerateCipherKey.generateNewKey());
return cookieRememberMeManager;
}
public static class GenerateCipherKey {
public static byte[] generateNewKey() {
KeyGenerator kg;
try {
kg = KeyGenerator.getInstance("AES");
} catch (NoSuchAlgorithmException var5) {
String msg = "Unable to acquire AES algorithm. This is required to function.";
throw new IllegalStateException(msg, var5);
}
// 密钥应选长度(192 或 256)位
kg.init(256);
SecretKey key = kg.generateKey();
return key.getEncoded();
}
}
当 XML 解析器处理从不受信任的来源接收到的 XML 时支持 XML 实体,可能会发生 XML 外部实体(XXE)攻击。
脆弱代码:
public void parseXML(InputStream input) throws XMLStreamException {
XMLInputFactory factory = XMLInputFactory.newFactory();
XMLStreamReader reader = factory.createXMLStreamReader(input);
[...]
}
解决方案:
读取外部传入 XML 文件时,XML 解析器初始化过程中配置关闭 DTD 解析。
"http://apache.org/xml/features/nonvalidating/load-external-dtd", false
"http://xml.org/sax/features/external-general-entities", false
"http://xml.org/sax/features/external-parameter-entities", false
如果不需要 inline DOCTYPE 声明,应使用以下配置将其完全禁用。
"http://apache.org/xml/features/disallow-doctype-decl", true
以上配置等价于:
XMLConstants.ACCESS_EXTERNAL_DTD, ""
XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""
// 使用默认解析器
XStream xStream = new XStream();
// 必须开启安全模式,安全模式采用白名单限制输入的数据类型
XStream.setupDefaultSecurity(xStream);
// 在白名单内添加一些基本数据类型
xstream.addPermission(NullPermission.NULL);
xstream.addPermission(PrimitiveTypePermission.PRIMITIVES);
xstream.allowTypeHierarchy(Collection.class);
// 在白名单中添加可信任包内所有的子类
xstream.allowTypesByWildcard(new String[] {
Blog.class.getPackage().getName()+".*"
});
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
dbf.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder builder = dbf.newDocumentBuilder();
[...]
}
SAXBuilder builder = new SAXBuilder();
builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
builder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
Document doc = builder.build(InputSource);
[...]
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
SAXParser parser = spf.newSAXParser();
parser.parse(InputSource, (HandlerBase) null);
[...]
SAXReader saxReader = new SAXReader();
saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
saxReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
saxReader.read(InputSource);
[...]
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
reader.parse(new InputSource(InputSource));
[...]
SAXTransformerFactory sf = (SAXTransformerFactory)SAXTransformerFactory.newInstance();
sf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
sf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
StreamSource source = new StreamSource(InputSource);
sf.newTransformerHandler(source);
[...]
SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
StreamSource source = new StreamSource(InputSource);
Schema schema = factory.newSchema(source);
[...]
TransformerFactory tf = TransformerFactory.newInstance();
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
StreamSource source = new StreamSourceInputSource);
tf.newTransformer().transform(source, new DOMResult());
[...]
XPath 注入类似于 SQL 注入,如果不校验而直接拼接用户输入,可能导致应用程序执行恶意的 XPath 语句,而攻击者则可以越权读取或恶意篡改目标 XML 数据。
下面以登录验证中的模块为例,说明 XPath 注入攻击的实现原理。
在应用程序的登录验证程序中,一般有用户名(username)和密码(password) 两个参数,程序会通过用户所提交输入的用户名和密码来执行授权操作。若验证数据存放在 XML 文件中,其原理是通过查找 user 表中的用户名 (username)和密码(password)的结果进行授权访问。
例如存在 user.xml 文件:
<user>
<firstname>Ben</firstname>
<lastname>Elmore</lastname>
<loginID>abc</loginID>
<password>test123</password>
</user>
<user>
<firstname>Shlomy</firstname>
<lastname>Gantz</lastname>
<loginID>xyz</loginID>
<password>123test</password>
</user>
在 XPath 中典型的查询语句如下:
//users/user[loginID/text()='xyz'and password/text()='123test']
正常用户传入 login 和 password,例如 loginID = 'xyz' 和 password = '123test',则该查询语句将返回 true。但如果恶意用户传入类似 ' or 1=1 or ''=' 的值,那么该查询语句也会得到 true 返回值,因为 XPath 查询语句最终会变成如下代码:
//users/user[loginID/text()=''or 1=1 or ''='' and password/text()='' or 1=1 or ''='']
脆弱代码:
public int risk(HttpServletRequest request,
Document doc, XPath xpath ,org.apache.log4jLogger logger) {
int len = 0;
String path = request.getParameter("path");
try {
XPathExpression expr = xpath.compile(path);
Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
len = nodes.getLength();
} catch (XPathExpressionException e) {
logger.warn("Exception", e);
}
return len;
}
解决方案:
public int fix(HttpServletRequest request,
Document doc, XPath xpath ,org.apache.log4j.Logger logger) {
int len = 0;
String path = request.getParameter("path");
try {
// 使用过滤函数 filterForXPath 过滤用户输入
String filtedXPath = filterForXPath(path);
XPathExpression expr = xpath.compile(filtedXPath);
Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
len = nodes.getLength();
} catch (XPathExpressionException e) {
logger.warn("Exception", e);
}
return len;
}
// 限制用户的输入数据,尤其应限制特殊字符
public String filterForXPath(String input) {
if (input == null) {
return null;
}
StringBuilder out = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (c >= 'A' && c <= 'Z') {
out.append(c);
} else if (c >= 'a' && c <= 'z') {
out.append(c);
} else if (c >= '0' && c <= '9') {
out.append(c);
} else if (c == '_' || c == '-') {
//限制特殊字符的使用
out.append(c);
} else if (c >= 0x4e00 && c <= 0x9fa5) {
//允许汉字使用
out.append(c);
}
}
return out.toString();
}
攻击者可以构造恶意 EL 代码注入到 EL 表达式引擎执行恶意代码。
脆弱代码 1:
public void parseExpressionInterface(Person personObj, String property) {
ExpressionParser parser = new SpelExpressionParser();
// property 变量内容不做限制可能导致任意的EL表达式执行
Expression exp = parser.parseExpression(property+" == 'Albert'");
StandardEvaluationContext testContext = new StandardEvaluationContext(personObj);
boolean result = exp.getValue(testContext, Boolean.class);
}
脆弱代码 2:
public void evaluateExpression(String expression) {
FacesContext context = FacesContext.getCurrentInstance();
ExpressionFactory expressionFactory = context.getApplication().getExpressionFactory();
ELContext elContext = context.getELContext();
// expression 变量不做任何处理就传入表达式引擎执行可能导致任意的EL表达式执行
ValueExpression vex = expressionFactory.createValueExpression(elContext, expression, String.class);
return (String) vex.getValue(elContext);
}
解决方案:
禁止使用动态的 EL 表达式编写复杂的业务逻辑,也应禁止用户输入的 EL 表达式使用 StandardEvaluationContext 执行,可以使用安全的 SimpleEvaluationContext 进行执行。
攻击者可以构造恶意 js 注入到 js 引擎执行恶意代码。
脆弱代码:
public void runCustomTrigger(String script) {
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("JavaScript");
// 不执行安全校验,直接eval执行可能造成恶意的js代码执行
engine.eval(script);
}
解决方案:
在 java 中使用 js 引擎应使用安全的沙盒模式执行 js 代码。
<dependency>
<groupId>org.javadelight</groupId>
<artifactId>delight-nashorn-sandbox</artifactId>
<version>[insert latest version]</version>
</dependency>
// 创建沙盒
NashornSandbox sandbox = NashornSandboxes.create();
// 沙盒内默认禁止js代码访问所有的java类对象
// 沙盒可以手工授权js代码能访问的java类对象
sandbox.allow(File.class);
// eval执行js代码
sandbox.eval("var File = Java.type('java.io.File'); File;")
public void runCustomTrigger(String script) {
// 启用 Rhino 引擎的js沙盒模式
SandboxContextFactory contextFactory = new SandboxContextFactory();
Context context = contextFactory.makeContext();
contextFactory.enterContext(context);
try {
ScriptableObject prototype = context.initStandardObjects();
prototype.setParentScope(null);
Scriptable scope = context.newObject(prototype);
scope.setPrototype(prototype);
context.evaluateString(scope,script, null, -1, null);
} finally {
context.exit();
}
}
如果系统设置 bean 属性前未进行严格的校验,攻击者可以设置能影响系统完整性的任意 bean 属性。 例如 BeanUtils.populate 函数或类似功能函数允许设置 Bean 属性或嵌套属性。攻击者可以利用此功能来访问特殊的 Bean 属性 class.classLoader,从而可以覆盖系统属性并可能执行任意代码。
脆弱代码:
MyBean bean = ...;
HashMap map = new HashMap();
Enumeration names = request.getParameterNames();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
map.put(name, request.getParameterValues(name));
}
BeanUtils.populate(bean, map);
解决方案:
Bean 属性的成分复杂,用户输入的数据应严格校验后才能填充到 Bean 的属性。
将用户输入数据绑定到对象时如不做限制,可能造成攻击者恶意覆盖用户数据。
脆弱代码:
@javax.persistence.Entity
class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private Long role;
}
@Controller
class UserController {
@PutMapping("/user/")
@ResponseStatus(value = HttpStatus.OK)
public void update(UserEntity user) {
// 攻击者可以构造恶意user对象,将id字段构造为管理员id,将password字段构造为弱密码
// 如果鉴权不完整,接口读取恶意user对象的id字段后会覆盖管理员的password字段成为弱密码
userService.save(user);
}
}
解决方案 1:
使用 @InitBinder 局部限制 Controller 的数据绑定
@Controller
class UserController {
@InitBinder
public void initBinder(WebDataBinder binder, WebRequest request){
// 对允许绑定的字段设置白名单,阻止其他所有字段
binder.setAllowedFields(["username","firstname","lastname"]);
// 或者
// 对不允许绑定的字段设置黑名单,允许其他所有字段
binder.setDisallowedFields(["home_address","password"]);
}
}
解决方案 2:
使用 @ControllerAdvice + @InitBinder 全局限制所有 Controller 的数据绑定
@ControllerAdvice
public final class GlobalDataBinder
{
@InitBinder
public void initBinder(WebDataBinder binder, WebRequest request) {
// 对允许绑定的字段设置白名单,阻止其他所有字段
binder.setAllowedFields(["username","firstname","lastname"]);
// 或者
// 对不允许绑定的字段设置黑名单,允许其他所有字段
binder.setDisallowedFields(["home_address","password"]);
}
}
解决方案 3:
使用 jackson 注解 @JsonIgnore 限制数据绑定。
@Controller
class UserController {
// 参数业务应使用POST传递
@PostMapping("/user")
public UserEntity updateUser(@RequestBody JSONObject requestJson) {
// 敏感字段 password 已受限
return userService.updateById(requestJson).update();
}
}
class UserEntity {
@Id
private Long id;
private String username;
@JsonIgnore
private String password;
}
解决方案 4:
使用 jackson 注解 @JsonIgnoreProperties 限制数据绑定。
@Controller
class UserController {
// 参数业务应使用POST传递
@PostMapping("/user")
public UserEntity updateUser(@RequestBody JSONObject requestJson) {
// 敏感字段 password secretKey 已受限
return userService.updateById(requestJson).update();
}
}
@JsonIgnoreProperties({"password","secretKey"})
class UserEntity {
@Id
private Long id;
private String username;
private String password;
private String secretKey;
}
当存在缺陷的正则表达式处理某些字符串时,正则表达式引擎可能会花费大量时间甚至导致宕机,此类风险称为 ReDOS。
脆弱代码:
符号 | 符号 [] 符号 + 三者联合使用可能受到 ReDOS 攻击:
表达式: (\d+|[1A])+z
需求: 会匹配任意数字或任意(1或A)字符串加上字符z
匹配字符串: 111111111 (10 chars)
计算步骤数: 46342
如果两个重复运算符过近,那么有可能收到攻击。请看以下例子:
例子1:
表达式: .*\d+\.jpg
需求: 会匹配任意字符加上数字加上.jpg
匹配字符串: 1111111111111111111111111 (25 chars)
计算步骤数: 9187
例子2:
表达式: .*\d+.*a
需求: 会匹配任意字符串加上数字加上任意字符串加上a字符
匹配字符串: 1111111111111111111111111 (25 chars)
计算步骤数: 77600
例子3:
表达式: ^(a+)+$ 处理 aaaaaaaaaaaaaaaaX
重复运算符嵌套将使正则表达式引擎分析65536个不同的匹配路径。
解决方案:
^(a+)+$
应替换成^a+$
攻击者嵌入恶意脚本代码到正常用户会访问到的页面中,当正常用户访问该页面时,则可导致嵌入的恶意脚本代码的执行,从而达到恶意攻击用户的目的。
常见的攻击向量:
<Img src = x onerror = "javascript: window.onerror = alert; throw XSS">
<Video> <source onerror = "javascript: alert (XSS)">
<Input value = "XSS" type = text>
<applet code="javascript:confirm(document.cookie);">
<isindex x="javascript:" onmouseover="alert(XSS)">
"></SCRIPT>”>’><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>
"><img src="x:x" onerror="alert(XSS)">
"><iframe src="javascript:alert(XSS)">
<object data="javascript:alert(XSS)">
<isindex type=image src=1 onerror=alert(XSS)>
<img src=x:alert(alt) onerror=eval(src) alt=0>
<img src="x:gif" onerror="window['al\u0065rt'](0)"></img>
解决方案:
禁止简单的正则过滤,浏览器存在容错机制,可能会将攻击者精心构造的变形前端代码渲染成攻击向量。
原则上禁止用户输入特殊字符,或者转义用户输入的特殊字符。
富文本输出内容应进行白名单校验,只能对用户渲染安全的 HTML 标签和安全的 HTML 属性,请参照以下链接。
禁止上传包含JavaScript代码的pdf文件
// 引入pdfbox依赖包
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.30</version>
</dependency>
// 使用pdfbox限制包含js代码的pdf上传
public static void main(String[] args) throws IOException {
String s1 = "poc.pdf";
File file = new File(s1);
boolean b = containsJavaScript(file);
if (b) {
System.out.println("pdf文件包含,js脚本代码");
}
}
/**
* 校验pdf文件是否包含js脚本
**/
public static boolean containsJavaScript(File file) throws IOException {
PDDocument document = PDDocument.load(file);
return containsJavaScript(document);
}
/**
* 校验pdf文件是否包含js脚本
**/
public static boolean containsJavaScript(InputStream input) throws IOException {
PDDocument document = PDDocument.load(input);
return containsJavaScript(document);
}
/**
* 校验pdf文件是否包含js脚本,这里使用分页解析,因为对于大pdf文件(文字较多),会出现加载问题。
**/
public static boolean containsJavaScript(PDDocument document) {
//Getting the PDDocumentInformation object
PDPageTree pages = document.getPages();
return IntStream.range(0, pages.getCount()).anyMatch(i -> {
//return str.contains("JavaScript") || str.contains("COSName{JS}");
return pages.get(i).getCOSObject().toString().contains("COSName{JS}");
});
}
当应用程序读取文件名打开对应的文件以读取其内容,而该文件名来自于用户的输入数据。那么将未经过滤的文件名数据传递给文件 API,则攻击者可以从系统中读取任意文件。
脆弱代码:
@GET
@Path("/images/{image}")
@Produces("images/*")
public Response getImage(@javax.ws.rs.PathParam("image") String image) {
// image变量中未校验 ../ 或 ..\
File file = new File("resources/images/", image);
if (!file.exists()) {
return Response.status(Status.NOT_FOUND).build();
}
return Response.ok().entity(new FileInputStream(file)).build();
}
解决方案:
import org.apache.commons.io.FilenameUtils;
@GET
@Path("/images/{image}")
@Produces("images/*")
public Response getImage(@javax.ws.rs.PathParam("image") String image) {
// 首先进行逻辑校验,判断用户是否有权限访问接口 以及 用户对访问的资源是否有权限
// 过滤image变量中的 ../ 或 ..\
File file = new File("resources/images/", FilenameUtils.getName(image));
if (!file.exists()) {
return Response.status(Status.NOT_FOUND).build();
}
return Response.ok().entity(new FileInputStream(file)).build();
}
当应用程序打开文件并写入数据,而该文件名来自于用户的输入数据。那么将未经过滤的文件名数据传递给文件 API,则攻击者可以写入任意数据到系统中。
脆弱代码:
@RequestMapping("/MVCUpload")
public String MVCUpload(@RequestParam( "description" ) String description, @RequestParam("file") MultipartFile file) throws IOException {
// 首先进行逻辑校验,判断用户是否有权限访问接口 以及 用户对访问的资源是否有权限
InputStream inputStream=file.getInputStream();
String fileName=file.getOriginalFilename();
// 文件名fileName未校验 ../ 或 ..\ 并且也未校验文件后缀
OutputStream outputStream=new FileOutputStream("/tmp/"+fileName);
byte[] bytes=new byte[10];
int len=-1;
// 将文件写入系统中
while((len=inputStream.read(bytes))!=-1){
outputStream.write(bytes,0,len);
}
outputStream.close();
inputStream.close();
// 记录审计日志
return "success";
}
解决方案:
import org.apache.commons.io.FilenameUtils;
@RequestMapping("/MVCUpload")
public String MVCUpload(@RequestParam( "description" ) String description, @RequestParam("file") MultipartFile file) throws IOException {
// 首先进行逻辑校验,判断用户是否有权限访问接口 以及 用户对访问的资源是否有权限
InputStream inputStream=file.getInputStream();
String fileInput;
if(file.getOriginalFilename() == null){
return "error";
}
// 获取上传文件名后强制转化为小写并过滤空白字符
fileInput=file.getOriginalFilename().toLowerCase().trim();
// 对变量fileInput所代表的文件路径去除目录和后缀名,可以过滤文件名中的 ../ 或 ..\
String fileName=FilenameUtils.getBaseName(fileInput);
// 获取文件后缀
String ext=FilenameUtils.getExtension(fileInput);
// 文件名应大于等于 1 并且小于等于 30
if ( 1 > fileName.length() || fileName.length() > 30 ) {
return "error";
}
// 文件名只能包含大小写字母、数字和中文
if(fileName.matches("0-9a-zA-Z\u4E00-\u9FA5]+")){
return "error";
}
// 依据业务逻辑使用白名单校验文件后缀
if(!"jpg".equals(ext)){
return "error";
}
// 将文件写入系统时,应确保文件不写入web路径中
OutputStream outputStream=new FileOutputStream("/tmp/"+ fileName + "." + ext);
byte[] bytes=new byte[10];
int len=-1;
while((len=inputStream.read(bytes))!=-1){
outputStream.write(bytes,0,len);
}
outputStream.close();
inputStream.close();
// 记录审计日志
return "success";
}
当两个或两个以上的线程对同一个数据进行操作的时候,可能会产生“竞争条件”的现象。这种现象产生的根本原因是因为多个线程在对同一个数据进行操作,此时对该数据的操作是非“原子化”的,可能前一个线程对数据的操作还没有结束,后一个线程又开始对同样的数据开始进行操作,这就可能会造成数据结果的变化未知。
解决方案:
java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,应通过 catch NumberFormatException 来实现。
if (obj != null) {
...
}
try {
obj.method();
} catch ( NullPointerException e ) {
...
}
异常捕获后不要用来做流程控制,条件控制。
说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
正例: 用户注册的场景中,如果用户用户名称已存在或用户输入密码过于简单,在程序上作出"用户名或密码错误",并提示给用户。
反例: 用户提交表单场景中,如果用户输入的价格为感叹号,系统不做任何提示,系统在后台提示报错。
捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务。
finally 块中必须对临时文件、资源对象、流对象进行资源释放,有异常也要做 try-catch。
说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。
不要在 finally 块中使用 return。
说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点。
private int x = 0;
public int checkReturn(){
try {
/* x 等于 1,此处不返回 */
return(++x);
} finally {
/* 返回的结果是 2 */
return(++x);
}
}
捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable 类来进行拦截。
说明:通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。什么情况会抛出 NoSuchMethodError 呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出 NoSuchMethodError。
方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。
说明:本手册明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。
防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
public int f() {
return Integer; // 对象
} // 如果为 null,自动解箱抛 NPE。
定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(),更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException 等。
对于公司外的 http/api 开放接口必须使用 errorCode;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess()方法、errorCode、errorMessage;而应用内部直接抛出异常即可。
说明:关于 RPC 方法返回方式使用 Result 方式的理由:
系统应具有全局性的异常处理机制,避免未知异常直接抛出,防止攻击者探查系统内部敏感信息。
@ControllerAdvice
public class GlobalException {
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public ResultData<String> runtimeException(RuntimeException e) {
log.error("运行异常", e);
return ResultData.fail(ResultCode.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(Exception.class)
@ResponseBody
public ResultData<String> exception(Exception e) {
log.error("系统未知异常", e);
return ResultData.fail(ResultCode.INTERNAL_SERVER_ERROR);
}
}
SQL 注入是指应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在应用程序中事先定义好的 SQL 语句中添加额外的 SQL 语句。
Druid 对 sql 语句进行"词法解析",把完整的 sql 语句切分成单独的 sql token,解析成"词法树",再将词法树中的 token 节点进行 sql 语义识别,将其解析成符合 sql 规范的"语法树",最后对语法树进行安全校验。该技术是目前攻击者最难绕过的 sql 注入防护技术。
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
...
<property name="filters" value="wall"/>
</bean>
脆弱代码:
public void risk(HttpServletRequest request, Connection c, org.apache.log4j.Logger logger) {
String text = request.getParameter("text");
// 使用拼接导致SQL注入
String sql = "select * from tableName where columnName = '" + text + "'";
try {
Statement s = c.createStatement();
s.executeQuery(sql);
} catch (SQLException e) {
logger.warn("Exception", e);
}
}
解决方案:
public void fix(HttpServletRequest request, Connection c, org.apache.log4j.Logger logger) {
String text = request.getParameter("text");
// 使用 PreparedStatement 预编译并使用占位符防止SQL注入
String sql = "select * from tableName where columnName = ?";
try {
PreparedStatement s = c.prepareStatement(sql);
s.setString(1, text);
s.executeQuery();
} catch (SQLException e) {
logger.warn("Exception", e);
}
}
1) Mybatis 执行 like 语句的安全编码
脆弱代码:
模糊查询like
Select * from news where title like '%#{title}%'
但由于这样写程序会报错,研发人员将SQL查询语句修改如下:
Select * from news where title like '%${title}%'
修改SQL语句之后,程序停止报错。但是却引入了 SQL 语句拼接的问题,极有可能引发 SQL 注入漏洞。
解决方案:
可使用 concat 函数解决 SQL 语句动态拼接的问题
select * from news where tile like concat('%', #{title}, '%')
注意!对搜索的内容必须进行严格的逻辑校验:
1)例如搜索用户手机号,应限制输入数据只能输入数字,防止出现搜索英文或中文的无效搜索
2)mybatis预编译不会转义 % 符号,应阻止用户输入 % 符号以防止全表扫描
3)输入数据长度和搜索频率应进行限制,防止恶意搜索导致的数据库拒绝服务
2) Mybatis 执行 in 语句的安全编码
脆弱代码:
在对同条件多值查询的时候,如当用户输入1001,1002,1003…100N时,如果考虑安全编码规范问题,其对应的SQL语句如下:
Select * from news where id in (#{id})
但由于这样写程序会报错,研发人员将SQL查询语句修改如下:
Select * from news where id in (${id})
修改SQL语句之后,程序停止报错。但是却引入了SQL语句拼接的问题,极有可能引发SQL注入漏洞。
解决方案:
可使用 Mybatis 自带的 foreach 指令解决 SQL 语句动态拼接的问题
select * from news where id in
<foreach collection="ids" item="item" open="(" separator="," close=")">#{item}</foreach>
3) Mybatis 执行 order by 或 group by 的安全编码
脆弱代码:
当根据发布时间、点击量等信息进行排序的时候,如果考虑安全编码规范问题,其对应的SQL语句如下:
Select * from news where title = '安全' order by #{time} asc
但由于发布时间time不是用户输入的参数,无法使用预编译。研发人员将SQL查询语句修改如下:
Select * from news where title = '安全' order by ${time} asc
修改SQL语句之后,程序停止报错。但是却引入了SQL语句拼接的问题,极有可能引发SQL注入漏洞。
解决方案:
可使用 Mybatis 自带的 choose 指令解决 SQL 语句动态拼接的问题
ORDER BY
<choose>
<when test="orderBy == 1">
id desc
</when>
<when test="orderBy == 2">
date desc
</when>
<otherwise>
time desc
</otherwise>
</choose>
LDAP 注入是指应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在应用程序中事先定义好的 LDAP 语句中添加额外的 LDAP 语句。
脆弱代码:
String username = request.getParameter("username");
// 未对 username 进行校验直接拼接
NamingEnumeration answers = context.search("dc=People,dc=example,dc=com","(uid=" + username + ")", ctrls);
解决方案:
因为 LDAP 没有类似于 SQL 的预编译函数,所以针对 LDAP 注入的主要防御措施是依照 LDAP 数据库字段设计围绕长度、格式、逻辑、特殊字符 4 个维度对每一个输入参数进行安全校验。
脆弱代码:
密码不应保留在源代码中。源代码只能在企业环境中受限的共享,禁止在互联网中共享。 为了安全管理,密码和密钥应存储在单独的加密配置文件或密钥库中。
private String SECRET_PASSWORD = "letMeIn!";
Properties props = new Properties();
props.put(Context.SECURITY_CREDENTIALS, "password");
脆弱代码:
密钥不应保留在源代码中。源代码只能在企业环境中受限的共享,禁止在互联网中共享。 为了安全管理,密码和密钥应存储在单独的加密配置文件或密钥库中。
byte[] key = {1, 2, 3, 4, 5, 6, 7, 8};
SecretKeySpec spec = new SecretKeySpec(key, "AES");
Cipher aes = Cipher.getInstance("AES");
aes.init(Cipher.ENCRYPT_MODE, spec);
return aesCipher.doFinal(secretData);
空的 TrustManager 通常用于实现直接连接到未经根证书颁发机构签名的主机。同时,如果客户端将信任所有的证书会导致应用程序很容易受到中间人攻击。
脆弱代码:
class TrustAllManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
//Trust any client connecting (no certificate validation)
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
//Trust any remote server (no certificate validation)
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
解决方案:
KeyStore ks = //加载包含受信任证书的密钥库
SSLContext sc = SSLContext.getInstance("TLS");
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ks);
sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(),null);
应该构建允许特定证书(例如基于信任库)的 TrustManager,并创建通配符证书,保证可以在多个子域上重用。
由于许多主机上都重复使用了证书,因此很多开发人员编码 HostnameVerifier 时经常接受任何主机的请求。但是这很容易受到中间人攻击,因为客户端将信任所有证书。
脆弱代码:
public class AllHosts implements HostnameVerifier {
public boolean verify(final String hostname, final SSLSession session) {
return true;
}
}
解决方案:
KeyStore ks = //加载包含受信任证书的密钥库
SSLContext sc = SSLContext.getInstance("TLS");
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ks);
sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
应该构建允许特定证书(例如基于信任库)的 TrustManager,并创建通配符证书,保证可以在多个子域上重用。
1)使用 webpack-obfuscator 混淆js代码时,不论混淆级别多少,都应设定以下参数为启用状态:
// 限制使用开发者工具的控制台选项卡
debugProtection: true
// 使用间隔强制调试模式,从而更难使用“开发人员工具”的其他功能。
debugProtectionInterval: true
// 禁用console.log,console.info,console.error和console.warn 阻碍攻击者调试
disableConsoleOutput: true
// 压缩,无换行
compact: true
// 混淆后的代码,不能使用代码美化,需要配置 cpmpat:true;
selfDefending: true
2)使用js调试探测器 https://www.npmjs.com/package/devtools-detector
当探测到调试器时,应限制攻击者继续渗透,例如:清除cookie或token、执行死循环、破坏逻辑等等
3)productionSourceMap应设置为false关闭调试,前端部署禁止在生产环境开启调试模式。
合规的双向加密数据的传输方案:
1)后端生成非对称算法(国密SM2、RSA2048)的公钥B1、私钥B2,前端访问后端获取公钥B1。
2)前端每次发送请求前,随机生成对称算法(国密SM4、AES256)的密钥A1。
3)公钥、私钥可以全系统固定为一对,前端可以储存公钥,但私钥不能保存在后端数据库中。
4)前端用步骤2的密钥A1加密所有业务数据生成en_data,用步骤1获取的公钥B1加密密钥A1生成en_key。
5)前端用哈希算法对en_data + en_key的值形成一个校验值check_hash。
6)前端将en_data、en_key、check_hash三个参数包装在同一个http数据包中发送到后端。
7)后端获取三个参数后先判断哈希值check_hash是否匹配en_data + en_key以验证完整性。
8)后端用私钥B2解密en_key获取本次请求的对称算法的密钥A1。
9)后端使用步骤8获取的密钥A1解密en_data获取实际业务数据。
10)后端处理完业务逻辑后,将需要返回的信息使用密钥A1进行加密后回传给前端。
11)加密数据回传给前端后,前端使用A1对加密的数据进行解密获得返回的信息。
12)步骤2随机生成的密钥A1已经使用完毕,前端应将其销毁。
当在某些安全性极为关键的上下文中使用可预测的随机值时,可能会导致漏洞。
例如,当该值用作:
脆弱代码:
String generateSecretToken() {
Random r = new Random();
return Long.toHexString(r.nextLong());
}
解决方案:
import org.apache.commons.codec.binary.Hex;
String generateSecretToken() {
SecureRandom secRandom = new SecureRandom();
byte[] result = new byte[32];
secRandom.nextBytes(result);
return Hex.encodeHexString(result);
}
使用随机性更高的 java.security.SecureRandom 替换 java.util.Random
将包含哈希签名的字节数组转换为人类可读的字符串时,如果逐字节读取该数组,则可能会发生转换错误。
脆弱代码:
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] resultBytes = md.digest(password.getBytes("UTF-8"));
StringBuilder stringBuilder = new StringBuilder();
for(byte b :resultBytes) {
stringBuilder.append( Integer.toHexString( b & 0xFF ) );
}
return stringBuilder.toString();
以上代码当 resultBytes 等于 “0x0679” 和 “0x6709” 都将输出为 “679”
解决方案:
stringBuilder.append(String.format("%02X", b));
所有对于数据格式化的操作应优先使用规范的数据格式化处理机制。
cookie 中的数据如果直接存储具体的明文敏感数据,可以导致攻击者窃取敏感数据。
脆弱代码:
response.addCookie(new Cookie("userAccountID", acctID));
以上代码直接设置 cookie 中 userAccountID 的具体数值,导致攻击者可以窃取 acctID。
解决方案:
cookie 中的数据只能使用标准的哈希值,不能存储具体的数据内容,原则应使用 session 进行存取身份信息。
日志注入攻击是将未经验证的用户输入写到日志文件中,可以允许攻击者伪造日志内容或将恶意内容注入到日志中。
日志伪造的实现原理
如果用户提交 val 的字符串"twenty-one",则会记录以下条目:
INFO: Failed to parse val=twenty-one
如果攻击者提交包含换行符%0d 和%0a 的字符串”twenty-one%0d%0aHACK:+User+logged+in%3dbadguy”,会记录以下条目:
INFO: Failed to parse val=twenty-one
HACK: User logged in=badguy
显然,攻击者可以利用以上手法插入任意日志内容。
脆弱代码:
public void risk(HttpServletRequest request, HttpServletResponse response) {
String val = request.getParameter("val");
try {
int value = Integer.parseInt(val);
out = response.getOutputStream();
}
catch (NumberFormatException e) {
e.printStackTrace(out);
log.info("Failed to parse val = " + val);
}
}
解决方案:
public void risk(HttpServletRequest request, HttpServletResponse response) {
String val = request.getParameter("val");
try {
int value = Integer.parseInt(val);
}
catch (NumberFormatException e) {
val = val.replace("\r", "");
val = val.replace("\n", "");
log.info("Failed to parse val = " + val);
//不要直接 printStackTrace 输出错误日志
}
}
所有写入日志的数据必须去除 \r 和 \n 字符。
攻击者任意构造 HTTP 响应数据并传递给应用程序可以构造:缓存中毒(Cache Poisoning),跨站点脚本(XSS) 和页面劫持(Page Hijacking)等攻击。
HTTP 响应截断的实现原理
下面的代码从 HTTP 请求中读取用户输入的作者姓名,并将其设置为 HTTP 响应的 cookie 头。
String author = request.getParameter(AUTHOR_PARAM);
[...]
Cookie cookie = new Cookie("author", author);
cookie.setMaxAge(cookieExpiration);
response.addCookie(cookie);
假设一个用户名:
Jane Smith
用户在登陆成功后包括 cookie 在内的 HTTP 响应值:
HTTP/1.1 200 OK
Set-Cookie: author=Jane Smith
[...]
如果 cookie 的值校验不严格,当攻击者提交恶意字符串:
Wiley Hacker \r\n Content-Length:999 \r\n \r\n
则 HTTP 响应将被分割成伪造的响应,导致原始响应被忽略掉:
HTTP/1.1 200 OK
Set-Cookie: author=Wiley Hacker
Content-Length: 999
malicious content... (to 999th character in this example)
Original content starting with character 1000, which is now ignored by the web browser...
脆弱代码:
public void risk(HttpServletRequest request, HttpServletResponse response) {
String key = request.getParameter("key");
String value = request.getParameter("value");
response.setHeader(key, value);
}
解决方案 1:
public void fix(HttpServletRequest request, HttpServletResponse response) {
String key = request.getParameter("key");
String value = request.getParameter("value");
key = key.replace("\r", "");
key = key.replace("\n", "");
value = value.replace("\r", "");
value = value.replace("\n", "");
response.setHeader(key, value);
}
解决方案 2:
public void fix(HttpServletRequest request, HttpServletResponse response) {
String key = request.getParameter("key");
String value = request.getParameter("value");
if (Pattern.matches("[0-9A-Za-z]+", key) && Pattern.matches("[0-9A-Za-z]+", value)) {
response.setHeader(key, value);
}
}
修复建议
HTTPS 应使用 TLS 1.2 或 以上版本,旧版本协议存在中间人劫持漏洞。
RequestMapping 默认情况下映射到所有 HTTP 动词,应强制要求只能使用 GET 和 POST。
脆弱代码:
@Controller
public class UnsafeController {
// RequestMapping 默认情况下映射到所有HTTP动词
@RequestMapping("/path")
public void writeData() {
return "";
}
}
解决方案:
// 基于Spring Framework 4.3及更高版本
@Controller
public class SafeController {
// 只接受GET动词,不执行数据修改操作
@GetMapping("/path")
public String readData() {
return "";
}
// 只接受POST动词,执行数据修改操作
@PostMapping("/path")
public void writeData() {
return "";
}
}
使用 GetMapping 和 PostMapping 进行 HTTP 动词限制。
应用程序未限制返回用户侧的数据字段,导致敏感内容泄露。
脆弱代码:
@javax.persistence.Entity
class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
[...]
}
@Controller
class UserController {
// 在查询字符串中关联业务逻辑是不规范的
@RequestMapping("/user/{id}")
public UserEntity getUser(@PathVariable("id") String id) {
// 返回用户所有字段内容,可能导致 password home_address 等敏感字段泄露
return userService.findById(id).get();
}
}
解决方案 1:
禁止在 SQL 语句中使用 select * from
语句,应只查询必需的数据库字段以兼顾性能和安全性。
解决方案 2:
使用 jackson 注解 @JsonIgnore 限制数据执行序列化。
@Controller
class UserController {
// 参数业务应使用POST传递
@PostMapping("/user")
public UserEntity getUser(@RequestBody JSONObject requestJson) {
// 敏感字段 password 已受限
return userService.findById(requestJson).get();
}
}
class UserEntity {
@Id
private Long id;
private String username;
@JsonIgnore
private String password;
}
解决方案 3:
使用 jackson 注解 @JsonIgnoreProperties 限制数据执行序列化。
@Controller
class UserController {
// 参数业务应使用POST传递
@PostMapping("/user")
public UserEntity getUser(@RequestBody JSONObject requestJson) {
// 敏感字段 password secretKey 已受限
return userService.findById(requestJson).get();
}
}
@JsonIgnoreProperties({"password","secretKey"})
class UserEntity {
@Id
private Long id;
private String username;
private String password;
private String secretKey;
}
SpringBoot Actuator 如果不进行任何安全限制直接对外暴露访问接口,可导致敏感信息泄露甚至恶意命令执行。
解决方案:
// 参考版本 springboot 2.3.2
// pom.xml 配置参考
<!-- 引入 actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 引入 spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
// application.properties 配置参考
#路径映射
management.endpoints.web.base-path=/sec_coding
#允许访问的ip列表
management.access.iplist = 127.0.0.1,192.168.1.100,192.168.2.3/24,192.168.1.6
#指定端口,应保证监控信息端口与应用业务端口不同
management.server.port=8081
#关闭默认打开的endpoint
management.endpoints.enabled-by-default=false
#需要访问的endpoint在这里打开
management.endpoint.info.enabled=true
management.endpoint.health.enabled=true
management.endpoint.env.enabled=true
management.endpoint.metrics.enabled=true
management.endpoint.mappings.enabled=true
#sessions需要spring-session包的支持
#management.endpoint.sessions.enabled=true
#允许查询所有列出的endpoint
management.endpoints.web.exposure.include=info,health,env,metrics,mappings
#显示所有健康状态
management.endpoint.health.show-details=always
参考链接:
Swagger 如果不进行任何安全限制直接对外暴露端访问路径,可导致敏感接口以及接口的参数泄露。
解决方案:
// 测试环境配置文件 application.properties 中
swagger.enable=true
// 生产环境配置文件 application.properties 中
swagger.enable=false
// java代码中变量 swaggerEnable 通过读取配置文件设置swagger开关
@Configuration
public class Swagger {
@Value("${swagger.enable}")
private boolean swaggerEnable;
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
// 变量 swaggerEnable 控制是否开启 swagger
.enable(swaggerEnable)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.tao.springboot.action"))
//controller路径
.paths(PathSelectors.any())
.build();
}
如果 Cookie 缺失 HttpOnly 标志,攻击者可以利用 XSS 攻击窃取用户的 Cookie。
脆弱代码:
Cookie cookie = new Cookie("email",userName);
response.addCookie(cookie);
解决方案:
Cookie cookie = new Cookie("email",userName);
cookie.setSecure(true);
cookie.setHttpOnly(true); //开启HttpOnly
强制保持 cookie 开启 HttpOnly 以保护用户鉴权。
所有涉及跨域的请求如不进行限制,可以导致攻击者窃取用户敏感信息。
脆弱代码:
response.addHeader("Access-Control-Allow-Origin", "*");
解决方案:
Access-Control-Allow-Origin 字段的值应依照部署情况进行白名单限制。
Cookie 的如果不限制过期时间,可能导致攻击者窃取 Cookie 后执行越权操作。
脆弱代码:
Cookie cookie = new Cookie("email", email);
cookie.setMaxAge(60*60*24*365); // 设置一年的cookie有效期
解决方案:
在未指定广播者权限的情况下注册的接收者将接收来自任何广播者的消息。如果这些消息包含恶意数据或来自恶意广播者,可能会对应用程序造成危害。应禁止 app 应用无条件接受广播。
脆弱代码:
Intent i = new Intent();
i.setAction("com.insecure.action.UserConnected");
i.putExtra("username", user);
i.putExtra("email", email);
i.putExtra("session", newSessionId);
this.sendBroadcast(v1);
解决方案:
配置(接收器)
<manifest>
<!-- 权限宣告 -->
<permission android:name="my.app.PERMISSION" />
<receiver
android:name="my.app.BroadcastReceiver"
android:permission="my.app.PERMISSION"> <!-- 权限执行 -->
<intent-filter>
<action android:name="com.secure.action.UserConnected" />
</intent-filter>
</receiver>
</manifest>
配置(发送方)
<manifest>
<!-- 声明拥有上述接收器发送广播的许可 -->
<uses-permission android:name="my.app.PERMISSION"/>
<!-- 使用以下配置,发送方和接收方应用程序都需要由同一个开发人员证书签名 -->
<permission android:name="my.app.PERMISSION" android:protectionLevel="signature"/>
</manifest>
或者禁止响应一切外部广播
<manifest>
<!-- 权限宣告 -->
<permission android:name="my.app.PERMISSION" android:exported="false" />
<receiver
android:name="my.app.BroadcastReceiver"
android:permission="my.app.PERMISSION">
<intent-filter>
<action android:name="com.secure.action.UserConnected" />
</intent-filter>
</receiver>
</manifest>
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。