程序员的自我修养之快速填坑法修炼中

验证码前端验证、接口访问不需要授权、未授权任意密码读取、物理路径泄露、sql注入等严重问题,你敢相信这是运行一年多的系统存在的漏洞?

背景:

今天运维小伙伴微信发消息给我:"哥,在吗?",我猜应该是有活要解决了,果不其然,接着发了一个文件过来,我一看又是"某某系统漏洞监测报告",心想政府行业系统要求确实高(这里说一下,隔一段时间你会收到他们漏洞监测的报告,公布出来的软件方面代码或依赖包存在的漏洞报告,例如:Spring Framework 安全绕过漏洞、Log4j漏洞...),之前基本遇到这种架构方面的漏洞基本就是先排查,然后搜罗解决方案。

这回,看到漏洞报告内容,是真的有点想哭又想笑。从报告内容来看,这份报告是由第三方公司提供的,报告内容描述的非常详细清晰。不得不说现在国家很重视信息安全这方面,也不惜花重金做这方面事。

打开报告,给我的感觉是“专业”、“规范”,人家还不是普通给弄一个测试报告,测测功能是否正常、能不能使用,相信这些在系统正式投产前已经通过了。他给整个渗透测试报告,嘛事渗透测试呢?我们来学习一下...

渗透测试是一种通过一些手段来找到网站,APP,网络服务,软件,服务器等网络设备和应用的漏洞,告诉管理员有哪些漏洞,怎么填补,从而防止黑客的入侵。渗透测试通常指模拟黑客采用的漏洞发掘技术及攻击方法,测试工程师对被测试单位的网络、主机、应用及数据是否存在安全问题进行检测的过程。渗透测试分为白盒测试和黑盒测试。其中黑盒测试就是只告诉我们这个网站的url,其他什么都不告诉,然后让你去渗透,模拟黑客对网站的渗透。

简单的理解这是一种攻击测试,监测系统的安全性能。因为政府部门的系统涉及到方方面面的数据,关系到民生、企业、地理、建设、金融等等,涉及到各行各业,小到个人敏感信息、大到国家机密数据。所以系统安全,对于政府系统无比重要。因此修复这些漏洞,以确保系统和数据的安全性刻不容缓。

我们这个系统可能因为和重要核心的系统沾不到边,也不是什么大平台,功能也必将单一。我接手的时候已经是运维阶段了,后面开发的事情基本都是变更、扩展某些功能。

我记得,去年对项目维护的时候,从代码库迁下拉看到项目代码结构以及数据,我有的惊讶,因为干了这么多年程序员,还是头一次见这么写代码做开发的,心想太不严谨了吧,好随意好任性!

或许当时的人想法是"劳资不管三七二十一,什么安全、性能都是扯淡,先干出来再说,至于后面的事,不一定待到哪天呢",该不会当时公司拖他几个月工资了吧,要不然不至于这么挖坑啊!好了就吐槽到这里了。

下面我们干正事,拿了工资公司待我不薄,该我实现价值的时候了,程序员上场了,你们项目经理、测试、运维都给我一边看着,站稳脚,我要填坑了。

漏洞问题:

权限问题(高危),接口未进行授权允许任意访问,最可恨的是密码都给你返回了,而且是明文的。

紧急情况:

运维和我说客户催的急,要求多短多短时间要完成,哇靠,这是烫手的山芋啊!我心想,果然只要我还在这里一天,这个问题迟早要坑我一下!哎,闷头写代码吧。

解决方案:

想过整合权限认证的框架进来,常见的有 Apache Shiro 和 Spring Security,考虑到要快速解决问题,整合一个框架改动会很大,花费时间不好控制,万一整出其他事来了,那不是给自己挖坑嘛。于是就想到在后台使用拦截器,对所有的访问路径进行拦截,哎呀,我感觉这个方案靠谱,比Shiro还轻量级,再说我们只要解决接口的授权问题,不需要其他一些功能。

思路及步骤:

1. 创建一个拦截器类,继承HandlerInterceptorAdapter类,并重写preHandle方法。

public class WebHandlerInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//TODOreturn true;}}

我的实际代码是这样的

/*** 接口访问权限校验拦截器** @author dmh*/
public class WebHandlerInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//options直接放行if (request.getMethod().equals("OPTIONS")) {return true;}
​String uri = request.getRequestURI();//放行静态资源if (uri.contains(".")) {return true;}System.out.println("++++++++++请求地址="+uri);response.setContentType("application/json;charset=UTF-8");//拦截获取请求头tokenString userToken = null;try {userToken = request.getHeader("Authorization");} catch (NullPointerException e) {e.printStackTrace();}if (StringUtils.isNotEmpty(userToken)) {//获取缓存中token的key储存的valueYh yh = (Yh) CacheUtil.get(userToken);
​if (yh != null) {//请求进来就刷新一次token的有效期CacheUtil.put(userToken, yh, Constants.TOKEN_EXPIRE);CacheUtil.put(Constants.USER_TOKEN + yh.getId(), userToken, Constants.TOKEN_EXPIRE);return true;} else {//授权过期,“登录有效期已过,请重新登陆”response.setStatus(403);}} else {//未授权,“您还没有登录,请登陆”response.setStatus(401);}//解决跨域问题response.addHeader("Access-Control-Allow-Origin", "*");response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD");response.addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");response.addHeader("Access-Control-Max-Age", "3600");return false;}}

2. 在配置类中添加拦截器。

@Configuration
public class WebConfig implements WebMvcConfigurer {
​@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new WebHandlerInterceptor()).addPathPatterns("/**");}
}

我的代码是这样的

@Configuration
public class WebConfig implements WebMvcConfigurer {/*** 重写addCorsMappings解决跨域问题* 配置:允许http请求进行跨域访问** @param registry*/@Overridepublic void addCorsMappings(CorsRegistry registry) {//指哪些接口URL需要增加跨域设置registry.addMapping("/**")//.allowedOrigins("*")//指的是前端哪些域名被允许跨域.allowedOriginPatterns("*")//需要带cookie等凭证时,设置为true,就会把cookie的相关信息带上.allowCredentials(true)//指的是允许哪些方法.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")//cookie的失效时间,单位为秒(s),若设置为-1,则关闭浏览器就失效.maxAge(3600);}@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {/** 静态资源配置 */registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");}/*** 重写addInterceptors实现拦截器* 配置:要拦截的路径以及不拦截的路径** @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册WebHandlerInterceptor拦截器InterceptorRegistration registration = registry.addInterceptor(new WebHandlerInterceptor());//addPathPatterns()方法添加需要拦截的路径//所有路径都被拦截registration.addPathPatterns("/**");//excludePathPatterns()方法添加不拦截的路径//添加不拦截路径registration.excludePathPatterns(//登录"/login",//退出登录"/loginOut",//获取验证码"/getCode",//发送短信"/sendshortMessage",//文件上传"/uploadImg",//html静态资源"/**/*.html",//js静态资源"/**/*.js",//css静态资源"/**/*.css");}
}

3. 登录接口修改,登录成功生成token(用户认证标识),后台将用户信息、token保存至缓存并将token响应给前端。

//登录方法public JSONObject getYhByYhmAndYhmm(String yhm, String yhmm, String yzm, String uuid) {JSONObject result =new JSONObject();//登录验证的逻辑...不给看^_^  //用户tokenString userToken = "";//查询缓存key的value是否存在if (CacheUtil.get(Constants.USER_TOKEN + user.getId()) != null) {userToken = (String) CacheUtil.get(Constants.USER_TOKEN + user.getId());}//没有userToken,生成(其实感觉上面一步可以不要)if (StringUtils.isEmpty(userToken)) {userToken = Constants.USER_ACCESS_TOKEN + UUID.randomUUID().toString().replaceAll("-", "");//缓存用户信息,userToken作为key,user作为value,token过期时间自己定吧CacheUtil.put(userToken, user, Constants.TOKEN_EXPIRE);//缓存token,用户统一前缀+用户id作为key,userToken作为value,token过期时间同上CacheUtil.put(Constants.USER_TOKEN + user.getId(), userToken, Constants.TOKEN_EXPIRE);}result.put("token", userToken);result.put("user", user);result.put("code","ok");return result;}

4. 前端登录修改,登录成功将接口返回的token保存至浏览器cookie。

//登录
function login(){var username=$(".username").val();var password=$("#password").val();var yzm=$("#yzm").val().trim().toLowerCase();//验证逻辑省略...$.ajax({type:"POST",url:SERVICE_URL+"user/login",data:{yhm: username,yhmm: password,yzm: yzm,uuid: yzmuuid},error:function(data){alert("连接失败,请重试...");createVerificationIamge();},success:function(data){if(data.code==="ok"){//将token保存到浏览器cookiesetCookieLogin('token', data.token);//登录成功,跳转到主页面window.location.href="index.html?lx="+$.getUrlParam('lx');}else if(data.code==="error"){alert(data.msg);}else{alert("查询失败");}}});
}
​
function setCookieLogin(c_name, value) {var expiresDate = new Date();expiresDate.setTime(expiresDate.getTime() + (12 * 60 * 60 * 1000));//单位为毫秒 设置12小时过期$.cookie(c_name, value, {expires: expiresDate, path: '/'});
}

5. 前端做全局http请求拦截,对发送的请求统一在请求头传token。

/*** ajax全局拦截器*/
$.ajaxSetup({headers: { // 发送请求前触发"Authorization" : getCookie('token')}
}
​
//获取cookie信息
function getCookie(c_name) {if (document.cookie.length > 0) {c_start = document.cookie.indexOf(c_name + "=");if (c_start != -1) {c_start = c_start + c_name.length + 1;c_end = document.cookie.indexOf(";", c_start);if (c_end == -1) c_end = document.cookie.length;return decodeURI(document.cookie.substring(c_start, c_end));}}return ""
}

6. 后台接收到请求,通过token验证用户有没有登录,有没有某些接口或资源的访问权限。

这一步测试的时候遇到两个问题,看第一步创建拦截器我的代码中头部和尾部两端代码:

//options直接放行,解决接收不到前端传入的header参数信息
if (request.getMethod().equals("OPTIONS")) {return true;
}
//解决跨域问题
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD");
response.addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
response.addHeader("Access-Control-Max-Age", "3600");

7. 前端做全局响应拦截,对响应状态为"401"、"403"的进行逻辑处理,增加系统用户使用友好度。

/*** ajax全局拦截器*/
$.ajaxSetup({headers: { // 发送请求前触发"Authorization" : getCookie('token')},complete: function(xhr) {//未登录访问或token过期,则跳转到登录页面if(xhr.status == 401){var msg = layui.layer.msg('您还没有登录,请登陆!', {time:false,icon:2,btn:['确定','取消'],btnAlign:'center',yes:function () {top.location.href = "login.html";layui.layer.close(msg);//手动关闭}});}else if(xhr.status == 403){var msg = layui.layer.msg('登录有效期已过,请重新登陆!', {time:false,icon:2,btn:['确定','取消'],btnAlign:'center',yes:function () {deleteCookie();top.location.href = "login.html";layui.layer.close(msg);//手动关闭}});}}
});
​
// 清除所有的cookie
function deleteCookie() {var cookies = document.cookie.split(";");for (var i = 0; i < cookies.length; i++) {var cookie = cookies[i];var eqPos = cookie.indexOf("=");var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";}if(cookies.length > 0){for (var i = 0; i < cookies.length; i++) {var cookie = cookies[i];var eqPos = cookie.indexOf("=");var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;var domain = location.host.substr(location.host.indexOf('.'));document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=" + domain;}}
}

总结:

问题算是解决了,让我不爽的是“前人挖坑,后人填”,为什么不是“前人种树,后人乘凉” 😂,好在这不是什么重要的系统,没有造成生产问题。记的我入这行头一年,就天天驻场天天在客户眼皮底下干活,这种安全性问题被客户抓到,项目负责人等着挨批吧!总之作为一名程序员,我觉得技术强不强不是最重要的,最重要的你得有良好的职业操守,写好代码勤劳的双手,以及解决问题的思路。

本方案在前端小伙伴的配合下完成编码、测试工作。

"三个臭皮匠,顶个诸葛亮"


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部