首页  编辑  

Fortify扫描整改方法

Tags: /Java/   Date Created:
Fortify扫描的问题和整改方法
✔ 的表示验证有效
问题说明整改方法
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对象,直接用数据库的实体类来作为控制器的数据交互对象。这可能导致两个问题:

  1. 无意泄露不需要的数据给前端,例如查询对象返回了整个数据库的内部字段信息
  2.  可能无意中更新了不想更新的某些数据库的字段。例如本来只想更新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的方法消除告警:

  1. <dependency>
  2.          <groupId>org.springframework.boot</groupId>
  3.          <artifactId>spring-boot-starter-data-ldap</artifactId>
  4. </dependency>
  5. <dependency>
  6.          <groupId>org.apache.directory.api</groupId>
  7.          <artifactId>api-ldap-model</artifactId>
  8.          <version>2.0.0</version>
  9. </dependency>


  1. import org.apache.directory.api.ldap.model.url.LdapUrl;
  2. import org.springframework.ldap.filter.AndFilter;
  3. import org.springframework.ldap.filter.EqualsFilter;
  4. import org.springframework.ldap.filter.NotFilter;
  5.  
  6. Sting ldapUrl = "ldap://host/DC=1,DC=2,DC=3,DC=4";
  7. LdapUrl ldapUrl = new LdapUrl(ldapUrl);
  8.  
  9. Hashtable<String, String> ldapEnv = new Hashtable<>();
  10. ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
  11. ldapEnv.put(Context.PROVIDER_URL, ldapUrl.getScheme() + ldapUrl.getHost());
  12.  
  13. AndFilter filter = new AndFilter();
  14. filter.and(new EqualsFilter("objectclass""user"));
  15. filter.and(new EqualsFilter("sAMAccountName", StringTools.cleanString(userId)));
  16. filter.and(new NotFilter(new EqualsFilter("UserAccountControl:1.2.840.113556.1.4.803:""2")));
  17. String searchFilter = filter.encode();
  18.                            
  19. NamingEnumeration<SearchResult> namingEnum = ctx.search(ldapUrl.getDn().toString(),
  20. searchFilter, searchControls);
​Header Manipulation​HTTP请求的头,未对输入数据进行处理,可能导致跨站脚本攻击、页面劫持、缓存投毒、页面转向、Cookie操纵等
示例代码:
  1.   public <T> T doPost(String url, Map<StringObject> params, 
  2. Map<StringObject> heads, Class<T> responseClass) {
  3.     org.springframework.http.HttpHeaders headers = new HttpHeaders();
  4.     for (Map.Entry<StringObject> head : heads.entrySet()) {
  5.       headers.add(head.getKey(), String.valueOf(head.getValue()));
  6.     }
  7.     headers.setContentType(MediaType.APPLICATION_JSON);
  8.     try {
  9.       String jsonStr = JSON.toJSONString(params);
  10.       HttpEntity<String> requestEntity = new HttpEntity<>(jsonStr, headers);
  11.       ResponseEntity<String> response = restTemplate.exchange(url, 
  12. HttpMethod.POST, requestEntity, String.class);
  13.       return parseObject(response.getBody(), responseClass);
  14.     } catch (Exception e) {
  15.       return null;
  16.     }
  17.   }
尽量使用自己的值,而不是用户输入的值,例如用户传递filename,那么设置Header的时候,最好file = new File(filename)​,然后使用 file.getName(),而不是直接使用filename。
另外可以对输入的数据进行处理:
org.apache.commons.lang3.StringUtils.normalizeSpace(head.getValue())    

​SQL Injection未对用户输入做处理,直接使用用户前端输入的数据作为字符串拼接SQL,导致​SQL注入风险。例如下面:
  1. "    ON a.user_role_id = c.role_id" +
  2. "    AND c.delete_yn = 'N' " +
  3. "    WHERE" +
  4. "    a.delete_yn = 'N'" +
  5. "    %s " +
  6. ") a ";
  7. jdbcTemplate.execute(
  8.  con -> {
  9.   String storedProc = "{call " + propertiesConfig.getDefaultSchema() 
  10. ".cc_covid_ward_view_setting_save(?,?,?,?,?,?,?)}";
  11.   CallableStatement cs = con.prepareCall(storedProc);
  12.   cs.setString(1, hosp);
  13.   cs.setString(2, dto.getWardLocation());
  14.   cs.setString(3, dto.getGeneralWard() ? TRUE : FALSE);
  15.   cs.setString(4, dto.getWardViewText());
  16.   cs.setString(5, dto.getWardViewColor());
  17.   cs.setString(6, dto.getCovidLisResultApi() ? TRUE : FALSE);
  18.   cs.setString(7, userName);
  19.   return cs;
  20.  }, (CallableStatementCallback) cs -> {
  21.   cs.execute();
  22.   return null;
  23.  });
永远不要直接使用用户输入来拼接SQL字符串
可以使用prepareStatement来处理SQL查询,并使用参数化来填充用户输入的内容。例如:
  1. import java.sql.Connection;
  2. import java.sql.DriverManager;
  3. import java.sql.PreparedStatement;
  4. import java.sql.SQLException;
  5. public class SQLInjectionPrevention {
  6.     public static void main(String[] args) {
  7.         // 连接数据库
  8.         String jdbcUrl = "jdbc:mysql://your_database_url";
  9.         String username = "your_username";
  10.         String password = "your_password";
  11.         try (Connection connection = DriverManager.getConnection(jdbcUrl, username, password)) {
  12.             // 要执行的SQL查询
  13.             String usernameInput = "user123";
  14.             String passwordInput = "password123";
  15.             // 使用PreparedStatement,其中"?"是占位符
  16.             String sqlQuery = "SELECT * FROM users WHERE username = ? AND password = ?";
  17.             try (PreparedStatement preparedStatement = connection.prepareStatement(sqlQuery)) {
  18.                 // 设置参数值,避免直接拼接字符串
  19.                 preparedStatement.setString(1, usernameInput);
  20.                 preparedStatement.setString(2, passwordInput);
  21.                 // 执行查询
  22.                 // 这里可以根据需要执行executeQuery()或executeUpdate()等方法
  23.             } catch (SQLException e) {
  24.                 e.printStackTrace();
  25.             }
  26.         } catch (SQLException e) {
  27.             e.printStackTrace();
  28.         }
  29.     }
  30. }
​Access Specifier Manipulation​反射操作导致数据对象直接被存取可能导致绕过该字段的存取控制(setXxx和getXxx),从而导致数据验证和处理失效。
  1. for (Field field : fieldList) {
  2.   ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
  3.   int columnIndex = annotation.index();
  4.   field.setAccessible(true);
  5. }
​整改方法,使用ReflectionUtils.makeAccessible替代field.setAccessible:
org.springframework.util.ReflectionUtils.makeAccessible(field);
​Server-Side Request Forgery
​直接使用用户输入来进行第三方请求,这可能导致用户输入恶意的网址,例如内部敏感信息文件网址,从而导致网络安全控制失效。因为用户提交恶意网址后,请求是在服务器内部网络上请求的,可能绕过了防火墙或者安全策略了。例如下面代码中,url直接来自于用户输入。
  1. public Map<String, byte[]> downLoadFile(String url) {
  2.  ResponseEntity<byte[]> responseEntity = restTemplate.getForEntity(url, byte[].class);
  3.  Map<String, byte[]> result = new HashMap<>();
  4.  result.put(responseEntity.getHeaders().getContentDisposition().getFilename(), responseEntity.getBody());
  5.  return result;
  6. }
​对用户的输入进行审查,确保用户输入的URL是符合安全要求的:

  1. import org.springframework.web.util.UriComponents;
  2. import org.springframework.web.util.UriComponentsBuilder;
  3. public Map<String, byte[]> downLoadFile(String url) throws Exception{     
  4.  Map<String, byte[]> result = new HashMap<>();
  5.  URL urls = new URL(url);
  6.  // 这里检查 urls 的各个协议、参数、hosts、path等的合法性
  7.  UriComponents uriComponents = UriComponentsBuilder.newInstance()                      
  8.     .scheme(urls.getProtocol()).port(urls.getPort()).host(urls.getHost())
  9.     .path(urls.getPath()).query(urls.getQuery()).build();
  10.  ResponseEntity<byte[]> responseEntity = restTemplate.getForEntity(uriComponents.toString(), byte[].class);
  11.  result.put(responseEntity.getHeaders().getContentDisposition().getFilename(), responseEntity.getBody());
  12.  return result;
  13. }
​XML External Entity Injection使用用户的输入直接拼接导致​XML注入风险。
  1. DocumentBuilderFactory df = DocumentBuilderFactory.newInstance();
  2. df.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); // Compliant
  3. df.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); // compliant
  4. DocumentBuilder builder = df.newDocumentBuilder();
  5. Document doc = builder.parse(new ByteArrayInputStream(accessKeyXml.getBytes()));
上面代码使用了accessKeyXml直接作为输入,若用户提供下面的xml(xxe攻击),将会导致系统崩溃,因为XML处理器会用 /dev/random 即随机内容填充实体:
  1. <?xml version="1.0" encoding="ISO-8859-1"?>
  2.  <!DOCTYPE foo [
  3.   <!ELEMENT foo ANY >
  4.   <!ENTITY xxe SYSTEM "file:///dev/random" >]><foo>&xxe;</foo>

​参考解决方案:
  1. JAXBContext context = JAXBContext.newInstance(clazz);
  2. XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
  3. xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
  4. xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, true);
  5. XMLStreamReader xsr = xmlInputFactory.createXMLStreamReader(new StringReader(xml));
  6. Unmarshaller unmarshaller = context.createUnmarshaller();
  7. return (T) unmarshaller.unmarshal(xsr);


附: cleanString函数


  1. public static String cleanString(String aString) {
  2.  if (aString == nullreturn null;
  3.  
  4.  StringBuilder cleanString = new StringBuilder();
  5.  for (int i = 0; i < aString.length(); ++i) {
  6.   cleanString.append(cleanChar(aString.charAt(i)));
  7.  }
  8.  
  9.  return cleanString.toString();
  10. }
  11. private static char cleanChar(char aChar) {
  12.  // 0 - 9
  13.  for (int i = 48; i < 58; ++i) {
  14.   if (aChar == i) return (char) i;
  15.  }
  16.  // 'A' - 'Z'
  17.  for (int i = 65; i < 91; ++i) {
  18.   if (aChar == i) return (char) i;
  19.  }
  20.  // 'a' - 'z'
  21.  for (int i = 97; i < 123; ++i) {
  22.   if (aChar == i) return (char) i;
  23.  }
  24.  // other valid characters
  25.  switch (aChar) {
  26.  case '/':
  27.   return '/';
  28.  case '\\':
  29.   return '\\';
  30.  case ':':
  31.   return ':';
  32.  case '.':
  33.   return '.';
  34.  case '-':
  35.   return '-';
  36.  case '_':
  37.   return '_';
  38.  case ' ':
  39.   return ' ';
  40.  default:
  41.   return '%';
  42.  }
  43. }