问题 | 说明 | 整改方法 |
Path Manipulation | 使用了用户输入的文件名,但用户可能输入../../config.xml 类似导致访问非法的文件,示例:
@RequestMapping
public Object getFile(String filename) {
InputStream stream = new FileInputStream("/app/data/" + filename);
// ...
}
红色表示报错行,下同
| 对用户输入的文件名,必须进行白名单或者黑名单处理。
例如尽量让前端用户在列表内选择文件名,并在后端验证白名单列表。
如果实在无法用白名单,那必须验证文件名是否有非法的字符(黑名单)。
参考: java - 如何解决 fortify 给出的路径操作错误?对文件名进行处理:
cleanString(filename); // cleanString 函数见下面代码 ✔ |
Cross-Site Scripting: Persistent | 从非可信任区域(如数据库)返回数据存在跨站攻击弱点(XSS, Cross-site scripting)。 示例:
@GetMapping("/getTrainingPortalLink")
public ResponseEntity<String> getTrainingPortalLink() {
String trainingPortalUrl = systemParameterService.getSystemParameter("trainingPortalUrl");
return ResponseEntity.ok(trainingPortalUrl);
}
| pom.xml增加:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
</dependency> 对返回数据转义处理:
StringEscapeUtils.escapeHtml4(trainingPortalUrl) |
Mass Assignment: Insecure Binder Configuration
| 没有区分DTO和DAO对象,直接用数据库的实体类来作为控制器的数据交互对象。这可能导致两个问题: - 无意泄露不需要的数据给前端,例如查询对象返回了整个数据库的内部字段信息
- 可能无意中更新了不想更新的某些数据库的字段。例如本来只想更新name字段,但是前端可能传输了name和addr,导致addr也无意中更新
示例: @PostMapping("/addxxxx")
public ResponseEntity<Boolean> addxxxx(Authentication authentication,
@RequestBody AddCcCovidWardViewSettingDto dto) {
// 数据库操作 dto
} | 方法一: 定义单独的DTO对象,类型和属性等与DAO对象相同,专门用于传输。此方法比较繁琐,会重复一份代码出来。
方法二(推荐),✔:
在相应的Controller类上,增加 @InitBinder 忽略敏感字段。
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.setDisallowedFields(new String[]{"field1", "password", "detail.phoneNumber", ...});
} |
Password Management: Hardcoded Password | 明文密码或者硬编码密码和用户名,因为用户可能更改服务器连接,所以永远不要硬编码数据库连接信息。示例:
spring:
datasource:
url: jdbc:oracle:xyz:@www.server.com:12345:aaaa
username: test
password: test123 | 改用环境变量。HA的Docker容器中可以定义环境变量,会有相关help/configMap的设置,使用相应的环境变量替代。整改示例:
spring:
datasource:
url: ${ha.datasource.url}
username: ${ha.datasource.username}
password: ${ha.datasource.pass} |
LDAP Injection | 存在LDAP注入风险,由于用户输入用户名和密码,可以输入一些特殊字符来注入LDAP查询,从而可以绕过密码验证或者泄露账户数据甚至恶意更新AD数据。例如:
Sting ldapUrl = "ldap://host/DC=1,DC=2,DC=3,DC=4";
Hashtable<String, String> ldapEnv = new Hashtable<>();
ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
ldapEnv.put(Context.PROVIDER_URL, ldapUrl.substring(0, ldapUrl.indexOf("DC=")));
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
String searchFilter = "(sAMAccountName=" + userId + ")";
NamingEnumeration<SearchResult> namingEnum = ctx.search(ldapDcName, searchFilter, searchControls); | Spring可以使用org.springframework.ldap.support.LdapEncoder来编码filter。
String searchFilter = "(sAMAccountName=" + userId + ")";
searchFilter = org.springframework.ldap.support.LdapEncoder.filterEncode(searchFilter); 当然你也可以自己来转义特殊字符,对用户的输入先转义再拼接:public static final String escapeLDAPSearchFilter(String filter) {
StringBuffer sb = new StringBuffer(); // If using JDK >= 1.5 consider using StringBuilder
for (int i = 0; i < filter.length(); i++) {
char curChar = filter.charAt(i);
switch (curChar) {
case '\\':
sb.append("\\5c");
break;
case '*':
sb.append("\\2a");
break;
case '(':
sb.append("\\28");
break;
case ')':
sb.append("\\29");
break;
case '\u0000':
sb.append("\\00");
break;
default:
sb.append(curChar);
}
}
return sb.toString();
}
String searchFilter = "(sAMAccountName=" + escapeLDAPSearchFilter(userId) + ")"; 你也可以使用参数化来避免问题(推荐),例如:
// Perform the search
NamingEnumeration answer = ctx.search("ou=NewHires",
"(&(mySpecialKey={0}) (cn=*{1}))", // Filter expression
new Object[]{key, name}, // Filter arguments
null); 也可以用cleanString的方法消除告警: - <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-ldap</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.directory.api</groupId>
- <artifactId>api-ldap-model</artifactId>
- <version>2.0.0</version>
- </dependency>
- import org.apache.directory.api.ldap.model.url.LdapUrl;
- import org.springframework.ldap.filter.AndFilter;
- import org.springframework.ldap.filter.EqualsFilter;
- import org.springframework.ldap.filter.NotFilter;
-
- Sting ldapUrl = "ldap://host/DC=1,DC=2,DC=3,DC=4";
- LdapUrl ldapUrl = new LdapUrl(ldapUrl);
-
- Hashtable<String, String> ldapEnv = new Hashtable<>();
- ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
- ldapEnv.put(Context.PROVIDER_URL, ldapUrl.getScheme() + ldapUrl.getHost());
-
- AndFilter filter = new AndFilter();
- filter.and(new EqualsFilter("objectclass", "user"));
- filter.and(new EqualsFilter("sAMAccountName", StringTools.cleanString(userId)));
- filter.and(new NotFilter(new EqualsFilter("UserAccountControl:1.2.840.113556.1.4.803:", "2")));
- String searchFilter = filter.encode();
-
- NamingEnumeration<SearchResult> namingEnum = ctx.search(ldapUrl.getDn().toString(),
- searchFilter, searchControls);
|
Header Manipulation | HTTP请求的头,未对输入数据进行处理,可能导致跨站脚本攻击、页面劫持、缓存投毒、页面转向、Cookie操纵等 示例代码:- public <T> T doPost(String url, Map<String, Object> params,
- Map<String, Object> heads, Class<T> responseClass) {
- org.springframework.http.HttpHeaders headers = new HttpHeaders();
- for (Map.Entry<String, Object> head : heads.entrySet()) {
- headers.add(head.getKey(), String.valueOf(head.getValue()));
- }
- headers.setContentType(MediaType.APPLICATION_JSON);
- try {
- String jsonStr = JSON.toJSONString(params);
- HttpEntity<String> requestEntity = new HttpEntity<>(jsonStr, headers);
- ResponseEntity<String> response = restTemplate.exchange(url,
- HttpMethod.POST, requestEntity, String.class);
- return parseObject(response.getBody(), responseClass);
- } catch (Exception e) {
- return null;
- }
- }
| 尽量使用自己的值,而不是用户输入的值,例如用户传递filename,那么设置Header的时候,最好file = new File(filename),然后使用 file.getName(),而不是直接使用filename。 另外可以对输入的数据进行处理: org.apache.commons.lang3.StringUtils.normalizeSpace(head.getValue()) ✔
|
SQL Injection | 未对用户输入做处理,直接使用用户前端输入的数据作为字符串拼接SQL,导致SQL注入风险。例如下面:
- " ON a.user_role_id = c.role_id" +
- " AND c.delete_yn = 'N' " +
- " WHERE" +
- " a.delete_yn = 'N'" +
- " %s " +
- ") a ";
- jdbcTemplate.execute(
- con -> {
- String storedProc = "{call " + propertiesConfig.getDefaultSchema()
- + ".cc_covid_ward_view_setting_save(?,?,?,?,?,?,?)}";
- CallableStatement cs = con.prepareCall(storedProc);
- cs.setString(1, hosp);
- cs.setString(2, dto.getWardLocation());
- cs.setString(3, dto.getGeneralWard() ? TRUE : FALSE);
- cs.setString(4, dto.getWardViewText());
- cs.setString(5, dto.getWardViewColor());
- cs.setString(6, dto.getCovidLisResultApi() ? TRUE : FALSE);
- cs.setString(7, userName);
- return cs;
- }, (CallableStatementCallback) cs -> {
- cs.execute();
- return null;
- });
| 永远不要直接使用用户输入来拼接SQL字符串! 可以使用prepareStatement来处理SQL查询,并使用参数化来填充用户输入的内容。例如:
- import java.sql.Connection;
- import java.sql.DriverManager;
- import java.sql.PreparedStatement;
- import java.sql.SQLException;
- public class SQLInjectionPrevention {
- public static void main(String[] args) {
-
- String jdbcUrl = "jdbc:mysql://your_database_url";
- String username = "your_username";
- String password = "your_password";
- try (Connection connection = DriverManager.getConnection(jdbcUrl, username, password)) {
-
- String usernameInput = "user123";
- String passwordInput = "password123";
-
- String sqlQuery = "SELECT * FROM users WHERE username = ? AND password = ?";
- try (PreparedStatement preparedStatement = connection.prepareStatement(sqlQuery)) {
-
- preparedStatement.setString(1, usernameInput);
- preparedStatement.setString(2, passwordInput);
-
-
- } catch (SQLException e) {
- e.printStackTrace();
- }
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }
|
Access Specifier Manipulation | 反射操作导致数据对象直接被存取可能导致绕过该字段的存取控制(setXxx和getXxx),从而导致数据验证和处理失效。- for (Field field : fieldList) {
- ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
- int columnIndex = annotation.index();
- field.setAccessible(true);
- }
| 整改方法,使用ReflectionUtils.makeAccessible替代field.setAccessible:
org.springframework.util.ReflectionUtils.makeAccessible(field);
|
Server-Side Request Forgery
| 直接使用用户输入来进行第三方请求,这可能导致用户输入恶意的网址,例如内部敏感信息文件网址,从而导致网络安全控制失效。因为用户提交恶意网址后,请求是在服务器内部网络上请求的,可能绕过了防火墙或者安全策略了。例如下面代码中,url直接来自于用户输入。
- public Map<String, byte[]> downLoadFile(String url) {
- ResponseEntity<byte[]> responseEntity = restTemplate.getForEntity(url, byte[].class);
- Map<String, byte[]> result = new HashMap<>();
- result.put(responseEntity.getHeaders().getContentDisposition().getFilename(), responseEntity.getBody());
- return result;
- }
| 对用户的输入进行审查,确保用户输入的URL是符合安全要求的:
- import org.springframework.web.util.UriComponents;
- import org.springframework.web.util.UriComponentsBuilder;
- public Map<String, byte[]> downLoadFile(String url) throws Exception{
- Map<String, byte[]> result = new HashMap<>();
- URL urls = new URL(url);
-
- UriComponents uriComponents = UriComponentsBuilder.newInstance()
- .scheme(urls.getProtocol()).port(urls.getPort()).host(urls.getHost())
- .path(urls.getPath()).query(urls.getQuery()).build();
- ResponseEntity<byte[]> responseEntity = restTemplate.getForEntity(uriComponents.toString(), byte[].class);
- result.put(responseEntity.getHeaders().getContentDisposition().getFilename(), responseEntity.getBody());
- return result;
- }
|
XML External Entity Injection | 使用用户的输入直接拼接导致XML注入风险。
- DocumentBuilderFactory df = DocumentBuilderFactory.newInstance();
- df.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
- df.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
- DocumentBuilder builder = df.newDocumentBuilder();
- Document doc = builder.parse(new ByteArrayInputStream(accessKeyXml.getBytes()));
上面代码使用了accessKeyXml直接作为输入,若用户提供下面的xml(xxe攻击),将会导致系统崩溃,因为XML处理器会用 /dev/random 即随机内容填充实体:
- <?xml version="1.0" encoding="ISO-8859-1"?>
- <!DOCTYPE foo [
- <!ELEMENT foo ANY >
- <!ENTITY xxe SYSTEM "file:///dev/random" >]><foo>&xxe;</foo>
| 参考解决方案:
- JAXBContext context = JAXBContext.newInstance(clazz);
- XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
- xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
- xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, true);
- XMLStreamReader xsr = xmlInputFactory.createXMLStreamReader(new StringReader(xml));
- Unmarshaller unmarshaller = context.createUnmarshaller();
- return (T) unmarshaller.unmarshal(xsr);
|
| | |