AOP技術應用于安全防御與漏洞修復
關于AOP技術
AOP(Aspect-Oriented Programming)面向切面編程。切面是什么?切面表示從業務邏輯分離出來的橫切邏輯,比如性能監控、日志記錄、權限控制,這些功能可從核心邏輯代碼中抽離出去。也就是說AOP可以解決代碼耦合問題,讓職責更加單一。
這里要講的是利用AOP技術解決代碼安全問題。把安全代碼從業務邏輯中分離出來,讓其單一的解決安全問題。
優勢:不影響業務代碼,修復安全漏洞的時候,可以對原代碼很好的繼承,不需修改原代碼(基于配置定義切點的時候)。
將AOP技術應用于安全技術的案例很少,網上很難找到較多示例代碼。
關于ESAPI
OWASP Enterprise Security API (ESAPI)
ESAPI (OWASP企業安全應用程序接口)是一個免費、開源的、網頁應用程序安全控件庫,它使程序員能夠更容易寫出更低風險的程序。ESAPI接口庫被設計來使程序員能夠更容易的在現有的程序中引入安全因素。ESAPI庫也可以成為作為新程序開發的基礎。
通俗點說ESAPI是OWASP提供的一個安全開發API庫。
優勢:對比自己開發的安全處理代碼更成熟穩定。
ESAPI具體的使用實際也是比較少,網上也很難找到較多示例代碼。開發過程比較困難點
AOP 技術做 XSS 防御
OWASP提供了幾種XSS防御的方法,包括阻止非信任的數據插入,escaping HTML輸入、escaping attribute、escaping JavaScript、以及escaping幾種其他類型。ESAPI提供了多種encoding庫。另外也可以采用白名單validate方法(比如字母數字),也可以自定義正則表達式。
AOP技術實現ESAPI提供的encoding和validation庫,來做XSS攻擊的防御工作。
ESAPI提供的幾種encoding方法:
- 將用戶數據輸出到html body某處時,須經過html轉義。ESAPI.encoder().encodeForHTML( request.getParameter( “input” ) )
- 將用戶數據輸出到html標簽的屬性時,須經過標簽屬性的轉義。ESAPI.encoder().encodeForHTMLAttribute( request.getParameter( “input” ) )
- 將用戶數據輸出到JavaScript數據域時,須經過JavaScript轉義。ESAPI.encoder().encodeForJavaScript( request.getParameter( “input” ) )
- 將用戶數據輸出到URL的參數時,須經過URL轉義。ESAPI.encoder().encodeForURL( request.getParameter( “input” ) )
方案邏輯
實現
定義切點的注解
在需要進行XSS防御的方法前使用相應的注解。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface XSSAllTag { }<br />
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface XSSBeanTag { }<br />
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface XSSStringTag { }<br />
定義切面類
攔截指定方法,遍歷String類型參數,和數據模型(JAVABean)中的String屬性,并對其進行XSS validate。
@Aspect @Component public class SecXSSAspect { @SuppressWarnings("unchecked") @Around(value = "@annotation(com.tony.security.aspectbox.XSSAllTag)") public Object xssAllAdvice(ProceedingJoinPoint pjp) throws Throwable { // 獲取切入方法的參數 Object[] args = pjp.getArgs(); // 獲取切入方法 Method method = ((MethodSignature) pjp.getSignature()).getMethod(); // 獲取參數的類型 Class<?>[] paramTypes = method.getParameterTypes(); // 獲取參數名 ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); String[] parameterNames = parameterNameDiscoverer.getParameterNames(method); for (int i = 0; i < paramTypes.length; i++) { System.out.println("parameterName" + i + ": " + parameterNames[i]); if (paramTypes[i].equals(String.class)) { if (args[i] != null && args[i] != "" && !parameterNames[i].equalsIgnoreCase("password")) { args[i] = XSSValidation.escapeHTML((String) args[i]); } } else if (!GeneralHelper.isSimpleType(paramTypes[i]) && !GeneralHelper.isSpringType(paramTypes[i])) { if (args[i] != null) { Class<?> class1 = args[i].getClass(); Method[] ms = class1.getDeclaredMethods(); for (int j = 0; j < ms.length; j++) { // 遍歷所有返回為String類型的get方法(不包含password字段),獲取并處理get返回值,調用其set方法重寫安全編碼的值 if (ms[j].getName().startsWith("get") && ms[j].getReturnType().equals(String.class)) { if (!ms[j].getName().toLowerCase().contains("password")) { String result = XSSValidation.escapeHTML((String) ms[j].invoke(args[i])); String methodName = ms[j].getName().replace("get", "set"); Method setM = class1.getDeclaredMethod(methodName, String.class); setM.invoke(args[i], result); } } } } } else { continue; } } return pjp.proceed(); } ... }<br />
使用ESAPI庫處理XSS攻擊input
針對XSS攻擊不同location與類別,提供相應的encoding方法。
public abstract class XSSValidation { public static String REGEX_ALPHANUMERIC = "AlphaNumberic"; public static String REGEX_ALPHA = "Alpha"; public static String REGEX_NUMERIC = "Numeric"; public static String REGEX_EMAIL = "Email"; public static String REGEX_ZIP_CODE = "ZipCode"; public static String REGEX_IP_ADDRESS = "IPAddress"; public static String REGEX_SSN = "SSN"; public XSSValidation() { } public static String escapeCustomString(String s, String regex, int maxLength) { Pattern p = ESAPI.securityConfiguration().getValidationPattern(regex); if (p == null) p = Pattern.compile(regex); // Sending pattern directly. try { return ESAPI.validator().getValidInput("CUSTOM_STRING_ESCAPE", s, regex, maxLength, false, false); } catch (Exception e) { e.printStackTrace(); String resultString = ""; Matcher m = p.matcher(s); while (m.find()) { resultString += m.group(0); } return resultString; } } public static String escapeJavaScript(String s) { return ESAPI.encoder().encodeForJavaScript(s); } public static String escapeCSS(String s) { return ESAPI.encoder().encodeForCSS(s); } public static String escapeHTML(String s) { return ESAPI.encoder().encodeForHTML(s); } public static String escapeHTMLAttribute(String s) { return ESAPI.encoder().encodeForHTMLAttribute(s); } public static String escapeURL(String s) throws EncodingException { return ESAPI.encoder().encodeForURL(s); } @SuppressWarnings("finally") public static String validateCreditCard(String s) { try { s = ESAPI.validator().getValidCreditCard("CREDIT_CARD", s, false); } catch (ValidationException e) { s = "VALIDATION FAILED " + s; e.printStackTrace(); } catch (Exception e) { s = "VALIDATION FAILED " + s; e.printStackTrace(); } finally { return s; } } }<br />
判定是否簡單數據類型
public class GeneralHelper { / 簡單數據類型集合 */ public static final Set<Class<?>> SMIPLE_CLASS_SET = new HashSet<Class<?>>(18); / Spring Controller常用數據類型集合 / public static final Set<Class<?>> SPRING_CLASS_SET = new HashSet<Class<?>>(18); static { SMIPLE_CLASS_SET.add(int.class); SMIPLE_CLASS_SET.add(long.class); SMIPLE_CLASS_SET.add(float.class); SMIPLE_CLASS_SET.add(double.class); SMIPLE_CLASS_SET.add(byte.class); SMIPLE_CLASS_SET.add(char.class); SMIPLE_CLASS_SET.add(short.class); SMIPLE_CLASS_SET.add(boolean.class); SMIPLE_CLASS_SET.add(Integer.class); SMIPLE_CLASS_SET.add(Long.class); SMIPLE_CLASS_SET.add(Float.class); SMIPLE_CLASS_SET.add(Double.class); SMIPLE_CLASS_SET.add(Byte.class); SMIPLE_CLASS_SET.add(Character.class); SMIPLE_CLASS_SET.add(Short.class); SMIPLE_CLASS_SET.add(Boolean.class); SMIPLE_CLASS_SET.add(String.class); SMIPLE_CLASS_SET.add(Date.class); } /** 檢查 clazz 是否為簡單數據類型 / public final static boolean isSimpleType(Class<?> clazz) { return SMIPLE_CLASS_SET.contains(clazz); }static { SPRING_CLASS_SET.add(Model.class); SPRING_CLASS_SET.add(ModelAndView.class); SPRING_CLASS_SET.add(HttpSession.class); } /** 檢查 clazz 是否為Spring Controller常用數據類型 */ public final static boolean isSpringType(Class<?> clazz) { return SPRING_CLASS_SET.contains(clazz); }
}<br /></pre>
Controller中使用
在Contoller層使用前面定義的注解,進行XSS攻擊防護。
@XSSAllTag @RequestMapping(value="/user/addUser") public ModelAndView addUser(String flag,@ModelAttribute User user,ModelAndView mv){ if(flag.equals("1")){ mv.setViewName("user/showAddUser");}else{ hrmService.addUser(user); mv.setViewName("redirect:/user/selectUser"); } return mv;
}<br /></pre>
AOP技術做FileUpload防御
方案邏輯
定義注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface FileSuffixTag { }<br />定義切面
@Aspect @Component public class FileUploadAspect { @SuppressWarnings("unchecked") @Around(value = "@annotation(com.tony.security.aspectbox.FileSuffixTag)") public Object fileSuffixAdvice(ProceedingJoinPoint pjp) throws Throwable { boolean result = true; // 獲取切入方法的參數 Object[] args = pjp.getArgs(); // 獲取切入方法 Method method = ((MethodSignature) pjp.getSignature()).getMethod(); // 獲取參數的類型 Class<?>[] paramTypes = method.getParameterTypes(); // 獲取參數名 ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); String[] parameterNames = parameterNameDiscoverer.getParameterNames(method); for (int i = 0; i < paramTypes.length; i++) { System.out.println("parameterName" + i + ": " + parameterNames[i]); if (!GeneralHelper.isSimpleType(paramTypes[i]) && !GeneralHelper.isSpringType(paramTypes[i])) { if (args[i] != null) { Class<?> class1 = args[i].getClass(); Method[] ms = class1.getDeclaredMethods(); for (int j = 0; j < ms.length; j++) { // 遍歷所有返回為MutipartFile類型的get方法 if (ms[j].getName().startsWith("get") && ms[j].getReturnType().equals(MultipartFile.class)) { MultipartFile file = (MultipartFile) ms[j].invoke(args[i]); if (file !=null && !file.isEmpty()) { result = FileUploadValidation.suffixValid(file); System.out.println(result); } } } } } } if (result == true) { System.out.println("正確上傳"); return pjp.proceed(); }else{ System.out.println("非法上傳"); ModelAndView mv = new ModelAndView(); mv.addObject("message","非法上傳"); mv.setViewName("forward:/loginForm"); return mv; } } }<br />使用ESAPI中的文件上傳校驗庫
public abstract class FileUploadValidation { private static final String GIF_IMAGE_EXTENSION = "gif"; private static final String JPEG_IMAGE_EXTENSION = "jpeg"; private static final String JPG_IMAGE_EXTENSION = "jpg"; private static final String PNG_IMAGE_EXTENSION = "png"; public static final String FILE_UPLOAD_CONTEXT = "fileUpload";
public static boolean allowNull = false; public FileUploadValidation() { } @SuppressWarnings("null") public static boolean suffixValid(MultipartFile file) throws IntrusionException, ValidationException {
String filename = file.getOriginalFilename();List<String> allowedExtensions = new ArrayList<String>(); allowedExtensions.add(GIF_IMAGE_EXTENSION); allowedExtensions.add(JPEG_IMAGE_EXTENSION); allowedExtensions.add(JPG_IMAGE_EXTENSION); allowedExtensions.add(PNG_IMAGE_EXTENSION); return ESAPI.validator().isValidFileName(FILE_UPLOAD_CONTEXT,filename,allowedExtensions,allowNull); }
}<br /></pre>
Controller中使用
@FileSuffixTag @RequestMapping(value="/document/addDocument") public ModelAndView addDocument(String flag,@ModelAttribute Document document,ModelAndView mv,HttpSession session) throws Exception{if(flag.equals("1")){ mv.setViewName("document/showAddDocument"); }else{ String path = session.getServletContext().getRealPath("/upload"); String fileName = document.getFile().getOriginalFilename(); System.out.println("path -->>"+path); System.out.println("fileName -->>"+fileName); document.getFile().transferTo(new File(path+File.separator+fileName)); document.setFilename(fileName); User user = (User) session.getAttribute(HrmConstants.USER_SESSION); document.setUser(user); hrmService.addDocument(document); mv.setViewName("redirect:/document/selectDocument"); } return mv;
}<br /></pre>
AOP技術做SQL注射防御
消除SQL注射漏洞主要有3種方法:參數化查詢、存儲過程(類似參數化查詢,但是query存在在數據庫上,供應用調用)、escaping用戶輸入。另外一種是做白名單驗證,阻止查詢中輸入非法的格式。我這里選擇escaping用戶輸入和白名單validate。其優點是可以不修改原代碼。Aspects能去攔截和分析query,然后在執行前escape所有的表達式(expressions,可能包含惡意內容)。
而參數化查詢需要重寫動態查詢代碼,存儲過程需要開發move所有查詢到數據庫層。都需要修改原有代碼,可能引入bug和未知行為。此外OWASP提供了一個安全的encoding庫,可以直接調用。
方案邏輯
SQL Injection validator 邏輯
Aspect攔截到query后,先進行注釋移除,然后利用 JSqlParser API 對query的where進行解析,輸出簡單的expression List。
遍歷上一步獲得的simpleExpressions,對其進行語義重復檢測(類似1=1),如果檢測到有語義重復,就替換成“1=2”。
最后對類型為String的value進行encode。
https://github.com/JSQLParser/JSqlParser/wiki
ESAPI Encode
ESAPI.encoder().encodeForSQL()編碼結果樣例:
輸入:foo” and 1 = 2
輸出:foo\” and 1 \= 2
SQL Parser邏輯
來自:http://www.freebuf.com/articles/network/156069.html