AOP技术应用于安全防御与漏洞修复
TaraOrellan
7年前
<h2>关于AOP技术</h2> <p>AOP(Aspect-Oriented Programming)面向切面编程。切面是什么?切面表示从业务逻辑分离出来的横切逻辑,比如性能监控、日志记录、权限控制,这些功能可从核心逻辑代码中抽离出去。也就是说AOP可以解决代码耦合问题,让职责更加单一。</p> <p>这里要讲的是利用AOP技术解决代码安全问题。把安全代码从业务逻辑中分离出来,让其单一的解决安全问题。</p> <p>优势:不影响业务代码,修复安全漏洞的时候,可以对原代码很好的继承,不需修改原代码(基于配置定义切点的时候)。</p> <p>将AOP技术应用于安全技术的案例很少,网上很难找到较多示例代码。</p> <h2>关于ESAPI</h2> <p>OWASP Enterprise Security API (ESAPI)</p> <p>ESAPI (OWASP企业安全应用程序接口)是一个免费、开源的、网页应用程序安全控件库,它使程序员能够更容易写出更低风险的程序。ESAPI接口库被设计来使程序员能够更容易的在现有的程序中引入安全因素。ESAPI库也可以成为作为新程序开发的基础。</p> <p>通俗点说ESAPI是OWASP提供的一个安全开发API库。</p> <p><strong>优势:对比自己开发的安全处理代码更成熟稳定。</strong></p> <p>ESAPI具体的使用实际也是比较少,网上也很难找到较多示例代码。开发过程比较困难点</p> <h2>AOP 技术做 XSS 防御</h2> <p>OWASP提供了几种XSS防御的方法,包括阻止非信任的数据插入,escaping HTML输入、escaping attribute、escaping JavaScript、以及escaping几种其他类型。ESAPI提供了多种encoding库。另外也可以采用白名单validate方法(比如字母数字),也可以自定义正则表达式。</p> <p>AOP技术实现ESAPI提供的encoding和validation库,来做XSS攻击的防御工作。</p> <p>ESAPI提供的几种encoding方法:</p> <ul> <li>将用户数据输出到html body某处时,须经过html转义。ESAPI.encoder().encodeForHTML( request.getParameter( “input” ) )</li> </ul> <ul> <li>将用户数据输出到html标签的属性时,须经过标签属性的转义。ESAPI.encoder().encodeForHTMLAttribute( request.getParameter( “input” ) )</li> </ul> <ul> <li>将用户数据输出到JavaScript数据域时,须经过JavaScript转义。ESAPI.encoder().encodeForJavaScript( request.getParameter( “input” ) )</li> </ul> <ul> <li>将用户数据输出到URL的参数时,须经过URL转义。ESAPI.encoder().encodeForURL( request.getParameter( “input” ) )</li> </ul> <h3>方案逻辑</h3> <p style="text-align:center"><img src="https://simg.open-open.com/show/f6ffbddb8e3f49cfa38ccccc0b1fe934.jpg"></p> <h3>实现</h3> <p>定义切点的注解</p> <p>在需要进行XSS防御的方法前使用相应的注解。</p> <pre> @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface XSSAllTag { }<br /></pre> <pre> @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface XSSBeanTag { }<br /></pre> <pre> @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface XSSStringTag { }<br /></pre> <p>定义切面类</p> <p>拦截指定方法,遍历String类型参数,和数据模型(JAVABean)中的String属性,并对其进行XSS validate。</p> <pre> @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 /></pre> <p>使用ESAPI库处理XSS攻击input</p> <p>针对XSS攻击不同location与类别,提供相应的encoding方法。</p> <pre> 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 /></pre> <p>判定是否简单数据类型</p> <pre> 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> <p>Controller中使用</p> <p>在Contoller层使用前面定义的注解,进行XSS攻击防护。</p> <pre> @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> <p>AOP技术做FileUpload防御</p> <h3>方案逻辑</h3> <p style="text-align:center"><img src="https://simg.open-open.com/show/64756c571676a7c38e205073162afc92.jpg"></p> <p>定义注解</p> <pre> @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface FileSuffixTag { }<br /></pre> <p>定义切面</p> <pre> @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 /></pre> <p>使用ESAPI中的文件上传校验库</p> <pre> 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> <p>Controller中使用</p> <pre> @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> <h2>AOP技术做SQL注射防御</h2> <p>消除SQL注射漏洞主要有3种方法:参数化查询、存储过程(类似参数化查询,但是query存在在数据库上,供应用调用)、escaping用户输入。另外一种是做白名单验证,阻止查询中输入非法的格式。我这里选择escaping用户输入和白名单validate。其优点是可以不修改原代码。Aspects能去拦截和分析query,然后在执行前escape所有的表达式(expressions,可能包含恶意内容)。</p> <p>而参数化查询需要重写动态查询代码,存储过程需要开发move所有查询到数据库层。都需要修改原有代码,可能引入bug和未知行为。此外OWASP提供了一个安全的encoding库,可以直接调用。</p> <h3>方案逻辑</h3> <p style="text-align:center"><img src="https://simg.open-open.com/show/e0a43ebd40d9998e091a90a21ff793d3.jpg"></p> <h3><strong>SQL Injection validator 逻辑</strong></h3> <p>Aspect拦截到query后,先进行注释移除,然后利用 JSqlParser API 对query的where进行解析,输出简单的expression List。</p> <p>遍历上一步获得的simpleExpressions,对其进行语义重复检测(类似1=1),如果检测到有语义重复,就替换成“1=2”。</p> <p>最后对类型为String的value进行encode。</p> <p><a href="/misc/goto?guid=4959755726538586693" rel="nofollow,noindex">https://github.com/JSQLParser/JSqlParser/wiki</a></p> <p style="text-align:center"><img src="https://simg.open-open.com/show/d5768e5f26f393c00e5f8513f28ebef5.jpg"></p> <p><strong>ESAPI Encode</strong></p> <pre> ESAPI.encoder().encodeForSQL()</pre> <p>编码结果样例:</p> <p>输入:foo” and 1 = 2</p> <p>输出:foo\” and 1 \= 2</p> <h3><strong>SQL Parser逻辑</strong></h3> <p style="text-align:center"><img src="https://simg.open-open.com/show/fcbb2ac11abe812480c804833d9eb41f.jpg"></p> <p> </p> <p>来自:http://www.freebuf.com/articles/network/156069.html</p> <p> </p>