웹/클라우드/인프라로 돌아가기
웹/클라우드/인프라신윤섭·2026년 8월 10일

로그인은 붙였는데 왜 남의 데이터가 보이나: 인증과 인가는 다른 일이다

공유

로그인은 됐는데, 주문 번호를 바꿨더니

AI한테 쇼핑몰 비슷한 걸 만들어달라고 했다. 회원가입도 되고, 로그인하면 내 주문 내역이 뜬다. 주문 하나를 눌러보니 주소창이 이렇게 바뀐다. myshop.com/orders/1002. 상세 페이지가 잘 나온다. 로그인 안 하면 이 페이지는 아예 안 열린다. 여기까지 확인하고 "인증 붙었으니 됐다" 하고 넘어간다.

그날 저녁, 궁금해서 주소창의 10021003으로 바꿔본다. 그러자 처음 보는 주문이 뜬다. 받는 사람 이름도, 배송지 주소도, 전화번호도 내 것이 아니다. 남의 주문이다. 1004, 1005로 바꿔도 계속 나온다. 로그인은 분명히 내 계정으로 되어 있는데, 화면에는 다른 사람들의 개인정보가 줄줄이 흘러나온다.

이상하다. 로그인을 붙였는데 왜 이런 일이 벌어질까. 답은 간단하다. 로그인은 "이 사람이 누구인지"만 확인했을 뿐, "이 사람이 이 주문을 봐도 되는지"는 아무도 확인하지 않았기 때문이다. 이 둘은 완전히 다른 질문이고, AI가 짜준 코드는 앞의 질문만 답하고 뒤의 질문은 통째로 건너뛰는 일이 아주 흔하다.

인증과 인가는 서로 다른 질문이다

여기서 두 단어를 확실히 갈라놓고 가자. 인증(authentication)과 인가(authorization)다. 발음도 비슷하고 영어 약자도 둘 다 auth로 시작해서 자꾸 뭉뚱그려지는데, 하는 일이 다르다.

인증은 "너 누구야?"에 답하는 일이다. 아이디와 비밀번호를 확인하고, 이 요청을 보낸 사람이 정말 그 계정의 주인인지를 가린다. 로그인이 바로 인증이다.

인가는 "너 이거 해도 돼?"에 답하는 일이다. 신원이 확인된 다음, 그 사람이 지금 요청한 이 특정 자원에 손댈 권한이 있는지를 가린다. 남의 주문을 볼 권한이 있는가, 이 글을 지울 권한이 있는가, 관리자 페이지에 들어갈 권한이 있는가. 전부 인가의 영역이다.

호텔로 비유하면 딱 떨어진다. 프런트에서 신분증을 내고 체크인하는 게 인증이다. "예약하신 손님 맞으시네요" 하고 카드키를 내준다. 그런데 그 카드키는 아무 문이나 다 열지 않는다. 내가 묵는 방 문만 열린다. 이게 인가다. 카드키를 1204호 문에 대면 열리지만 옆방 1205호에 대면 빨간불이 뜬다. 체크인한 손님이라는 사실과, 1205호에 들어가도 된다는 사실은 별개다.

앞의 쇼핑몰이 왜 뚫렸는지 이제 보인다. 이 호텔은 체크인만 확인하고, 카드키가 아무 방이나 다 열리게 만들어둔 것이다. 손님인 건 맞으니 로비까지는 정상이다. 그런데 복도의 모든 방문이 체크인한 사람 앞에서 전부 열려버린다.

AI는 왜 인증은 잘 붙이고 인가는 빼먹나

인증을 빠뜨리는 실수는 오히려 드물다. 로그인 화면은 눈에 보이고, 없으면 앱이 굴러가지도 않으니 누구나 챙긴다. AI도 마찬가지라 "로그인 기능 만들어줘"라고 하면 로그인 미들웨어를 꼬박꼬박 붙여준다.

문제는 인가다. 인가는 눈에 안 보인다. 화면 어디에도 "소유권 확인 중"이라는 표시가 없고, 내 계정으로 테스트할 때는 항상 내 데이터만 보이니 아무 이상이 없어 보인다. AI가 코드를 짤 때도 똑같다. 기능이 도는 것까지가 목표라, 요청을 보낸 사람이 로그인만 되어 있으면 데이터를 그냥 내준다. "이 로그인한 사람이 하필 이 주문의 주인이 맞는가"라는 한 겹의 확인은 시키지 않으면 대개 빠진다.

전형적인 코드가 이렇게 생겼다.

// 로그인은 확인하지만, 이 주문이 "내 것"인지는 안 본다
app.get('/api/orders/:id', requireLogin, async (req, res) => {
  const order = await Order.findById(req.params.id);
  res.json(order);
});

requireLogin이 인증을 맡는다. 로그인 안 한 사람은 여기서 막힌다. 그런데 그 아래를 보면, 주소로 들어온 id를 그대로 받아서 해당 주문을 찾고 응답으로 뱉는다. 이 주문이 요청한 사람의 것인지 대조하는 줄이 어디에도 없다. 그래서 로그인만 되어 있으면 id를 바꾸는 것만으로 세상 모든 주문이 열린다.

숫자만 바꾸면 열리는 문: IDOR

방금 본 것처럼, 주소나 요청에 담긴 식별자(id)를 바꿔서 남의 자원에 접근하는 걸 IDOR라고 부른다. Insecure Direct Object Reference, 우리말로 옮기면 "안전장치 없이 객체를 직접 가리키는 참조"쯤 된다. 이름은 거창하지만 공격은 앞에서 본 그대로다. URL의 숫자를 1002에서 1003으로 바꾸는 것. 특별한 도구도, 코드 실행도, 해킹 기술도 필요 없다. 브라우저 주소창이면 충분하다.

API에서 같은 결함을 부를 때는 BOLA라는 말을 쓴다. Broken Object Level Authorization, 객체 단위의 인가가 깨졌다는 뜻이다. IDOR와 BOLA는 사실상 같은 문제를 웹과 API 각각의 언어로 부르는 것이라고 봐도 된다. 핵심은 하나다. 자원 하나하나를 내줄 때마다 "이 사람이 이 자원의 주인인가"를 서버가 확인하지 않는다는 것.

무서운 대목은 이게 로그에도 정상으로 찍힌다는 점이다. 공격자는 남의 계정을 훔친 게 아니라 멀쩡한 자기 계정으로 로그인해서, 허락된 문법으로 요청을 보낸다. 서버 입장에서는 로그인한 정상 사용자가 주문을 조회한 평범한 기록일 뿐이다. 그래서 데이터가 몇 만 건씩 새어나가도 한참 뒤에야 알아채는 경우가 많다.

고치는 법은 허탈할 만큼 짧다. 자원을 내주기 전에 주인이 맞는지 한 줄로 대조하면 된다.

app.get('/api/orders/:id', requireLogin, async (req, res) => {
  const order = await Order.findById(req.params.id);
  // 이 주문의 주인이 지금 로그인한 사람과 같은가
  if (!order || order.userId !== req.user.id) {
    return res.status(403).json({ error: '접근 권한 없음' });
  }
  res.json(order);
});

order.userId !== req.user.id, 딱 이 한 줄이 인가다. 주문에 적힌 주인과 로그인한 사람이 다르면 403으로 막는다. 이 한 줄이 있고 없고가, 내 주소창 장난에 온 회원의 개인정보가 털리는 앱과 그렇지 않은 앱을 가른다.

인증만 있는 앱과 인가까지 있는 앱의 차이
인증만 있는 앱과 인가까지 있는 앱의 차이

프론트에서 버튼 숨기는 건 인가가 아니다

여기서 바이브 코더가 자주 빠지는 함정이 하나 있다. "관리자만 볼 수 있어야 하니까, 관리자가 아니면 그 버튼을 화면에서 숨겼어. 이러면 인가된 거 아냐?"

아니다. 프론트엔드에서 버튼을 숨기거나 메뉴를 안 보이게 하는 건 편의일 뿐 방어가 아니다. 화면에 안 보인다는 건 그냥 안 보이는 것이다. 그 버튼이 실제로 부르는 주소, 이를테면 POST /api/admin/users/1003/delete는 여전히 살아 있고, 누구든 개발자 도구를 열거나 직접 요청을 만들어 그 주소를 두드릴 수 있다. 브라우저 화면은 서버가 강제하는 규칙이 아니라 서버가 보내준 권유일 뿐이다.

그래서 인가는 반드시 서버에서 판정해야 한다. 프론트에서 관리자 버튼을 숨기는 것과 별개로, 서버의 그 삭제 엔드포인트 안에 "이 요청을 보낸 사람이 정말 관리자인가"를 확인하는 관문이 따로 있어야 한다. 화면 단속과 서버 단속은 다른 일이고, 진짜 자물쇠는 서버 쪽에만 달린다.

같은 이유로 자주 나오는 "그럼 주문 번호를 1, 2, 3 같은 순번 말고 추측하기 어려운 UUID로 바꾸면 되지 않냐"는 발상도 근본 해결은 아니다. 번호를 어렵게 만들면 무작정 찍어보기가 힘들어질 뿐, 어딘가에서 그 UUID가 한 번이라도 노출되면(공유 링크, 로그, 다른 API 응답) 다시 그대로 열린다. 추측 난이도를 올리는 것과, 주인인지 확인하는 것은 전혀 다른 층위의 방어다.

OWASP가 이걸 1위에 둔 이유

이 문제가 얼마나 흔하고 심각한지는 보안 업계의 대표 목록만 봐도 드러난다. OWASP는 웹 애플리케이션에서 가장 위험한 취약점 열 가지를 정리해 발표하는데(OWASP Top 10), 2021년판에서 지금 이야기한 접근 통제 실패, 즉 Broken Access Control이 1위에 올랐다. 그전까지 오래 1위였던 인젝션 계열을 제친 것이다.

API로 눈을 돌리면 순위가 더 노골적이다. OWASP가 따로 내는 API 보안 Top 10에서는 방금 본 BOLA, 객체 단위 인가가 깨지는 문제가 목록 맨 앞자리를 차지한다. 요즘 앱이 대부분 프론트와 API로 나뉘어 돌아가고, 그 API가 id 하나로 자원을 주고받는 구조라는 걸 생각하면 당연한 결과다. 이 결함을 코드 약점으로 분류한 표준 번호도 있다. 인가 확인 자체가 빠진 경우는 CWE-862(Missing Authorization), 사용자가 넘긴 값으로 남의 객체를 가리키게 되는 IDOR 계열은 CWE-639로 정리되어 있다.

정리하면 이건 드물게 터지는 희귀 사고가 아니라, 웹과 API 모두에서 가장 자주 그리고 가장 크게 터지는 결함이다. 그리고 AI 생성 코드를 분석한 보안 리포트들이 반복해서 지적하는 항목이기도 하다. 인증 미들웨어는 잘 붙는데 객체 단위 소유권 검증이 빠진다는, 지금까지 본 바로 그 패턴이다.

그래서 무엇을 챙기면 되나

판단 기준은 하나로 요약된다. 자원을 하나 내줄 때마다 "이 사람이 누구인지"에 더해 "이 사람이 이 자원에 손대도 되는지"를 서버가 확인하는가.

로그인 관문 하나로 안심하지 않는 게 출발점이다. 로그인은 건물 로비를 통과시킨 것일 뿐, 복도의 각 방문은 방마다 따로 잠겨 있어야 한다. 주문, 청구서, 첨부파일, 쪽지, 프로필처럼 사용자별로 갈리는 자원은 조회든 수정이든 삭제든, 접근하는 순간마다 주인이 맞는지 대조하는 한 줄을 넣는다. 관리자 기능이라면 로그인 확인과 별개로 "정말 관리자인가"를 검사하는 관문을 서버에 따로 둔다.

AI한테 엔드포인트를 만들어달라고 할 때도 요청을 한 겹 더 얹는 습관이 필요하다. "이 주문 조회 API 만들어줘"에서 멈추지 말고, "로그인한 사용자가 자기 주문만 볼 수 있게, 소유권을 확인해서 남의 주문은 403으로 막아줘"까지 시키는 것이다. 시키면 대개 넣어준다. 문제는 안 시키면 대개 빠진다는 데 있다. 인가는 눈에 안 보이는 방어라, 눈에 보이게 요구해야 코드에 들어온다.

정리

로그인을 붙였다는 건 "네가 누구인지"를 확인했다는 뜻이지, "네가 이걸 봐도 되는지"까지 확인했다는 뜻이 아니다. 인증과 인가는 이름만 닮았을 뿐 서로 다른 질문이고, AI가 짜주는 코드는 앞의 질문에만 답하고 뒤의 질문을 자주 건너뛴다. 그 틈으로 새어나가는 게 IDOR이고 BOLA다. 주소창의 숫자 하나만 바꾸면 온 회원의 개인정보가 흘러나오는, 그러나 로그에는 정상 사용자로만 찍히는 조용한 사고다.

막는 방법은 화려하지 않다. 자원을 내주기 전에 주인이 맞는지 서버에서 한 줄로 대조하는 것, 화면에서 버튼을 숨기는 것과 서버에서 권한을 막는 것을 구분하는 것, 그리고 AI에게 엔드포인트를 시킬 때 소유권 검증까지 함께 주문하는 것. 로그인을 붙였다고 문이 다 잠긴 게 아니다. 방문은 방마다 따로 잠가야 한다.

YS

신윤섭

데이너스 대표 | AI 교육 & AX 컨설팅

90개 이상의 AI/AX 교육 과정을 설계하고, 50여 교육·협업 기관에서 강의했습니다. 강남세브란스, 삼성전자, 현대자동차 등 다양한 조직의 AI 역량 강화를 지원하고 있습니다.

AI 교육이 필요하신가요?

조직에 맞는 맞춤형 AI/AX 교육 프로그램을 설계해드립니다. 커리큘럼 상담부터 시작해보세요.

같은 주제의 다른 글