API横向越权修复之ID加密

  • 横向越权

横向越权一般发生在应用系统做了【认证】,但没有做【鉴权】的情况下,也是最常见的漏洞之一。

  • 认证:即识别是否有权限访问系统;
  • 鉴权:即识别在系统中的权限是什么;

例如:

// 访问某数据查询接口,接口返回ID为123的数据信息

POST : https://xxxx/iservice/queryInfo?detail_id=123 

请求接口时一般都会要求携带TOKEN,无论是JWT还是RSA的,至少不会是裸奔。这里的TOKEN就是【认证】信息,接口通过TOKEN去判断当前用户是否有请求接口的权限。但如果接口中没有做【鉴权】则会发生横向越权,用户通过修改detail_id的值就可以遍历DB中的所有记录

解决的思路:

  • 建立数据权限:常见的有:【RBAC:role based access control】, 基于角色的权限控制,一般用户不被直接授予权限,而是通过Role去获取权限。将数据的访问权限与角色绑定,用户拥有什么角色才能看到什么数据,这样即使遍历接口也只能查询到当前用户自己的数据;
  • ID加密:如例子中的detail_id,如果我们换成uuid,或其他无规则的值,也可以降低被遍历的可能性;

建立完善的权限策略是控制越权最合适的方法,但很多系统已经维护了很多年,里面的功能很庞大,往里面集成权限策略难度较大,需要去定义角色,梳理业务数据与角色的关系,然后开发权限管理功能,再挨个功能去添加鉴权;这里提供ID加密的方式去处理横向越权。

  • 目的:

      1、对原代码(业务)入侵小;
      2、降低数据遍历风险;
      3、投入人天小;
    
  • 代码实现思路

通过全局拦截API入参与返回值,对可遍历字段进行加解密。无需前端参与,后端返回数据时,对字段进行加密,加密算法保存在后端,前端使用加密字段进行后续业务处理,后端接口入参接收时进行解密。

序号 业务字段1 业务字段2 业务字段3 行ID【非业务字段,对用户不可见】
1 xxx xxx xxx wMul8LwP =》 实际值:123
2 xxx xxx xxx 3vRRDk6X =》 实际值:124
3 xxx xxx xxx TbxJ3IAe =》 实际值:125

用户选择查看序号1的行时,请求后端返回详细数据,接口如下:

https://xxxx/iservice/queryInfo?detail_id=wMul8LwP

此时如果要恶意遍历接口的话,难度相对较高,还可以将ID的加密强度提升来提供安全性。

  • 代码实现

以下均基于JAVA语言+springboot框架实现。通过反射,在拦截中判断字段是否有加密或解密注解,进行对应的加解密操作后流转。

  • 自定义注解
/**
 * 字段解密
 * @author lu
 */
@Target({
   ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Decrypt {
   

}
	解密在接口入参中使用,一般为RO对象,或者是基础类型的参数,所以作用域为FIELD或PARAMETER
/**
 * 字段加密
 * @author lu
 */
@Target({
   ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypt {
   

}
	加密在返回值(VO)中使用,一般都为对象,所以注解作用域为FIELD;
  • 加密算法

    加密强度自己选择,这里以DES加密为例

/**
 * 加解密
 * @author lu
 */
@Slf4j
public class DesUtil {
   

    public static final String SECURITY_KEY = "IxDQ4e5bCEY";
    public static String encrypt(String info) {
   
        byte[] key = new byte[0];
        try {
   
            key = new BASE64Decoder().decodeBuffer(SECURITY_KEY);
        } catch (IOException e) {
   
            log.error("加密失败",e);
        }
        DES des = SecureUtil.des(key);
        String encrypt = des.encryptHex(info);
        return encrypt;
    }

    public static String decode(String encrypt) {
   
        byte[] key = new byte[0];
        try {
   
            key = new BASE64Decoder().decodeBuffer(SECURITY_KEY);
        } catch (IOException e) {
   
            log.error("解密失败",e);
        }
        DES des = SecureUtil.des(key);
        return des.decryptStr(encrypt);
    }
}
  • 接口返回值加密

      responseBodyAdvice —— 响应体的统一处理器,一般用来统一返回值使用。这里用于返回值字段加密。
    
/**
 * 返回值字段加密
 * @author lu
 */
@Slf4j
@RestControllerAdvice
public class ResponseEncryptAdvice implements ResponseBodyAdvice {
   

    /** 此处如果返回false , 则不执行当前Advice的业务 */
    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
   
        return true;
    }

    /**
     * @title 写返回值前执行
     *
     * */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
   
        try {
   
            // 获取data类型
            Class clazz = body.getClass();
            // 是否是集合
            boolean isCollectionType = Collection.class.isAssignableFrom(clazz);
            if(isCollectionType){
   
                return encodeList(body);
            }else{
   
                return encode(body);
            }
        }catch (Exception e){
   
            log.error("请求后置处理异常",e);
        }
        return body;
    }

    /**
     * 递归加密
     */
    private JSONObject encode(Object object) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
   
        // 获取data类型
        Class clazz = object.getClass();
        // 转成JSON处理,字段加密后数据类型会变,原类无法处理
        JSONObject jsonObject = (JSONObject) JSONObject.toJSON(object);
        // 递归遍历类里及父类所有属性,找到所有带加密注解的字段
        Field[] fields = FieldsUtils.getClassAllFields(clazz);
        for (Field field : fields) {
   
            // 获取字段值
            field.setAccessible(true);
            Object val = field.get(object);
            if(val==null){
   
                // 空值不处理
                continue;
            }
            // final 修饰的跳过!!!,避免出现递归死循环的问题,例如:PageInfo
            int modify = field.getModifiers();
            if(Modifier.isFinal(modify)){
   
                continue;
            }
            // 字段类型
            Class valClass = val.getClass();
            // 是否是集合
            boolean isCollectionType = Collection.class.isAssignableFrom(valClass
  • 19
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值