验证码前端验证、接口访问不需要授权、未授权任意密码读取、物理路径泄露、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;
}
}
}
总结:
问题算是解决了,让我不爽的是“前人挖坑,后人填”,为什么不是“前人种树,后人乘凉” 😂,好在这不是什么重要的系统,没有造成生产问题。记的我入这行头一年,就天天驻场天天在客户眼皮底下干活,这种安全性问题被客户抓到,项目负责人等着挨批吧!总之作为一名程序员,我觉得技术强不强不是最重要的,最重要的你得有良好的职业操守,写好代码勤劳的双手,以及解决问题的思路。
本方案在前端小伙伴的配合下完成编码、测试工作。
"三个臭皮匠,顶个诸葛亮"