이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Role과 Permission은 어떻게 다르고 왜 함께 관리해야 하는가?
- role 변경 API에서 클라이언트가 보낸 권한 값을 그대로 믿으면 어떤 공격이 생기는가?
- RBAC 정책을 코드, DB, 감사 로그로 어떻게 검증 가능하게 만드는가?
개요
RBAC는 Role Based Access Control이다. 사용자가 가진 role을 기준으로 권한을 판단한다.
Role은 직무나 책임 묶음에 가깝다. 예를 들어 TEAM_OWNER, BILLING_MANAGER, SUPPORT_AGENT가 role이다.
Permission은 실제 허용 action에 가깝다. 예를 들어 TEAM_MEMBER_INVITE, BILLING_READ, BILLING_WRITE, USER_SECURITY_NOTE_READ가 permission이다.
실무에서는 role을 permission 묶음으로 관리하는 편이 안전하다. role 이름만 보고 코드 곳곳에서 분기하면 role이 늘어날수록 정책이 불투명해진다.
공격 시나리오
팀 협업 서비스에서 TEAM_OWNER는 구성원을 초대하고 결제 정보를 수정할 수 있다. TEAM_MAINTAINER는 구성원을 초대할 수 있지만 owner를 만들 수 없다.
공격자는 maintainer 계정으로 로그인한다.
프론트엔드 요청을 관찰해 role 변경 API를 찾고, body에 더 높은 role과 permission을 넣는다.
서버가 “maintainer도 팀 관리 화면에 접근 가능하다”는 이유로 body의 role과 permissions를 그대로 저장하면 권한 상승이 발생한다.
RBAC 공격은 단순히 role 이름을 바꾸는 문제가 아니다. “누가 어떤 role을 누구에게 부여할 수 있는가”라는 meta-permission이 빠질 때 생긴다.
취약한 요청/응답 예시
PATCH /api/teams/42/members/1004/role HTTP/1.1
Host: api.example.com
Authorization: Bearer maintainer-token
Content-Type: application/json
{
"role": "TEAM_OWNER",
"permissions": ["TEAM_DELETE", "BILLING_WRITE", "ROLE_ASSIGN"]
}HTTP/1.1 200 OK
Content-Type: application/json
{
"teamId": 42,
"memberId": 1004,
"role": "TEAM_OWNER",
"permissions": ["TEAM_DELETE", "BILLING_WRITE", "ROLE_ASSIGN"]
}서버가 permission catalog를 기준으로 판단하지 않고 클라이언트가 보낸 permission 배열을 저장하면 RBAC는 무너진다.
주제별 핵심 판단
- Role은 권한 묶음이고 Permission은 실제 action 단위다.
- Role 이름이 많아질수록 권한 매트릭스와 변경 감사가 더 중요해진다.
개선된 요청/응답 예시
먼저 permission catalog를 둔다.
- 개선된 권한 검사는
ADMIN문자열 비교보다order.refund.approve같은 action permission을 확인한다. - role 변경 API는 이전 role, 새 role, 변경자, 사유, 승인 정보를 로그로 남긴다.
방어 설계
TEAM_MEMBER_READ
TEAM_MEMBER_INVITE
TEAM_MEMBER_REMOVE
TEAM_ROLE_ASSIGN_MAINTAINER
TEAM_ROLE_ASSIGN_OWNER
BILLING_READ
BILLING_WRITE
TEAM_DELETErole은 permission 묶음으로 정의한다.
TEAM_MEMBER:
- TEAM_MEMBER_READ
TEAM_MAINTAINER:
- TEAM_MEMBER_READ
- TEAM_MEMBER_INVITE
TEAM_OWNER:
- TEAM_MEMBER_READ
- TEAM_MEMBER_INVITE
- TEAM_MEMBER_REMOVE
- TEAM_ROLE_ASSIGN_MAINTAINER
- BILLING_READ
- BILLING_WRITErole 변경은 별도 정책으로 다룬다.
TEAM_MAINTAINER can assign TEAM_MEMBER only
TEAM_OWNER can assign TEAM_MEMBER or TEAM_MAINTAINER
ORGANIZATION_ADMIN can assign TEAM_OWNER with approval클라이언트는 desired role만 보낼 수 있고, permission 목록은 서버 catalog에서 계산한다.
Spring/HTTP 예시
@PatchMapping("/api/teams/{teamId}/members/{memberId}/role")
public TeamMemberResponse changeRole(
@AuthenticationPrincipal AccountPrincipal principal,
@PathVariable long teamId,
@PathVariable long memberId,
@Valid @RequestBody ChangeRoleRequest request) {
return teamRoleService.changeRole(
new ChangeRoleCommand(principal.accountId(), teamId, memberId, request.role(), request.reason()));
}@Transactional
public TeamMemberResponse changeRole(ChangeRoleCommand command) {
TeamRole actorRole = teamMemberRepository.findRole(command.teamId(), command.actorId())
.orElseThrow(() -> denied(command, "actor_not_member"));
RoleChangeDecision decision = rolePolicy.canAssign(actorRole, command.requestedRole());
audit.roleChangeDecision(command, actorRole, decision);
if (decision.denied()) {
throw new AccessDeniedException("role change denied");
}
Set<Permission> permissions = roleCatalog.permissionsOf(command.requestedRole());
teamMemberRepository.updateRole(command.teamId(), command.targetMemberId(), command.requestedRole());
return TeamMemberResponse.of(command.targetMemberId(), command.requestedRole(), permissions);
}permissions는 request body가 아니라 roleCatalog에서 나온다.
운영 로그와 감사 지점
level=WARN event=role_change_denied result=blocked actor_id=77 actor_role=TEAM_MAINTAINER target_member_id=1004 team_id=42 requested_role=TEAM_OWNER reason=insufficient_meta_permission trace_id=4d901ab2level=INFO event=role_changed result=success actor_id=12 actor_role=TEAM_OWNER target_member_id=1004 team_id=42 previous_role=TEAM_MEMBER new_role=TEAM_MAINTAINER reason=project_onboarding trace_id=4d901ab3권한 변경 로그는 이전 값과 새 값, actor role, reason을 남겨야 한다.
실전 판단 기준
- maintainer가 owner를 부여하려 하면 거부되는가?
- role이 업무 직책인지 시스템 권한인지 섞이면 리뷰가 어려워진다.
- 권한 강등 후 기존 token claim과 cache가 갱신되는지 확인한다.
테스트 포인트
- request body의
permissions필드를 넣어도 무시되거나 거부되는가? - role catalog에 없는 role이 거부되는가?
- role 변경 후 기존 session이나 token의 권한 cache가 어떻게 갱신되는가?
- role 변경 성공과 실패가 서로 다른 감사 이벤트로 남는가?
- owner를 마지막 1명 제거하는 정책이 정의되어 있는가?
위험 신호!
- role 이름을 코드 곳곳의 문자열 비교로 직접 사용한다.
- permission 목록을 클라이언트가 보낸다.
ADMIN하나에 모든 관리 권한을 몰아넣는다.- role 변경 API가 reason, approval, previous role을 기록하지 않는다.
- role 변경 후 기존 token의 권한 claim이 오래 살아 있다.
- role을 늘리는 것으로 모든 예외 요구사항을 해결한다.
확인 질문
확인 질문
- Role과 Permission의 차이는 무엇인가?
- Role은 사용자나 직무의 권한 묶음이고, Permission은 실제 허용 action이다.
- role 변경에서 meta-permission이 필요한 이유는 무엇인가?
- 권한을 사용하는 능력과 권한을 다른 사람에게 부여하는 능력은 다른 action이기 때문이다.
- permission을 request body로 받으면 안 되는 이유는 무엇인가?
- 공격자가 더 높은 permission을 직접 넣어 서버 정책을 우회할 수 있기 때문이다.