Java开发常见漏洞及解决方案

1.健康监控Actuator暴露端口

​ Actuator是Springboot提供的用来对应用系统进行自省和监控的功能模块,借助于Actuator开发者可以很方便的对应用系统的某些监控指标进行查看,统计等。

​ 在springboot项目中引入Actuator:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

漏洞问题:

​ 系统启动后,我们通过访问Actuator暴露的一些端口,可以获取到信息,这样也就存在信息暴露的风险,,以下是一些暴露的端口。

(1)http://ip+port/actuator

​ 通过http://ip+port/actuator可以访问到系统暴露的端点集合,每一个端点都可以进行调用,例http://127.0.0.1:9876/api/actuator,查询结果如下:
在这里插入图片描述
(2)http://ip+port/actuator/env

​ 通过http://ip+port/actuator/env可以访问到系统配置环境信息,包含properties中数据库用户名、密码等敏感信息,例http://127.0.0.1:9876/api/actuator/env,查询结果如下:

(3)http://ip+port/actuator/heapdump

​ 通过http://ip+port/actuator/heapdump可以下载堆转储文件,在浏览器中执行此请求,会下载一份文件:

​ 然后利用heapdump_tool工具可以从此headdump文件中提取出数据库等敏感信息:

解决方案

1.在properties中禁止某些端口的访问,例如:

management:
  endpoints:
    web:
      exposure:
        #禁止env、heapdump、threaddump接口的访问
        exclude:
          - env
          - heapdump
          - threaddump

此时再访问禁止的端口,显示404:

2.在properties中完全禁用actuator,例如:

management:
  server:
    port: -1

此时再访问actuator的端口,都显示404:

2.SSRF漏洞攻击

​ 在项目开发中,有些需求是前端配置ip或者url信息,服务端通过此配置信息进行远程调用。例如系统提供前端页面配置数据库的链接信息:

系统支持前端页面配置获取第三方token的链接信息,通过服务端去调用获取的场景:

漏洞问题:

​ 可以对外网服务器所在的内网、本地进行端口扫描,获取一些服务的banner信息;攻击运行在内网或本地的应用程序(比如溢出);利用ftp、file协议读取本地文件等。

(1)扫描内网服务器开放的端口

​ 数据库链接配置:通过配置内网服务器的ip、端口信息,点击测试连接数据库,当此ip不存在或者端口不存在时,接口返回的报错速度会非常快;当访问到存活ip的存活端口,那么返回的时间会有非常大的差别。

不存在的端口,执行时间:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yctSFGca-1673429158542)(E:\我的工作文档\服务端文档\技术文档\java开发漏洞\微信截图_20230109112422.png)]
存活ip、存活端口,执行时间:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j8l5kSdz-1673429158543)(E:\我的工作文档\服务端文档\技术文档\java开发漏洞\微信截图_20230109112440.png)]
(2)访问内网文件

​ 服务器调用第三方系统配置链接信息:当接口地址配置ftp或file时,可以访问到服务器的文件信息。

解决方案:

1.禁止host配置为内网地址;

2.统一错误信息,避免用户可以根据错误信息来判断远端服务端的端口状态;

3.禁用不需要的协议,仅允许http和https请求,防止ftp、telnet、file、ldap等协议引起的问题;

4.对url进行非法特殊字符校验,防止CRLF(换行符\r\n)攻击;

5.对确实需要访问的内网ip、端口进行白名单的配置。

(1)校验url方法:

//校验url信息
private void checkUrl(String url) {
    
    
        try {
    
    
            URL url = new URL(url);
            //ip或者域名
            String host = url.getHost();
            //协议,http/https
            String protocol = url.getProtocol();
            //判断是否存在非法特殊字符,防止CRLF攻击
            String docode = URLDecoder.decode(tokenUrl,"utf-8");
            if(docode.contains("\r\n")){
    
    
                throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "url包含特殊字符,请确保参数的准确性");
            }
            //只允许http、https协议
            if(!(protocol.equals("https") || protocol.equals("http"))){
    
    
                throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "协议只支持http/https,请确保参数的准确性");
            }
            //获取端口
            int port = url.getPort();
            if(-1 == port){
    
    
                if(protocol.equals("https")){
    
    
                    port = 443;
                } else {
    
    
                    port = 80;
                }
            }
            //获取系统配置的白名单ip集合
            Map<String, String> ignoredMap = apiWhiteListConfig.getIgnoredMap();
            //校验host和port是否合法
            IpUtil.checkHostAndPort(host, String.valueOf(port),ignoredMap);
        } catch (MalformedURLException e) {
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "url解析异常,请确保参数的准确性");
        } catch (UnsupportedEncodingException e){
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "url编码异常,请确保参数的准确性");
        }
    }

(2)配置内网白名单

properties配置文件中配置白名单集合:

#出于屏蔽SSRF攻击,给出放开的内网白名单及端口号
inner:
  ignored:
    url-list:
      - 10.233.144.x:6777
      - 10.111.144.x:8888
      - 10.234.100.x:9088

加载配置的白名单集合:

@Configuration
@ConfigurationProperties(prefix="inner.ignored")
@Data
public class ApiWhiteListConfig {
    
    

    //忽略的内网访问集合,key:ip,value:port
    private Map<String,String> ignoredMap = new HashMap<String,String>();

    private List<String> urlList;


    @PostConstruct
    public void init(){
    
    
        if(null != urlList && urlList.size() > 0){
    
    
            for(int i = 0;i < urlList.size();i++){
    
    
                String str = urlList.get(i);
                String[] arr = str.split(":");
                if(null != arr && arr.length == 2){
    
    
                    ignoredMap.put(arr[0],arr[1]);
                }
             }
        }
    }
}

(3)校验host和port是否合法

//ip工具
public class IpUtil {
    
    

    static List<Pattern> ipFilterRegexList = new ArrayList<>();
    //指定内网ip的匹配正则集合
    static {
    
    
        Set<String> ipFilter = new HashSet<String>();
        ipFilter.add("^10\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])"
                + "\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])" + "\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
        ipFilter.add("^172\\.(1[6789]|2[0-9]|3[01])\\" + ".(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])\\"
                + ".(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
        ipFilter.add("^192\\.168\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])\\"
                + ".(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$");
        ipFilter.add("127.0.0.1");
        ipFilter.add("0.0.0.0");
        ipFilter.add("localhost");

        for (String reg : ipFilter) {
    
    
            ipFilterRegexList.add(Pattern.compile(reg));
        }
    }

    /**
     * @Description: 判断ip是否属于内网
     * @Author: qingyun
     * @Param: [ip]
     * @Return: boolean
     * @Date: 2023/1/6 15:50
     */
    public static boolean ipIsInner(String ip) {
    
    
        for (Pattern reg : ipFilterRegexList) {
    
    
            Matcher matcher = reg.matcher(ip);
            if (matcher.find()){
    
    
                return true;
            }
        }
        return false;
    }

    /**
     * @Description: 根据主机host,获取对应的ip
     * @Author: qingyun
     * @Param: [hostName]
     * @Return: java.lang.String[]
     * @Date: 2023/1/6 15:44
     */
    public static String[] getAllIPByHostName(String hostName){
    
    
        try {
    
    
            InetAddress[] addrs=InetAddress.getAllByName(hostName);//根据域名创建主机地址对象
            String[] ips=new String[addrs.length];
            for (int i = 0; i < addrs.length; i++) {
    
    
                ips[i] = addrs[i].getHostAddress();//获取主机IP地址
            }
            return ips;
        } catch (UnknownHostException e) {
    
    
            return null;
        }
    }

    /**
     * @Description: 校验host和port是否合法
     * @Author: qingyun
     * @Param: [host, port, ignoredMap]
     * @Return: void
     * @Date: 2023/1/6 17:32
     */
    public static void checkHostAndPort(String host, String port, Map<String, String> ignoredMap) {
    
    
        //获取到host解析后的真实ip地址
        String[] allIPByHostName = getAllIPByHostName(host);
        if(null == allIPByHostName || allIPByHostName.length == 0){
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "host:"+host+"解析异常,请确保您输入的地址是真实有效的");
        }
        if(null != ignoredMap && !ignoredMap.isEmpty()){
    
    
            boolean flag = true;
            for(int i = 0;i < allIPByHostName.length;i++){
    
    
                if(!ignoredMap.containsKey(allIPByHostName[i])){
    
       //host配置的ip不存在与白名单内
                    flag = false;
                    break;
                } else {
    
      //当包含此ip时,还需要比对下放开的端口
                    if(!port.equals(ignoredMap.get(allIPByHostName[i]))){
    
      //放开的端口不相同
                        flag = false;
                        break;
                    }
                }
            }
            if(flag){
    
       //host和port存在于配置的白名单内,可以直接放行
                return;
            }
        }

        //校验此host解析出来的ip是否是内网,内网则直接返回,防止ssrf攻击
        for(int j = 0;j < allIPByHostName.length;j++){
    
    
            if(ipIsInner(allIPByHostName[j])){
    
      //是内网地址,则直接抛出异常
                throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "host:"+host+"不是外网地址,请确保参数的准确性");
            }
        }
    }
}

(4)统一错误处理,不返回具体报错信息

 try {
    
        
    //校验地址是否符合要求
    checkUrl(String url)
    //调用远端服务器
    String result = HttpClientUtils.doPost(url, param.toString(), null, null);      
 } catch (Exception e) {
    
    
    e.printStackTrace();
    throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "接口请求调用失败,请确保参数的准确性");
 }

3.富文本XSS攻击

​ 在项目中,有些输入框的文本内容需要带有样式、引入图片等需求,传统的input输入框没法满足,这时候就需要引入富文本编辑器,前端显示富文本的时候使用html标签。例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ubzokp42-1673429158544)(E:\我的工作文档\服务端文档\技术文档\java开发漏洞\微信截图_20230109142438.png)]
漏洞问题:

​ 当富文本内容包含一些前端js的alert、script等字符时,在进行文本回显的时候,浏览器会执行上述脚本,从而劫持用户会话、危害网站、插入恶意内容、重定向用户、使用恶意软件劫持用户浏览器等。

(1)恶意弹窗

​ 在富文本中包含以下恶意js脚本:

<img src=1 onerror="alert(1)">
<script>alert(1);</script>
<img src=1 onerror=alert(document.cookie);>

​ 在富文本回显的时候,出现弹框:

服务端解决方案:

1.引入jsoup过滤前端提交过来的富文本

​ jsoup是一款java的HTML解析器,可直接解析某个URL地址、HTML文本内容。java引入jsoup的方式:在pom.xml中引入jar包:

 <dependency>
   <groupId>org.jsoup</groupId>
   <artifactId>jsoup</artifactId>
   <version>1.15.3</version>
</dependency>

​ 调用jsoup提供的API进行字符过滤:

  /**
     * @Description: 获取经过json处理后的数据,防止xss攻击
     * @Author: qingyun
     * @Return: java.lang.String
     * @Date: 2023/1/5 14:36
     */
    private String getJsonCleanValue(String msg) {
    
    
        Safelist safelist = Safelist.relaxed();
        String res = Jsoup.clean(msg,safelist);
        return res;
    }

2.当Safelist.relaxed()提供的白名单不够用时,进行扩展

​ 首先来看下Safelist.relaxed()定义的白名单集合:

 public static Safelist relaxed() {
    
    
        return new Safelist()
                .addTags( "a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6","i", "img", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong","sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul")
                .addAttributes("a", "href", "title")
                .addAttributes("blockquote", "cite")
                .addAttributes("col", "span", "width")
                .addAttributes("colgroup", "span", "width")
                .addAttributes("img", "align", "alt", "height", "src", "title", "width")
                .addAttributes("ol", "start", "type")
                .addAttributes("q", "cite")
                .addAttributes("table", "summary", "width")
                .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width")
                .addAttributes( "th", "abbr", "axis", "colspan", "rowspan", "scope","width")
                .addAttributes("ul", "type")

                .addProtocols("a", "href", "ftp", "http", "https", "mailto")
                .addProtocols("blockquote", "cite", "http", "https")
                .addProtocols("cite", "cite", "http", "https")
                .addProtocols("img", "src", "http", "https")
                .addProtocols("q", "cite", "http", "https");
    }

addTags(String … tags):参数的内容表示html标签(元素)的白名单;

addAttributes(String tag,String … attributes):表示指定标签允许配置哪些属性;

addProtocols(String tag,String attribute,String … protocols):表示指定标签的指定属性允许使用哪些协议。

​ 当默认的白名单不满足需求时,可以在既有的基础上添加新的白名单,例如:

 Safelist safelist = Safelist.relaxed().addTags("test").addAttributes("test","src","href").addProtocols("test","src","http");

​ 测试结果如下:

 //输入
 String str = "<test src='http://xxxxxx'>";
 //输出
 <test src="http://xxxxxx"></test>
 
 //输入,不支持的https协议
 String str = "<test src='https://xxxxxx'>";
 //输出
 <test></test>

前端解决方案

1.引入js-xss过滤待显示富文本

​ 前端将数据渲染到页面呈现之前,先对内容进行过滤,推荐使用js-xss,官网:http://jsxss.com

(1)在Node.js上使用

安装:

$ npm install xss --save

使用:

var xss = require('xss');
console.log(xss('<a href="#" onclick="alert(/xss/)">click me</a>'));

(2)在浏览器器上使用

引入文件:

https://raw.github.com/leizongmin/js-xss/master/dist/xss.js

使用:

console.log(filterXSS('<a href="#" onclick="alert(/xss/)">click me</a>'));

4.暴力破解短信验证码登录

​ 现在越来越多的网站支持通过手机号+验证码的方式登录系统,验证码一般4位或者6位数字。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5gQTBYqn-1673429158545)(E:\我的工作文档\服务端文档\技术文档\java开发漏洞\微信截图_20230109171826.png)]
漏洞问题:

​ 当不法用户输入某个已知的其他用户的手机号,点击获取验证码,然后使用程序生成符合位数的验证码进行暴力登录,破解成功后即可获得合法用户的权限,甚至可以破解管理员的密码进而控制整个站点。

解决方案:

1.使用验证码进行验证登录;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7WcXoGtR-1673429158546)(E:\我的工作文档\服务端文档\技术文档\java开发漏洞\微信截图_20230109172700.png)]
2.生成短信验证码时,设置失效时间、可以验证的剩余次数(每次验证数量减1,数量小于0则删除);

(1)生成验证码

​ 验证码存放到redis中,设置失效时间、设置可以验证的剩余次数。

//组织存在redis中的key值
String key = RedisKeyPrefixConstants.PHONE_NUM+phone;
//使用hash方式来存,因为每次验证需要单独更新可验证的次数值
redisTemplate.opsForHash().put(key,"code",verCode);
//设置可以验证的剩余次数
redisTemplate.opsForHash().put(key,"num",5);
//设置过期时间
redisTemplate.expire(key,300,TimeUnit.SECONDS);

(2)校验验证码

​ 判断是否有给此手机号发送过验证码,没发送过直接验证失败;有发送过验证码,判断输入的验证码是否等于redis中存放的,若是相等,则校验通过,删除redis中的记录;若是不相等,则剩余次数减1,然后判断剩余次数是否小于0,小于0,则删除redis中的记录,验证失败,若是大于0则提示验证码错误。

//传递用户手机号、输入的验证码
public boolean verifyCaptcha(String phone, String captcha){
    
    
        String key = RedisKeyPrefixConstants.PHONE_NUM+phone;
        //获取此手机号的验证码
        Object object = redisTemplate.opsForHash().get(key, "code");
        if(null == object){
    
    
            //没有给此手机号发送的验证码记录
            throw WebErrorUtils.commonError(null, HttpStatus.PROCESSING, "未请求验证码或验证码已失效,请重新登录!");
        }
        String res= object.toString();
        //比较输入的验证码是否正确
        if(res.equals(captcha)){
    
    
            //验证通过,删除redis的记录
            redisTemplate.delete(key);
            return true;
        }else{
    
    
            //校验次数减1
            double num = redisTemplate.opsForHash().increment(key,"num",-1);
            if(num < 0 ){
    
    
                //剩余可以校验的次数小于0,已经用完,则删除redis的记录
                redisTemplate.delete(key);
                throw WebErrorUtils.commonError(null, HttpStatus.PROCESSING, "未请求验证码或验证码已失效,请重新登录!");
            } else {
    
    
                //还有剩余可以校验的次数,返回验证码错误
                throw WebErrorUtils.commonError(null, HttpStatus.PROCESSING, "验证码错误!");
            }
        }
    }

5.恶意短信轰炸骚扰用户

​ 现在越来越多的网站支持通过手机号+验证码的方式登录系统,验证码一般4位或者6位数字。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LECLGTPk-1673429158547)(E:\我的工作文档\服务端文档\技术文档\java开发漏洞\微信截图_20230109171826.png)]
漏洞问题:

​ 当不法用户输入某个已知的其他用户的手机号,点击获取验证码,或者用程序循环去跑发送短信验证码的接口,导致此手机号的用户被短信轰炸,受到骚扰。

解决方案:

1.前端进行控制

​ 在点击了获取验证码后,前端隐藏此按钮或者让此按钮灰显不能点击,然后出现一个倒计时的标签,等倒计时结束后才允许点击获取验证码按钮。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B3qv0584-1673429158548)(E:\我的工作文档\服务端文档\技术文档\java开发漏洞\微信截图_20230109175155.png)]
2.服务端进行控制

​ 只是前端进行控制,也会存在绕过倒计时限制进行接口调用的情况,例如使用其他api工具直接调用接口。服务端需要对发送短信的接口进行限制,这里使用redis来存已发送的验证码。当某个手机号请求发送验证码,先从redis中查看是否在有效期内给此号码发送过验证码,若发送过,则提示已经发送过,不再重复发送;若不存在,则新生成一个验证码,发送到此手机号上,并存到redis中。

public void sendCaptcha(String phone){
    
    
        String verCode;
        String key = RedisKeyPrefixConstants.PHONE_NUM+phone;
        Object object = redisTemplate.opsForHash().get(key, "code");
        if(null != object){
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.PROCESSING, "该用户验证码已发送,且未过期,请输入验证码登录或注册!");
        }else {
    
    
            //生成一个6位数的验证码
            Random r = new Random(System.currentTimeMillis());
            int low = 100000;
            int high = 999999;
            int code = (r.nextInt(high - low) + low);
            verCode = String.valueOf(code);
            //使用hash方式来存,因为每次验证需要单独更新可验证的次数值
            redisTemplate.opsForHash().put(key,"code",verCode);
            //设置可以验证的剩余次数
            redisTemplate.opsForHash().put(key,"num",5);
            //设置过期时间
            redisTemplate.expire(key,300,TimeUnit.SECONDS);
        }
        try {
    
    
            //调用发送短信的方法
            sendMsg(phone, verCode);
        }catch (Throwable throwable){
    
    
            //短信发送失败,删除redis记录
            redisTemplate.delete(key);
            throw WebErrorUtils.commonError(throwable, HttpStatus.PROCESSING, "短信发送失败!");
        }
    }

6.低版本Fastjson导致RCE漏洞

​ Fastjson是java的一个库,可以将java对象通过toJSONString()转化为json格式的字符串,也可以将json格式的字符串通过parseObject()转化为java对象。

​ 在springboot中引入fastjson:

 <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
 </dependency>

漏洞问题:

​ 前端使用json格式把参数传递到服务端,服务端在进行反序列化(json格式转为java对象)的时候,会进入parseField方法,进入该方法后,会调用setValue(object,value)方法,这里会执行构造的恶意代码,最终造成代码执行。Fastjson采用黑白名单的方法来防御反序列化漏洞,导致当不法用户不断发掘新的反序列化类时,就算在autoType关闭的情况下仍然可以绕过黑白名单防御机制,造成远程命令执行漏洞。

解决方案:

1.更新Fastjson到最新版本

​ 在<=1.2.68版本中,攻击者可以通过精心构造的json请求,远程执行恶意代码,所以把Fastjson版本更新到最新版解决此问题。

7.SQL注入漏洞

​ 在开发的接口中,存在需要前端输入参数,服务端根据参数组织sql查询数据的情况。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OFnACZfA-1673429158549)(E:\我的工作文档\服务端文档\技术文档\java开发漏洞\微信截图_20230110181929.png)]
漏洞问题:

​ 若是服务端对用户输入数据的合法性没有判断或过滤不严,攻击者可以在事先定义好的查询语句结尾添加上额外的sql语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务执行非授权的任意查询,从而进一步得到响应的数据信息。

(1)sql拼接参数传递问题

​ 对于mybatis,组织参数到sql中,可以使用拼接符号进行参数的拼接,也可以使用#{}进行参数的占位,当进行sql预编译的时候,${}修饰的会把参数值原样拼接到sql中;#{}修饰的会使用占位符?占位,并把参数值作为参数传递进去,当参数为字符串类型时,会默认给参数值加上单引号。

​ 例如以下查询sql:

select * from user where id = ${id}

若参数值id=1 or 1=1,则经过编译后的sql为:

select * from user where id = 1 or 1=1

此时会查询出所有的用户数据,导致信息泄露。

(2)没法使用#{}设置参数的order by排序字段

​ 当sql的查询结果支持多种方式排序,且排序字段和排序方式由前端进行传递时,若是我们使用#{}的方式来占位参数,编译出来的sql是错误的,编译之后会给此排序字段加上单引号。

​ 例如以下查询sql:

select * from user order by #{orderFiled} #{order}

若参数值orderFiled = ‘created_time’,order=‘desc’,经过编译后的sql为:

select * from user order by  'created_time' 'desc'

此sql是有问题的,排序字段和排序方式不能加单引号。那只能使用${}进行sql拼接的方式,此时若参数值orderFiled = ‘created_time’,order=‘desc,(SELECT*FROM(SELECT+SLEEP(3)UNION/**/SELECT+1)a)’,经过编译后的的sql为:

select * from user order by  created_time desc,(SELECT*FROM(SELECT+SLEEP(3)UNION/**/SELECT+1)a)

此时mysql会被恶意查询拖延时间,甚至拖垮mysql服务器。

解决方案:

1.sql组织时尽量使用#{}占位符代替${}拼接符;

2.动态字段排序order by这样没法使用#{}的情况,可以使用以下两种方式处理:

①代码中判断:服务端收到参数后,对参数的值进行校验,判断是否属于允许的值,允许才放行;校验通过后拼接sql即可使用${}的方式。

//校验排序字段,也可以定义枚举值的方式判断
if(!("created_time".equalsIgnoreCase(orderFiled) || "updated_time".equalsIgnoreCase(orderFiled))){
    
    
    throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "排序字段非法");
 }
  
//校验排序方式,也可以定义枚举值的方式判断
if(!("desc".equalsIgnoreCase(order) || "asc".equalsIgnoreCase(order))){
    
    
    throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "排序方式非法");
}

②组织sql时判断:判断参数的值是否等于系统约定的值,等于才进行拼接,不等于使用默认的排序方式。

SELECT * FROM USER 
ORDER BY 
<choose> 
    <WHEN  test="(orderFiled=='created_time' or orderFiled=='updated_time') and (order=='desc' or order=='asc')">
       ${orderFiled}  ${order} 
    </WHEN > 
    <otherwise>
        updated_time DESC,created_time DESC 
    </otherwise>
</choose> 

8.水平越权信息泄露

​ 服务端给出的接口,基本都是根据参数查询、删除、更新、添加数据,用得比较多的是根据某个id去操作数据,而出于性能考虑,id一般都采用整数自增的方式,这样就会存在id被枚举的情况。

漏洞问题:

​ 若是对某条记录的操作不加权限的验证,直接返回此参数对应的记录或者修改记录,则会越权操作本来不属于此用户的记录,导致信息会被恶意用户盗取甚至篡改其他合法用户的信息。

(1)通过枚举访问资源

​ 有些场景下,用户发布一个对外的链接地址(不需要登录直接访问),此链接对应的资源只想让他熟悉的人知道,用户会把此链接单独发给他信赖的人。若是此链接地址的结尾是一个自增型的整数,则用户通过枚举结尾,就能轻松访问到其他用户发布的链接资源。

​ 例如以下的链接地址:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qetq5IxU-1673429158550)(E:\我的工作文档\服务端文档\技术文档\java开发漏洞\微信截图_20230110174635.png)]
(2)越权查询记录

​ 用户使用系统时,可以通过浏览器查看调用了哪些接口,以及各个接口传递的参数。若是对访问的记录不加权限验证,则可能出现越权查看其他用户记录的情况。

​ 例如以下的接口访问数据:

当枚举id的值进行访问,没加权限验证,则可以越权访问记录。

解决方案:

1.对外公开的链接资源,结尾地址使用uuid,避免被枚举;

2.对于系统资源权限的验证,可以结合着项目当中引入的框架来定,例如使用spring security,在接口上使用注解的方式加上权限的标识,在管理后台对用户进行权限的分配,一般都是用户与角色挂钩,角色与菜单和权限挂钩;

3.对于比较细粒度的权限校验,可以结合着redis来实现,判断此记录是否属于此人的资源,属于才让访问,不属于则抛出异常。可以在记录产生的时候直接加到redis中,并且与某个用户进行关联,当操作此记录的时候,再从redis中取出,看此用户是否有权限操作。

//判断登录用户是否有权操作此记录
private boolean checkXXXById(Integer id) {
    
    
        //获取到当前用户
        String currentUserId = xxx.getLoginIdAsString();
        //组织redis的key
        String key = RedisKeyPrefixConstants.USER_XXX_ID_SET+currentUserId;
        Boolean aBoolean = redisTemplate.hasKey(key);
        if(aBoolean) {
    
       //存在此key的缓存值
            Boolean member = redisTemplate.opsForSet().isMember(key, id);
            if(member) {
    
        //set集合中存在此值
                return true;
            }
        }else {
    
       //不存在此缓存的值,则查询数据库
            List<Integer> xxxIdList = xxxkDao.getxxxIdList(currentUserId);  //
            if(null != xxxIdList && xxxIdList.size() > 0){
    
    
                //把查询结果放到redis中
                redisTemplate.opsForSet().add(key,xxxIdList.toArray());
                //判断从数据库中查询的记录是否包含此id,包含则放行
                if(xxxIdList.contains(id)){
    
    
                    return true;
                }
            }
        }
        return false;
    }

9.权限绕过漏洞

​ 在项目开发中,会有一些需求要等第一步校验通过,返回成功状态码之后,才进行第二步的操作。例如访问一个带密码的链接,需要先等密码验证通过后才加载资源。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-clD316Vf-1673429158551)(E:\我的工作文档\服务端文档\技术文档\java开发漏洞\微信截图_20230110183437.png)]
漏洞问题:

​ 当第一步校验后,抓包修改返回值,然后放包,即可绕过权限进行第二步的加载,导致攻击者访问未经授权的资源。

(1)抓包修改校验结果访问资源

​ 对于需要密码访问的链接,随便输入密码,对校验结果进行抓包,修改状态值为通过, 然后放包,即可查看到资源。

解决方案:

1.校验密码是否正确的时候,生成一个uuid的会话状态码,以此uuid+这个链接id作为redis的key值,把校验结果作为value存放到redis中,并设置过期时间为3秒,然后把校验结果和uuid状态码返回给前端;

2.执行第二步的时候,需要把第一步的uuid值、链接id作为参数传到服务端,服务端根据这两个参数组织redis的key,查询redis是否有此记录,若是没有,直接返回,说明是伪造参数或者已经过期;若是存在,则判断第一步校验的结果状态值,通过则放行,否则返回。

(1)第一步校验密码逻辑

 public XXX checkVisitPassword(String id, String visitPassword) {
    
    
        if(StringUtils.isEmpty(id)) {
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "id参数为空,请确保参数的准确性");
        }
        if(StringUtils.isBlank(visitPassword)) {
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "密码为空,请确保参数的准确性");
        }
        //查询是否存在此id与密码的值
        YYY yyy = yyyDao.getYyyByIdAndVisitPassword(id,visitPassword);
        XXX xxx = new XXX();
        xxx.setId(id);
        //生成uuid会话值
        String sessionId = IdUtil.simpleUUID();
        xxx.setSessionId(sessionId);
        //校验结果状态值
        String state = "ok";
        if(null == yyy) {
    
      //不通过
            xxx.setCode(1);
            state = "error";
        } else {
    
           //通过
            xxx.setCode(0);
        }
       //校验结果状态值存到redis中,过期时间3秒
               redisTemplate.opsForValue().set(RedisKeyPrefixConstants.VIEW_REQUESTID_STATE+id+":"+sessionId,state,3, TimeUnit.SECONDS);
        return xxx;
    }

(2)第二步获取资源逻辑

public XXX getMessage(String id,String sessionId) {
    
    
        if(StringUtils.isEmpty(id)) {
    
    
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "id参数为空,请确保参数的准确性");
        }
        if(StringUtils.isEmpty(sessionId)){
    
             //会话id为空
            throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "sessionId参数为空,请确保参数的准确性");
         }
         String key = RedisKeyPrefixConstants.VIEW_REQUESTID_STATE+id+":"+sessionId;
         Object object = redisTemplate.opsForValue().get(key);
         if(null == object) {
    
       //redis缓存中不存在此记录
              throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "输入密码的会话已失效,请重新加载访问");
         } else {
    
    
            redisTemplate.delete(key);
            String state = object.toString();
            if(StringUtils.isEmpty(state)){
    
             //回话状态为空
                throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "输入密码的会话已失效,请重新加载访问");
            }
            if("error".equals(state)){
    
    
                 throw WebErrorUtils.commonError(null, HttpStatus.UNPROCESSABLE_ENTITY, "输入的密码错误,请重新加载访问");
             }
         }
        //校验通过,查询数据返回
        XXX xxx = xxxService.findById(id);
        return xxx;
    }

猜你喜欢

转载自blog.csdn.net/ZHANGLIZENG/article/details/128648247
今日推荐