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

  • ABAC는 RBAC로 부족한 어떤 상황을 보완하는가?
  • Resource Ownership과 tenant 경계를 서버에서 어떻게 검증해야 하는가?
  • 속성 기반 권한 정책에서 클라이언트 입력을 믿으면 어떤 문제가 생기는가?

개요

ABAC는 Attribute Based Access Control이다. 권한 판단에 actor, resource, action, context의 속성을 함께 사용한다.

RBAC가 “이 사용자는 SUPPORT_AGENT role인가”를 묻는다면, ABAC는 더 구체적으로 묻는다.

actor.department == resource.assigned_department
resource.status != CLOSED
context.mfa_verified == true
context.request_time within support_hours
context.ticket_id assigned to actor

Resource Ownership은 ABAC에서 가장 자주 등장하는 속성이다. 누가 이 resource의 소유자인가, 같은 조직인가, 같은 tenant인가를 서버가 확인해야 한다.

공격 시나리오

고객센터 상담원은 자기에게 배정된 티켓의 회원 정보만 볼 수 있다.

공격자는 상담원 계정으로 로그인한 뒤 다음을 시도한다.

  • memberId를 유명인 계정으로 바꾼다.
  • ticketId를 아무 값으로 넣는다.
  • assigned=true 같은 클라이언트 속성을 추가한다.
  • 업무 시간이 아닌 시간에 대량 조회한다.

서버가 role만 보고 SUPPORT_AGENT면 조회를 허용하면 모든 회원 정보가 노출된다.

ABAC가 필요한 이유는 role 하나로는 “이 상담원이 이 회원을 지금 볼 수 있는가”를 설명할 수 없기 때문이다.

취약한 요청/응답 예시

GET /admin/support/members/9182?ticketId=T-0000&assigned=true HTTP/1.1
Host: admin.example.com
Authorization: Bearer support-agent-token
HTTP/1.1 200 OK
Content-Type: application/json
 
{
  "memberId": 9182,
  "email": "victim@example.com",
  "phone": "010-1234-5678",
  "securityNote": "fraud watch"
}

서버가 assigned=true를 클라이언트 입력으로 믿거나 ticket 배정을 확인하지 않으면 ABAC는 동작하지 않는다.

주제별 핵심 판단

  • ABAC는 actor, resource, action, context 속성을 함께 평가한다.
  • Resource ownership은 body의 ownerId가 아니라 서버가 조회한 resource 속성에서 나온다.

개선된 요청/응답 예시

ABAC 정책은 속성 출처를 분명히 해야 한다.

  • 개선된 조회는 resource.ownerId == principal.id나 tenant 조건을 서버 조회 결과로 확인한다.
  • 거부 로그에는 actor, resource, action, context, reason을 분리해 남긴다.

방어 설계

actor:
  - account_id: authentication principal
  - role: server permission store
  - department: HR system or internal DB
 
resource:
  - member_id: path variable
  - assigned_ticket: support DB
  - security_note: sensitive field
 
context:
  - request_time: server clock
  - mfa_verified: session state
  - source_ip: trusted request metadata

클라이언트가 보낸 속성은 후보값일 뿐이다. 권한 결정 속성은 서버가 조회하거나 계산해야 한다.

고객센터 조회 정책은 다음처럼 쓸 수 있다.

allow SUPPORT_AGENT read member_contact
if ticket is assigned to actor
and ticket.status == OPEN
and reason_code is present
deny member_security_note unless actor has FRAUD_ANALYST permission

Spring/HTTP 예시

@GetMapping("/admin/support/members/{memberId}")
public SupportMemberResponse readForSupport(
        @AuthenticationPrincipal AdminPrincipal principal,
        @PathVariable long memberId,
        @RequestParam String ticketId,
        @RequestParam String reasonCode) {
 
    return supportMemberService.readMember(
        new SupportReadCommand(principal.adminId(), memberId, ticketId, reasonCode));
}
@Transactional(readOnly = true)
public SupportMemberResponse readMember(SupportReadCommand command) {
    SupportAssignment assignment = supportAssignmentRepository.findOpenAssignment(
        command.ticketId(),
        command.memberId(),
        command.actorAdminId()
    ).orElseThrow(() -> denied(command, "ticket_not_assigned"));
 
    SupportPolicyDecision decision = supportPolicy.decide(
        assignment.actor(),
        assignment.member(),
        Action.READ_MEMBER_CONTACT,
        command.reasonCode());
 
    audit.supportAccessDecision(command, decision);
 
    if (decision.denied()) {
        throw new AccessDeniedException("support access denied");
    }
 
    return supportMemberRepository.findMaskedMember(command.memberId());
}

DB 조건에도 ownership 또는 assignment를 넣는다.

SELECT m.member_id, m.email_masked, m.phone_masked
FROM member_support_view m
JOIN support_ticket_assignment a ON a.member_id = m.member_id
WHERE m.member_id = ?
  AND a.ticket_id = ?
  AND a.agent_id = ?
  AND a.status = 'OPEN';

운영 로그와 감사 지점

level=WARN event=abac_denied result=blocked actor_id=17 actor_role=SUPPORT_AGENT resource_type=member resource_id=9182 action=read_member_contact ticket_id=T-0000 reason=ticket_not_assigned trace_id=9e130bc1
level=INFO event=abac_allowed result=allowed actor_id=17 resource_type=member resource_id=7123 action=read_member_contact ticket_id=T-4421 reason_code=customer_call fields=email_masked,phone_masked trace_id=9e130bc2

ABAC 로그는 정책에 사용된 주요 속성을 남겨야 한다. 단, 보안 메모 본문이나 전체 개인정보는 남기지 않는다.

실전 판단 기준

  • support agent가 배정되지 않은 ticket으로 회원을 조회하면 거부되는가?
  • 같은 role 사용자끼리 resource id를 바꿔도 접근할 수 없어야 한다.
  • 시간, 조직, 상태 같은 context 조건은 테스트 데이터에 포함되어야 한다.

테스트 포인트

  • assigned=true, department=fraud 같은 클라이언트 속성이 무시되는가?
  • ticket이 CLOSED 상태면 같은 상담원도 거부되는가?
  • 보안 메모 조회에는 별도 permission이나 step-up이 필요한가?
  • list 조회와 detail 조회가 같은 tenant/assignment 조건을 쓰는가?
  • ABAC 거부 로그에 사용된 속성과 reason이 남는가?

위험 신호!

  • role만 보고 모든 resource 접근을 허용한다.
  • tenant id, owner id, assigned flag를 클라이언트가 보낸 값으로 사용한다.
  • list API에는 tenant 조건이 있지만 detail API에는 없다.
  • ABAC 정책이 코드 조건문에 흩어져 테스트하기 어렵다.
  • 속성 출처가 문서화되어 있지 않다.
  • 고객센터 대리 조회가 일반 조회 로그와 구분되지 않는다.

확인 질문

확인 질문

  • ABAC가 필요한 대표 상황은 무엇인가?
    • role만으로는 actor와 resource의 관계, 상태, 시간, MFA, 위험도를 설명하기 어려운 상황이다.
  • Resource Ownership 검증은 어디에 들어가야 안전한가?
    • Service policy와 Repository 조회 조건 모두에 principal context가 들어가는 편이 안전하다.
  • 클라이언트가 보낸 속성을 ABAC에 쓰면 안 되는 이유는 무엇인가?
    • 공격자가 속성을 조작해 정책 판단을 원하는 방향으로 바꿀 수 있기 때문이다.

참고 문서