企业级管理平台横向越权问题及防护
横向越权简介
在企业级管理平台的开发过程中,安全性是至关重要的考量因素之一。其中,横向越权(Horizontal Privilege Escalation)是一种常见的安全漏洞,它允许用户访问或操作不属于自己的资源,但与自己的资源处于同一权限级别。本文将详细介绍横向越权的概念、示例以及防护措施,帮助开发者更好地理解和应对这一问题。
横向越权是指攻击者在没有提升自身权限的情况下,通过篡改请求参数等方式,访问或操作其他用户的数据。与纵向越权(Vertical Privilege Escalation)不同,纵向越权是攻击者通过漏洞提升自己的权限,从而访问更高权限级别的数据。
横向越权通常发生在多租户系统中,例如企业级管理平台,每个用户都属于特定的组织或项目,但系统没有正确地限制用户只能访问属于自己的数据。这种漏洞可能导致数据泄露、数据篡改等严重后果。
横向越权示例
假设我们有一个企业级管理平台,用户可以通过以下 URL 访问项目详情:
GET /project/{projectId}
其中 {projectId}
是项目 ID。正常情况下,用户只能访问自己所属项目的详情。然而,如果系统没有正确地校验用户是否有权访问该项目,攻击者可以通过篡改 {projectId}
来访问其他项目的数据。例如,攻击者将 URL 改为:
GET /project/123
如果系统没有进行权限校验,攻击者就可以访问项目 ID 为 123 的项目详情,即使这个项目并不属于他。
横向越权防护
设计与实现 URL 统一标准格式
为了更好地管理权限和防止横向越权,建议采用统一的 URL 标准格式。例如:
GET /org/{orgId}/project/{projectId}
这种格式明确指定了组织 ID(orgId
)和项目 ID(projectId
),便于后续的权限校验。在实现时,可以通过中间件或拦截器来解析 URL 中的参数,并进行相应的权限检查。
保证 userId 可靠性
用户 ID(userId
)是权限校验的关键参数,必须保证其可靠性和不可篡改性。建议从用户登录时生成的令牌(Token)中提取 userId
。例如,使用 JWT(JSON Web Token)作为令牌,可以在 Token 的 payload 中存储 userId
,并在每次请求时从 Token 中解析出 userId
,从而确保其来源可靠。
public class JwtUtil {public static String getUserIdFromToken(String token) {// 解析 Token 并获取 userIdClaims claims = Jwts.parser().setSigningKey("secretKey").parseClaimsJws(token).getBody();return claims.getSubject();}
}
项目横向越权防护过滤器(全局防护)
全局防护过滤器可以对所有请求进行统一的权限校验,确保用户只能访问自己所属的项目数据。大致可以分为以下两种情况:
- 查询或操作单一资源
- 范围查询资源(例如,资源列表)
查询或操作单一资源
GET /org/{orgId}/project/{projectId}
必须携带 组织id 和 项目id。过滤器可以直接根据当前用户的 userId
强制校验其操作的目标项目数据是否属于该用户。如果项目数据不属于当前用户,则直接拒绝访问。
/*** 检查项目越权** @param orgId 组织id* @param projectId 项目id* @param userId 用户id*/private void checkProjectPE(Long orgId, Long projectId, Long userId) {// 检查组织项目关系LambdaQueryWrapper<ProjectDo> projectWrapper = Wrappers.lambdaQuery();projectWrapper.eq(ProjectDo::getOrganizationId, orgId);projectWrapper.eq(ProjectDo::getProjectId, projectId);Integer projectCount = projectMapper.selectCount(projectWrapper);if (projectCount <= 0) {throw new UnauthorizedException("当前组织下不存在该项目");}// 检查组织越权LambdaQueryWrapper<OrgWithAccount> lambdaQueryWrapper = Wrappers.lambdaQuery();lambdaQueryWrapper.eq(OrgWithAccount::getOrgId, orgId);lambdaQueryWrapper.eq(OrgWithAccount::getAccountId, userId);Integer orgWithAccountCount = orgWithAccountMapper.selectCount(lambdaQueryWrapper);if (orgWithAccountCount <= 0) {throw new UnauthorizedException("当前组织没有操作权限");}// 检查项目越权LambdaQueryWrapper<ProjectWithAccountDo> projectWithAccountWrapper = Wrappers.lambdaQuery();projectWithAccountWrapper.eq(ProjectWithAccountDo::getProjectId, projectId);projectWithAccountWrapper.eq(ProjectWithAccountDo::getAccountId, userId);Integer projectWithAccountCount = projectWithAccountMapper.selectCount(projectWithAccountWrapper);if (projectWithAccountCount <= 0) {throw new UnauthorizedException("当前项目没有操作权限");}}
范围查询资源
大致可以分为以下两种情况:
- 子公司范围查询:
GET /org/{orgId}/project/-1
- 总公司范围查询:
GET /org/-1/project/-1
为了提高系统的灵活性和易用性,引入一个特殊语义参数 -1
。当用户请求的项目 ID 为 -1
时,系统自动将该用户有权限的所有项目 ID 列表注入上下文,供后续业务逻辑进行批量或范围查询。
/*** 检查项目越权** @param orgId 组织id* @param projectId 项目id* @param userId 用户id*/private void checkProjectPE(Long orgId, Long projectId, Long userId) {// 检查组织项目关系if (!ID_WITH_FIND_ALL_AUTHORIZED_RESOURCES.equals(orgId) && !ID_WITH_FIND_ALL_AUTHORIZED_RESOURCES.equals(projectId)) {LambdaQueryWrapper<ProjectDo> projectWrapper = Wrappers.lambdaQuery();projectWrapper.eq(ProjectDo::getOrganizationId, orgId);projectWrapper.eq(ProjectDo::getProjectId, projectId);Integer projectCount = projectMapper.selectCount(projectWrapper);if (projectCount <= 0) {throw new UnauthorizedException("当前组织下不存在该项目");}}// 检查组织越权if (!ID_WITH_FIND_ALL_AUTHORIZED_RESOURCES.equals(orgId)) {LambdaQueryWrapper<OrgWithAccount> lambdaQueryWrapper = Wrappers.lambdaQuery();lambdaQueryWrapper.eq(OrgWithAccount::getOrgId, orgId);lambdaQueryWrapper.eq(OrgWithAccount::getAccountId, userId);Integer orgWithAccountCount = orgWithAccountMapper.selectCount(lambdaQueryWrapper);if (orgWithAccountCount <= 0) {throw new UnauthorizedException("当前组织没有操作权限");}}// 检查项目越权if (!ID_WITH_FIND_ALL_AUTHORIZED_RESOURCES.equals(projectId)) {LambdaQueryWrapper<ProjectWithAccountDo> projectWithAccountWrapper = Wrappers.lambdaQuery();projectWithAccountWrapper.eq(ProjectWithAccountDo::getProjectId, projectId);projectWithAccountWrapper.eq(ProjectWithAccountDo::getAccountId, userId);Integer projectWithAccountCount = projectWithAccountMapper.selectCount(projectWithAccountWrapper);if (projectWithAccountCount <= 0) {throw new UnauthorizedException("当前项目没有操作权限");}}}/*** 填充信息到上下文** @param context 上下文* @param userId 用户id* @param orgId 组织id* @param projectId 项目id*/private void fillToContext(RequestContext context, Long userId, Long orgId, Long projectId) {context.setUserId(userId);Account account = accountMapper.selectById(userId);Optional.ofNullable(account).map(Account::getAccountName).ifPresent(context::setUserName);List<String> roleNames = roleMapper.listRoleNameByUserId(userId);if (roleNames.stream().anyMatch(CommonConstant.ROLE_ROOT.getCode()::equals)) { // 超级管理员拥有所有组织权限context.setIsAdmin(true);}// 填充组织id到上下文 非superAdmin场景,一个用户只会在一个组织下if (ID_WITH_FIND_ALL_AUTHORIZED_RESOURCES.equals(orgId)) {LambdaQueryWrapper<OrgWithAccount> orgWithAccountWrapper = Wrappers.lambdaQuery();orgWithAccountWrapper.eq(OrgWithAccount::getAccountId, userId);orgWithAccountWrapper.select(OrgWithAccount::getOrgId);context.setOrgId(orgWithAccountMapper.selectList(orgWithAccountWrapper).stream().limit(1).map(OrgWithAccount::getOrgId).findFirst().orElseThrow(() -> new UnauthorizedException("当前用户不在任何组织下")));} else {context.setOrgId(orgId);}// 填充项目id到上下文// (1.超级管理员处于总公司下,总公司下没有项目 2.超级管理员可以操作任何资源)if (!ID_WITH_FIND_ALL_AUTHORIZED_RESOURCES.equals(projectId)) {context.setProjectIds(Collections.singletonList(projectId));} else if(context.getIsAdmin()) {context.setProjectIds(Collections.emptyList());} else {LambdaQueryWrapper<ProjectWithAccountDo> projectWithAccountWrapper = Wrappers.lambdaQuery();projectWithAccountWrapper.eq(ProjectWithAccountDo::getAccountId, userId);projectWithAccountWrapper.select(ProjectWithAccountDo::getProjectId);List<Long> projectIds = projectWithAccountMapper.selectList(projectWithAccountWrapper).stream().map(ProjectWithAccountDo::getProjectId).collect(Collectors.toList());if (CollectionUtils.isEmpty(projectIds)) {throw new UnauthorizedException("当前用户不在任何项目下");}context.setProjectIds(projectIds);}}
资源横向越权防护(按需防护)
- 如果项目中只有单一资源,则可以直接在上面的过滤器中同时对资源进行横向越权防护;
- 如果系统中存在多种资源,例如,设备(
sn
)、任务(taskId
)、应用(appId
)等。可以在 Controller 层 按需实现 横向越权防护。
/*** 设备基础控制器** @author xxx* @since 2025-08-20*/
public abstract class DeviceBaseController extends BaseController {// 入参中有可能出现的资源keyprivate static final List<String> PARAM_SN_FIELD_NAMES = Arrays.asList("sn", "deviceId", "deviceIds", "pushTarget","deviceSnList");@Resourceprivate DeviceMapper deviceMapper;/*** 横向越权防护** @param request request* @throws IOException io异常*/@ModelAttributepublic void checkResource(HttpServletRequest request) throws IOException {List<String> snList = extractFromRequest(request, PARAM_SN_FIELD_NAMES);// 检查sn是否越权checkSnPE(snList);}/*** 检查Sn越权** @param snList sn列表*/private void checkSnPE(List<String> snList) {snList.forEach(sn -> {LambdaQueryWrapper<Device> deviceWrapper = Wrappers.lambdaQuery();WrapperUtils.fillProjectToQueryWrapper(deviceWrapper);deviceWrapper.eq(Device::getSn, sn);Device device = deviceMapper.selectOne(deviceWrapper);if (device == null) {throw new UnauthorizedException("当前设备不存在或没有操作权限:" + sn);}});}
}
完整代码实现
横向越权防护代码实现
总结
横向越权是一种常见的安全漏洞,可能导致严重的数据泄露和篡改问题。通过采用统一的 URL 标准格式、保证 userId
的可靠性、实现全局防护过滤器以及按需进行资源防护,可以有效防止横向越权的发生。在实际开发过程中,开发者应始终将安全性放在首位,确保系统能够抵御各种安全威胁。
至此,本次分享到此结束啦!!!