JSH_ERP issue5: Unauthorized Role Capability Modification via `/role/*`

Vulnerability call chain

1.1 Summary

JSH_ERP has a missing authorization vulnerability in POST /role/add, PUT /role/update, POST /role/batchSetStatus, DELETE /role/delete, DELETE /role/deleteBatch. A user can weaken price masking, change data-scope type, enable disabled roles, or delete roles. This can grant broader business data visibility and alter authorization behavior for all users assigned to the affected role

  • Attack precondition: Any authenticated user
  • Affected endpoint: POST /role/add`, `PUT /role/update`, `POST /role/batchSetStatus`, `DELETE /role/delete`, `DELETE /role/deleteBatch
  • Affected authorization property: Role.type`, `Role.priceLimit`, `Role.enabled`, `Role.deleteFlag
  • Security impact: A user can weaken price masking, change data-scope type, enable disabled roles, or delete roles. This can grant broader business data visibility and alter authorization behavior for all users assigned to the affected role

1.2 Exploit path

The attacker creates or updates role records with chosen type and priceLimit, or enables/disables/deletes role records. Existing users bound to those roles inherit the modified data scope and price visibility behavior

1.3 Key code evidence

  1. jshERP-boot/src/main/java/com/jsh/erp/controller/RoleController.java

Evidence location: https://gitee.com/jishenghua/JSH_ERP/blob/master/jshERP-boot/src/main/java/com/jsh/erp/controller/RoleController.java#L68

   65          return getDataTable(list);
   66      }
   67  
   68      @PostMapping(value = "/add")
   69      @ApiOperation(value = "新增")
   70      public String addResource(@RequestBody JSONObject obj, HttpServletRequest request)throws Exception {
   71          Map<String, Object> objectMap = new HashMap<>();
   72          int insert = roleService.insertRole(obj, request);
   73          return returnStr(objectMap, insert);
   74      }
   75  
   76      @PutMapping(value = "/update")
   77      @ApiOperation(value = "修改")
   78      public String updateResource(@RequestBody JSONObject obj, HttpServletRequest request)throws Exception {
   79          Map<String, Object> objectMap = new HashMap<>();
   80          int update = roleService.updateRole(obj, request);
   81          return returnStr(objectMap, update);
   82      }
  1. jshERP-boot/src/main/java/com/jsh/erp/service/RoleService.java

Evidence location: https://gitee.com/jishenghua/JSH_ERP/blob/master/jshERP-boot/src/main/java/com/jsh/erp/service/RoleService.java#L121

  118      }
  119  
  120      @Transactional(value = "transactionManager", rollbackFor = Exception.class)
  121      public int insertRole(JSONObject obj, HttpServletRequest request)throws Exception {
  122          Role role = JSONObject.parseObject(obj.toJSONString(), Role.class);
  123          int result=0;
  124          try{
  125              role.setEnabled(true);
  126              result=roleMapper.insertSelective(role);
  127              logService.insertLog("角色",
  128                      new StringBuffer(BusinessConstants.LOG_OPERATION_TYPE_ADD).append(role.getName()).toString(), request);
  129          }catch(Exception e){
  130              JshException.writeFail(logger, e);
  131          }
  132          return result;
  133      }
  134  
  135      @Transactional(value = "transactionManager", rollbackFor = Exception.class)
  136      public int updateRole(JSONObject obj, HttpServletRequest request) throws Exception{
  137          Role role = JSONObject.parseObject(obj.toJSONString(), Role.class);
  138          int result=0;
  139          try{
  140              result=roleMapper.updateByPrimaryKeySelective(role);
  141              logService.insertLog("角色",
  142                      new StringBuffer(BusinessConstants.LOG_OPERATION_TYPE_EDIT).append(role.getName()).toString(), request);
  143          }catch(Exception e){
  1. jshERP-boot/src/main/java/com/jsh/erp/service/RoleService.java

Evidence location: https://gitee.com/jishenghua/JSH_ERP/blob/master/jshERP-boot/src/main/java/com/jsh/erp/service/RoleService.java#L216

  213      }
  214  
  215      @Transactional(value = "transactionManager", rollbackFor = Exception.class)
  216      public int batchSetStatus(Boolean status, String ids)throws Exception {
  217          logService.insertLog("角色",
  218                  new StringBuffer(BusinessConstants.LOG_OPERATION_TYPE_ENABLED).toString(),
  219                  ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
  220          List<Long> roleIds = StringUtil.strToLongList(ids);
  221          Role role = new Role();
  222          role.setEnabled(status);
  223          RoleExample example = new RoleExample();
  224          example.createCriteria().andIdIn(roleIds);
  225          int result=0;
  226          try{
  227              result = roleMapper.updateByExampleSelective(role, example);
  228          }catch(Exception e){
  229              JshException.writeFail(logger, e);
  230          }
  231          return result;
  1. jshERP-boot/src/main/java/com/jsh/erp/service/UserService.java

Evidence location: https://gitee.com/jishenghua/JSH_ERP/blob/master/jshERP-boot/src/main/java/com/jsh/erp/service/UserService.java#L832

  829              }
  830              role = roleService.getRoleWithoutTenant(Long.parseLong(roleId));
  831          }
  832          return role;
  833      }
  834  
  835      /**
  836       * 获取用户id
  837       * @param request
  838       * @return
  839       */
  840      public Long getUserId(HttpServletRequest request) throws Exception{
  841          Object userIdObj = redisService.getObjectFromSessionByKey(request,"userId");
  842          Long userId = null;
  843          if(userIdObj != null) {
  844              userId = Long.parseLong(userIdObj.toString());
  845          }
  846          return userId;
  847      }
  848  
  849      /**
  850       * 用户的按钮权限
  851       * @param userId
  1. jshERP-boot/src/main/java/com/jsh/erp/service/RoleService.java

Evidence location: https://gitee.com/jishenghua/JSH_ERP/blob/master/jshERP-boot/src/main/java/com/jsh/erp/service/RoleService.java#L240

  237       * @param type
  238       * @return
  239       */
  240      public Object parseHomePriceByLimit(BigDecimal price, String type, String priceLimit, String emptyInfo, HttpServletRequest request) throws Exception {
  241          if(StringUtil.isNotEmpty(priceLimit)) {
  242              if("buy".equals(type) && priceLimit.contains("1")) {
  243                  return emptyInfo;
  244              }
  245              if("retail".equals(type) && priceLimit.contains("2")) {
  246                  return emptyInfo;
  247              }
  248              if("sale".equals(type) && priceLimit.contains("3")) {
  249                  return emptyInfo;
  250              }
  251          }
  252          return price;
  253      }
  254  
  255      /**
  256       * 根据权限进行屏蔽价格-单据
  257       * @param price
  258       * @param billCategory
  259       * @param priceLimit
  260       * @param request
  261       * @return
  262       * @throws Exception
  263       */
  264      public BigDecimal parseBillPriceByLimit(BigDecimal price, String billCategory, String priceLimit, HttpServletRequest request) throws Exception {
  265          if(StringUtil.isNotEmpty(priceLimit)) {
  266              if("buy".equals(billCategory) && priceLimit.contains("4")) {
  267                  return BigDecimal.ZERO;
  268              }
  269              if("retail".equals(billCategory) && priceLimit.contains("5")) {
  270                  return BigDecimal.ZERO;
  271              }
  272              if("sale".equals(billCategory) && priceLimit.contains("6")) {
  273                  return BigDecimal.ZERO;
  274              }
  275          }
  276          return price;
  277      }
  278  
  279      /**
  280       * 根据权限进行屏蔽价格-物料
  281       * @param price
  282       * @param type
  283       * @return
  284       */
  285      public Object parseMaterialPriceByLimit(BigDecimal price, String type, String emptyInfo, HttpServletRequest request) throws Exception {
  286          Long userId = userService.getUserId(request);
  287          String priceLimit = userService.getRoleTypeByUserId(userId).getPriceLimit();
  288          if(StringUtil.isNotEmpty(priceLimit)) {
  289              if("buy".equals(type) && priceLimit.contains("4")) {
  290                  return emptyInfo;
  291              }
  292              if("retail".equals(type) && priceLimit.contains("5")) {
  293                  return emptyInfo;
  294              }
  295              if("sale".equals(type) && priceLimit.contains("6")) {
  296                  return emptyInfo;
  297              }
  298          }
  299          return price;
  300      }
  301  
  302      public String getCurrentPriceLimit(HttpServletRequest request) throws Exception {
  303          Long userId = userService.getUserId(request);
  304          return userService.getRoleTypeByUserId(userId).getPriceLimit();
  305      }
  306  }

2. Existing checks and why they fail

  • insertRole forces enabled=true, but does not restrict type, priceLimit, tenant, or role management authority.
  • No role-management permission check exists.
  • No field-level ceiling prevents privilege expansion.

3. Root Cause Analysis

Root Cause 1: Missing server-side authorization on the vulnerable operation.

The endpoint accepts user-controlled authorization-sensitive identifiers or fields, but the write/read path does not prove that the current caller may operate on the target object.

Root Cause 2: Missing object-scope or grant-bound validation.

The implementation relies on endpoint access, UI filtering, or object existence checks instead of enforcing target ownership, tenant boundary, role ceiling, or grantable-resource constraints at the service layer.

Restrict role CRUD to tenant administrators or dedicated role managers. Enforce field-level constraints for type, priceLimit, and enabled/deleted state. Prevent modification of roles outside the caller's management scope

5. Verification after fix

  • Unauthorized callers receive HTTP 403 or equivalent rejection.
  • Out-of-scope target identifiers are rejected before database writes or sensitive reads.
  • Role, permission, tenant, organization, ownership, or grant-bound ceilings are enforced server-side.
  • Direct HTTP requests are rejected even when front-end controls are hidden.
posted @ 2026-06-18 12:39  Aibot  阅读(4)  评论(0)    收藏  举报