이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 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의 rolepermissions를 그대로 저장하면 권한 상승이 발생한다.

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_DELETE

role은 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_WRITE

role 변경은 별도 정책으로 다룬다.

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=4d901ab2
level=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을 직접 넣어 서버 정책을 우회할 수 있기 때문이다.

참고 문서