作者:京东物流刘海猫最近遇到一个值班报警事件,Web应用服务器CPU占用高达99%。经排查,发现是由于ReDoSExhaustion导致资源在服务器上被阻塞,访问系统速度慢。通过排查过程,我们将分享ReDos攻击的原理、常见场景、防范和解决方案。如有错误请指正。在后台值班的时候,突然报警,web应用服务器CPU占用率达到99%。同时,现场反馈系统访问速度慢。登录泰山平台,查看ump监控,发现系统CPU消耗突然爆满。使用java自带的dump工具下载jstock文件,发现有大量相同任务的线程在运行,具体堆栈信息如下,仔细查看这些线程的执行代码,发现UrlUtil.extractDomain叫做。该方法根据堆栈信息查看业务代码,发现是joybuy登录拦截器使用正则表达式匹配访问url解析主域的方法被拦截。至此可以判断是由于ReDoS导致服务器资源耗尽,访问系统缓慢。那么,什么是ReDoS?ReDos简介ReDoS攻击(正则表达式拒绝服务攻击(RegularExpressionDenialofService)),攻击者可以构造一个特殊的字符串,导致正则表达式运行会消耗大量内存和cpu,服务器资源也会耗尽。无法继续响应,一个不确定的正则表达式为什么会引起重做攻击?这就得从正则表达式的实现原理说起。目前正则表达式引擎的实现方式有两种:DFA自动机(DeterministicFiniteAutomaton,确定性有限状态自动机)NFA自动机(NondeterministicFiniteAutomaton,非确定性有限状态自动机)DFA自动机的构建成本远大于NFA自动机,但DFA自动机的执行效率高于NFA自动机。假设一个字符串的长度为n,如果使用DFA自动机作为正则表达式引擎,则匹配时间复杂度为O(n)如果使用NFA自动机作为正则表达式引擎,则有大量分支和NFA自动机匹配过程中的回溯。假设NFA的状态数为s,匹配时间复杂度为O(ns)NFA自动机的优点是支持更高级的功能,但都是基于子表达式独立匹配。因此,在编程语言中,所使用的正则表达式库都是基于NFA自动机实现的NFA的特点:一个有限的状态集S是一个输入符号集sigma,空字符epsilon不属于Sigma状态转移函数F.对于一个特定的输入字符和状态,输出对应的变化状态集4.s0是初始状态5.S子集是结束状态集description定义一个正则表达式^(a+)+$来匹配字符串aaaaX。使用NFA的正则化引擎,必须在2^4=16次失败尝试后取消匹配。同样,如果字符串是aaaaaaaaaaX,则需要尝试2^10=1024次。如果我们继续把a的数量增加到20个、30个甚至更多,那么这里的匹配就会变成指数级增长。常见的ReDoS场景以java为例,常见的ReDoS场景有以下几种:1.使用javax.validation.constraints.Pattern验证输入参数是否合理/***客户备注**/@ExcelProperty(index=14)@Length(min=11,max=11,message="VATnumbermustbe11digits")@Pattern(regexp="^(GB)\d{9}",message="VATnumbermustbestartwithGBandend9位数字”)privateStringvatNumber;2.使用String.matches进行业务数据校验的场景//发票日期格式yyyy-MM-ddStringregExp="^[1-9]\d{3}-(0?[1-9]|1[0-2])-(0?[1-9]|[1-2][0-9]|3[0-1])$";if(StringUtils.isNotBlank(outstockDto.getInvoiceDate())&&!outstockDto。getInvoiceDate().matches(regExp)){totalMsg.add(newErrorMsgDTO(ResultCodeEnum.OUTSTOCK_INVOICE_DATE_FORMAT_ERROR.getCode()));}3.使用String.replaceAll作为参数替换场景privateStringgetParamName(Stringstr){if(PATTERN_START_END.matcher(str).matches()){StringnewStr=str.replaceAll("#\{","").replaceAll("\}","");如果(StringUtils.isEmpty(newStr)){返回"";}elseif(newStr.contains(".")){returnStringUtils.substringAfterLast(newStr,".");}返回newStr;}returnnull;}4.配置文件匹配参数场景#joybuy登录主域joybuy.login.domain=.*fop.joybuy.com$#欧美账号B登录主域pulsar.login。domain=.*ifop.jd.com$ReDoS检测1.RegexStaticAnalysis工具测试方法如下:打包后使用maven包执行本地操作,输入待测试的正则表达式2.在线测试地址:https:///regex101.com/测试方法:直接在输入框中输入正则表达式和要测试的字符串,都可以看到匹配步数和结果。在调试器模式下,可以查看详细的匹配过程和步骤。预防措施只是为了降低风险,并不能100%消除ReDoS的威胁。当然,要避免这种威胁,最好的办法就是在业务场景中尽量减少正则表达式的使用或者多做测试,增加服务器性能监控等,降低正则表达式的复杂度,尽量少用组来严格限制用户输入的字符串。使用单元测试和fuzzing测试确保安全使用静态代码分析工具增加性能监控,例如ump、pfinder等解决方案了解ReDoS的原理和防范,优化本次CPU告警代码,使用判断请求路径和分段以字符串形式获取访问域,避免使用正则表达式导致的ReDoS问题。实际修复代码publicstaticStringextractDomain(Stringurl){if(StringUtils.isBlank(url)){return"";}整数索引=0;if(url.startsWith(HTTP)){index=HTTP.length();}elseif(url.startsWith(HTTPS)){index=HTTPS.length();}else{返回“”;}StringsafeUrl=url.substring(index);index=safeUrl.indexOf('/');if(index>0){safeUrl=safeUrl.substring(0,index);}String[]array=safeUrl.split("\.");如果(数组长度<2){返回“”;}Stringpart1=array[array.length-2];字符串part2=array[array.length-1];如果(StringUtils.isNotBlank(第1部分)&&StringUtils.isNotBlank(第2部分)){if(!isIn(part2,DOMAINS)){返回“”;}返回第1部分+'.'+第2部分;}返回””;}
