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

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

背景:

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

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

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

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

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

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

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

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

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

漏洞问题:

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

紧急情况:

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

解决方案:

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

思路及步骤:

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

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

我的实际代码是这样的

/**
 * 接口访问权限校验拦截器
 *
 * @author dmh
 */
public class WebHandlerInterceptor implements HandlerInterceptor {
    
    @Override
    public 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");
        //拦截获取请求头token
        
        String userToken = null;
        try {
            userToken = request.getHeader("Authorization");
        } catch (NullPointerException e) {
            e.printStackTrace();
        }
        
        if (StringUtils.isNotEmpty(userToken)) {
            //获取缓存中token的key储存的value
            Yh 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 {
​
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new WebHandlerInterceptor()).addPathPatterns("/**");
    }
}

我的代码是这样的

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    /**
     * 重写addCorsMappings解决跨域问题
     * 配置:允许http请求进行跨域访问
     *
     * @param registry
     */
    @Override
    public 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);
    }
 
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        /** 静态资源配置 */
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }
    
    /**
     * 重写addInterceptors实现拦截器
     * 配置:要拦截的路径以及不拦截的路径
     *
     * @param registry
     */
    @Override
    public 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();
        
        //登录验证的逻辑...不给看^_^  
            
        //用户token
        String 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保存到浏览器cookie
                setCookieLogin('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;
        }
    }
}

总结:

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

招风的黑耳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值