<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Fraud Prevention on jyukki's Blog</title><link>https://jyukki.com/tags/fraud-prevention/</link><description>Recent content in Fraud Prevention on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-KR</language><lastBuildDate>Sun, 30 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/fraud-prevention/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Abuse Prevention, 위험 점수와 점진적 마찰로 정상 사용자를 지키는 법</title><link>https://jyukki.com/learning/deep-dive/deep-dive-abuse-prevention-progressive-friction-playbook/</link><pubDate>Sun, 30 Aug 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-abuse-prevention-progressive-friction-playbook/</guid><description>로그인 대입, 가입 보너스 악용, 쿠폰 수집, 예약 선점 같은 어뷰징을 IP 차단 하나로 끝내지 않고, 행동 신호·위험 점수·점진적 마찰·재검증으로 운영하는 백엔드 설계 기준을 정리합니다.</description><content:encoded><![CDATA[<p>가입 보너스를 여러 번 받는 계정, 비밀번호를 수천 번 대입하는 봇, 인기 좌석을 잡아두고 되파는 자동화, 쿠폰을 대량 발급하는 스크립트는 겉으로 보면 모두 &ldquo;요청이 너무 많다&quot;는 문제처럼 보입니다. 그래서 처음에는 IP rate limit 하나를 두기 쉽습니다. 그러나 회사·학교·이동통신망처럼 여러 정상 사용자가 IP를 공유하는 환경에서는 이 방식이 곧 정상 사용자를 막습니다. 반대로 프록시와 계정 풀을 쓰는 공격자는 IP만 바꿔 제한을 비켜 갑니다.</p>
<p>어뷰징 방어는 차단 기능 하나가 아니라 <strong>제한된 신호로 위험을 추정하고, 피해가 큰 행동에만 마찰을 늘리는 의사결정 시스템</strong>입니다. 좋은 시스템은 공격자를 완벽하게 식별한다고 약속하지 않습니다. 대신 공격이 성공하기까지 필요한 계정·시간·인증·자금을 늘리고, 정상 사용자가 멈췄을 때 되돌릴 경로를 남깁니다. 이 글은 <a href="/learning/deep-dive/deep-dive-api-rate-limit-backpressure/">API 레이트 리밋과 백프레셔</a>, <a href="/learning/deep-dive/deep-dive-device-session-registry-revocation-playbook/">Device Session Registry와 강제 로그아웃</a>, <a href="/learning/deep-dive/deep-dive-step-up-authorization-high-risk-actions-playbook/">고위험 액션 Step-up Authorization</a>, <a href="/learning/deep-dive/deep-dive-structured-logging/">구조화 로그 설계</a>와 이어집니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>IP 하나가 아니라 account, session, device, 행동 속도, 대상 리소스를 조합해 보는 이유를 이해합니다.</li>
<li><code>allow → slowdown → challenge → step-up → review → deny</code>를 action 위험도에 따라 선택할 수 있습니다.</li>
<li>위험 점수의 임계값, shadow mode, false positive, fallback을 숫자와 운영 조건으로 설계합니다.</li>
<li>개인정보를 과하게 모으지 않으면서도 대응·재현·이의제기에 필요한 증거를 남기는 방법을 얻습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-방어-대상은-요청이-아니라-비정상적으로-싼-성공이다">1) 방어 대상은 요청이 아니라 &ldquo;비정상적으로 싼 성공&quot;이다</h3>
<p>동일한 초당 20건이라도 의미가 다릅니다. 검색 자동완성 20건은 사용자 타이핑일 수 있지만, 신규 계정 20개 생성이나 같은 쿠폰의 20회 발급은 경제적 손실을 만들 수 있습니다. 따라서 rate limit의 키를 endpoint와 IP로만 고정하지 말고, <strong>공격자가 얻는 가치와 성공 단위</strong>를 먼저 정의해야 합니다.</p>
<table>
  <thead>
      <tr>
          <th>action</th>
          <th>공격자의 이득</th>
          <th>먼저 볼 키</th>
          <th>기본 안전장치</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>로그인</td>
          <td>계정 탈취</td>
          <td>account + IP prefix + session</td>
          <td>실패 횟수 cooldown, credential stuffing 탐지</td>
      </tr>
      <tr>
          <td>가입·추천</td>
          <td>보너스·가짜 사용자</td>
          <td>device/session + payment/phone 검증 상태</td>
          <td>발급 지연, 보상 보류</td>
      </tr>
      <tr>
          <td>쿠폰 발급</td>
          <td>할인 비용</td>
          <td>account + campaign + device</td>
          <td>1인/1자격 제한, 발급 ledger</td>
      </tr>
      <tr>
          <td>예약·재고 선점</td>
          <td>희소 자원 독점</td>
          <td>account + target resource + 시간창</td>
          <td>짧은 hold, 결제 전 만료</td>
      </tr>
      <tr>
          <td>비밀번호 재설정</td>
          <td>takeover 준비</td>
          <td>account + channel + recent session</td>
          <td>메일 확인, 최근 위험 신호와 결합</td>
      </tr>
  </tbody>
</table>
<p>여기서 &ldquo;기기&quot;는 광고 식별자처럼 장기 추적을 위한 값일 필요가 없습니다. 로그인 cookie, 앱 설치 ID, 세션에서 파생한 회전 가능한 pseudonymous key처럼 서비스 목적에 필요한 최소 식별자로 시작하세요. 원문 IP·전화번호·user agent 전체를 무기한 로그에 보관하면 방어 데이터가 또 다른 민감 자산이 됩니다. 원문 접근은 짧게 제한하고, 집계 판단에는 hash, prefix, bucket을 우선 사용합니다.</p>
<h3 id="2-위험-점수는-블랙박스보다-판정-가능한-규칙-묶음으로-시작한다">2) 위험 점수는 블랙박스보다 판정 가능한 규칙 묶음으로 시작한다</h3>
<p>처음부터 복잡한 ML 모델을 만들 필요는 없습니다. 운영자가 &ldquo;왜 이 사용자가 challenge를 받았는가&quot;를 설명할 수 있는 규칙 합산이 더 낫습니다. 예를 들어 신규 세션에서 실패한 로그인 8회, 이전에 보지 못한 국가, 30초 안에 서로 다른 5개 계정 시도가 동시에 관측되면 각각의 점수를 더합니다. 단, IP가 새롭다는 이유 하나만으로 금전 action을 거부하는 식의 규칙은 오탐이 큽니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>risk = velocity(0..30)
</span></span><span style="display:flex;"><span>     + account_history(0..25)
</span></span><span style="display:flex;"><span>     + session_integrity(0..20)
</span></span><span style="display:flex;"><span>     + target_value(0..15)
</span></span><span style="display:flex;"><span>     + network_anomaly(0..10)
</span></span></code></pre></div><p>점수 항목은 공격을 &ldquo;증명&quot;하지 않고 행동의 위험을 우선순위화합니다. 특히 <code>target_value</code>를 넣어야 합니다. 같은 로그인 이상이라도 프로필 조회와 출금 계좌 변경은 피해가 다르므로, 사용자 전역 점수 하나로 모든 action을 판단하면 안 됩니다. 평가 결과에는 rule version과 개별 항목의 범주만 남기고, 사람이 다시 볼 수 있는 reason code를 만드세요. 예: <code>LOGIN_VELOCITY_HIGH</code>, <code>NEW_SESSION_HIGH_VALUE_ACTION</code>.</p>
<h3 id="3-단일-차단선-대신-점진적-마찰을-둔다">3) 단일 차단선 대신 점진적 마찰을 둔다</h3>
<p>위험을 0 또는 100으로 단정하면 방어는 단순하지만 제품은 거칠어집니다. 출발점으로는 아래 같은 계단이 실용적입니다. 수치는 서비스의 정상 분포를 본 뒤 바꿔야 하며, 그대로 복사할 정답은 아닙니다.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: right">위험 점수</th>
          <th>기본 반응</th>
          <th>적용 조건</th>
          <th>성공 후 처리</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right">0~29</td>
          <td>allow</td>
          <td>정상 속도·기존 신뢰 세션</td>
          <td>평상시 audit event</td>
      </tr>
      <tr>
          <td style="text-align: right">30~49</td>
          <td>slowdown</td>
          <td>짧은 시간창의 반복·새 세션</td>
          <td>5~30초 delay, 낮은 우선순위</td>
      </tr>
      <tr>
          <td style="text-align: right">50~69</td>
          <td>challenge</td>
          <td>자동화 가능성은 높지만 피해 확정 전</td>
          <td>bot challenge 또는 추가 확인</td>
      </tr>
      <tr>
          <td style="text-align: right">70~84</td>
          <td>step-up/review</td>
          <td>고가 쿠폰·권한 변경·새 기기</td>
          <td>passkey·메일 재확인, 보상 보류</td>
      </tr>
      <tr>
          <td style="text-align: right">85 이상</td>
          <td>deny + investigate</td>
          <td>금지 계정, 토큰 재사용, 확정된 공격 패턴</td>
          <td>세션 revoke, support·security queue</td>
      </tr>
  </tbody>
</table>
<p><code>challenge</code>는 CAPTCHA 하나를 뜻하지 않습니다. 사람이 풀기 어려운 puzzle만 반복하면 접근성, 저사양 기기, 지역 네트워크에서 정상 사용자도 손해를 봅니다. 이미 검증된 email을 재확인하거나, passkey를 요구하거나, 지급을 24시간 보류하거나, 제한된 manual review queue로 보내는 것도 마찰입니다. 피해와 되돌리기 비용이 낮은 구간에서는 요청을 조금 늦추는 것이 차단보다 나을 수 있습니다.</p>
<h3 id="4-수량-제한과-정합성-제한은-다른-문제다">4) 수량 제한과 정합성 제한은 다른 문제다</h3>
<p>어뷰징 방어를 rate limit에만 맡기면 경합 조건을 놓칩니다. &ldquo;쿠폰은 계정당 한 번&quot;이라는 규칙은 요청 10개 중 9개를 429로 만드는 것만으로 보장되지 않습니다. 서로 다른 서버가 거의 동시에 허용 결정을 내릴 수 있기 때문입니다. 한 번만 지급되어야 하는 금전·재고·보상은 DB unique constraint, 원자적 update, idempotency key, 발급 ledger처럼 <strong>정합성 경계</strong>에서 다시 막아야 합니다.</p>
<p>예를 들어 보상 지급은 <code>(account_id, campaign_id)</code> unique key를 갖는 ledger insert로 시작할 수 있습니다. risk score가 낮아도 insert가 충돌하면 이미 지급 또는 진행 중으로 처리합니다. risk score는 &ldquo;누구에게 더 많은 검증을 요구할지&quot;를 고르고, 데이터 제약은 &ldquo;한 번만 일어나야 하는 상태 변경&quot;을 보장합니다. 이 둘을 섞으면 장애 때 정확히 무엇이 실패했는지 알기 어렵습니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-action별-위험-계약부터-작성한다">1) action별 위험 계약부터 작성한다</h3>
<p>새 방어 rule을 늘리기 전에 아래와 같은 action contract를 상위 5개 위험 경로에 만드세요. 문서는 한 페이지여도 충분하지만 owner와 복구 경로가 빠지면 실제 incident에서 쓸 수 없습니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">action</span>: issue_promotion_coupon
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">asset</span>: <span style="color:#f1fa8c">&#34;campaign budget and fairness&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">normal_volume</span>: <span style="color:#f1fa8c">&#34;account당 1일 1회&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">signal_windows</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">account</span>: <span style="color:#f1fa8c">&#34;10분 3회&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">session</span>: <span style="color:#f1fa8c">&#34;1분 5회&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">campaign</span>: <span style="color:#f1fa8c">&#34;1분 신규 계정 발급률&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">responses</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">moderate</span>: <span style="color:#f1fa8c">&#34;10초 cooldown + challenge&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">high</span>: <span style="color:#f1fa8c">&#34;issuance hold 24h + review&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">confirmed</span>: <span style="color:#f1fa8c">&#34;deny + session revoke&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">integrity_guard</span>: <span style="color:#f1fa8c">&#34;unique(account_id, campaign_id)&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">owner</span>: <span style="color:#f1fa8c">&#34;growth-platform&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">appeal_path</span>: <span style="color:#f1fa8c">&#34;support ticket + reason code&#34;</span>
</span></span></code></pre></div><p>우선순위는 <strong>불가역적인 금전·권한 손실 방지 → 정상 사용자의 계정 접근 보호 → 캠페인 공정성 → 운영 편의</strong>입니다. 이 순서가 없으면 쿠폰 중복보다 로그인 false positive를 더 심각하게 다루어야 하는 상황을 놓치기 쉽습니다.</p>
<h3 id="2-shadow-mode와-rollout-gate를-분리한다">2) shadow mode와 rollout gate를 분리한다</h3>
<p>새 rule을 바로 deny로 켜지 마세요. 최소 7일 동안 실제로는 허용하되, &ldquo;이 rule이 있었다면 어떤 반응을 했을지&quot;를 기록합니다. 이 기간에는 challenge 가정 비율, 해당 cohort의 가입·구매 전환율, support contact rate, 확정 abuse 재발률을 함께 봅니다.</p>
<p>초기 rollout의 예시는 다음과 같습니다.</p>
<ol>
<li><strong>Shadow 7일</strong>: 정상 cohort와 의심 cohort의 분포·reason code를 확인합니다.</li>
<li><strong>5% canary 24시간</strong>: 피해가 낮은 action에서 slowdown만 적용합니다.</li>
<li><strong>25% 확대</strong>: false positive가 기존 대비 +0.2%p 미만이고 support 문의가 기준선 안일 때 challenge를 추가합니다.</li>
<li><strong>100% enforce</strong>: 우회율, conversion, incident volume을 1주 더 보고 85점 이상 deny만 별도 review 합니다.</li>
</ol>
<p>공격이 진행 중이면 이 순서를 생략할 수 있습니다. 대신 emergency rule에는 만료 시각을 두고, 24시간 안에 owner review를 강제하세요. incident 때 만든 광범위 IP 차단이 영구 정책으로 남는 일이 가장 흔한 운영 부채입니다.</p>
<h3 id="3-관측성은-몇-건-막았나보다-오판과-우회를-보여야-한다">3) 관측성은 &ldquo;몇 건 막았나&quot;보다 오판과 우회를 보여야 한다</h3>
<p>차단 수가 늘었다고 방어가 좋아졌다고 말할 수는 없습니다. 임계값을 낮추면 차단 수는 항상 늘기 때문입니다. 아래 지표를 action·rule version·신뢰 cohort별로 나누면 판단이 쉬워집니다.</p>
<table>
  <thead>
      <tr>
          <th>지표</th>
          <th>질문</th>
          <th>위험 신호</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>challenge rate</td>
          <td>얼마나 자주 마찰을 주는가</td>
          <td>정상 기존 사용자에서 급증</td>
      </tr>
      <tr>
          <td>challenge pass rate</td>
          <td>사람에게 너무 어려운가</td>
          <td>정상 cohort에서 급락</td>
      </tr>
      <tr>
          <td>false-positive appeal rate</td>
          <td>잘못 막았는가</td>
          <td>7일 이동 평균의 2배</td>
      </tr>
      <tr>
          <td>post-challenge abuse rate</td>
          <td>마찰 뒤에도 성공하는가</td>
          <td>일정 수준 이상 유지</td>
      </tr>
      <tr>
          <td>time-to-containment</td>
          <td>탐지부터 피해 제한까지 걸린 시간</td>
          <td>runbook·owner 부재로 지연</td>
      </tr>
      <tr>
          <td>reason-code coverage</td>
          <td>판단을 설명할 수 있는가</td>
          <td><code>unknown</code> 비율 증가</td>
      </tr>
  </tbody>
</table>
<p>구조화 event에는 raw password, 전체 cookie, 민감 challenge 답을 넣지 않습니다. <code>action</code>, <code>outcome</code>, <code>risk_bucket</code>, <code>reason_codes</code>, <code>rule_version</code>, <code>privacy_safe_subject_key</code>, <code>trace_id</code> 정도로 시작해도 incident 분석에 충분합니다. 조사 권한이 있는 별도 보안 저장소와 애플리케이션 운영 로그의 보존 기간도 분리하세요.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>어뷰징 방어는 필연적으로 공정성과 마찰 사이의 선택입니다. 새 기기, 해외 출장, 학교·회사 NAT, 보조기기 사용은 모두 공격 신호처럼 보일 수 있습니다. 그래서 신호가 많을수록 더 안전하다고 생각하면 위험합니다. 신호의 품질, 동의·고지, 보존 기간, 이의제기 가능성이 함께 있어야 합니다. 국가·통신사·브라우저 특성을 점수에 넣는다면 특정 집단의 실패율이 높아지지 않는지 cohort별로 검토해야 합니다.</p>
<p>외부 CAPTCHA나 device intelligence 서비스도 의존성입니다. provider timeout을 login 전체 timeout까지 기다리거나, 응답이 없을 때 모든 사용자를 deny하면 방어 서비스 장애가 곧 제품 장애가 됩니다. high-risk action에는 fail-closed가 맞을 수 있지만, 일반 로그인·가입에서는 짧은 timeout 뒤 local cooldown 또는 email verification으로 degraded path를 준비하세요. <a href="/learning/deep-dive/deep-dive-outbound-api-adapter-dependency-isolation-playbook/">외부 API 의존성 격리</a>처럼 provider 실패의 사용자 영향을 따로 설계하는 이유입니다.</p>
<p>마지막으로 공격자는 규칙에 반응합니다. 성공률이 낮아지면 계정 수를 늘리거나, 더 느리게 요청하거나, 사람이 개입한 트래픽을 섞습니다. 따라서 비밀 rule만 늘리기보다, 희소 자원은 서버 정합성으로 잠그고, 사용자에게는 복구 가능한 마찰을 주고, 운영자에게는 짧은 피드백 루프를 주는 구조가 오래 갑니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> 금전·권한·희소 자원이 걸린 상위 5개 action에 abuse contract와 owner가 있다.</li>
<li><input disabled="" type="checkbox"> IP 제한 외에 account, session/device, target resource, 시간창 중 필요한 키를 조합한다.</li>
<li><input disabled="" type="checkbox"> risk score와 DB unique constraint·idempotency·ledger의 역할을 분리했다.</li>
<li><input disabled="" type="checkbox"> <code>allow</code>, <code>slowdown</code>, <code>challenge</code>, <code>step-up</code>, <code>review</code>, <code>deny</code>의 복구 경로가 각각 있다.</li>
<li><input disabled="" type="checkbox"> 새 rule을 shadow mode에서 7일 이상 비교하고 false positive 기준을 확인했다.</li>
<li><input disabled="" type="checkbox"> external challenge provider timeout과 degraded path가 제품 SLO 안에 있다.</li>
<li><input disabled="" type="checkbox"> reason code, rule version, trace ID는 남기되 raw 식별자와 민감 답변은 최소화했다.</li>
<li><input disabled="" type="checkbox"> emergency rule에 만료일과 24시간 이내 review owner를 두었다.</li>
</ul>
<h3 id="연습-신규-계정-쿠폰-발급-방어-설계하기">연습: 신규 계정 쿠폰 발급 방어 설계하기</h3>
<p>신규 가입자에게 1만 원 쿠폰을 주는데, 같은 사람이 계정을 반복 생성해 사용하는 상황을 가정해 봅시다. 먼저 한 IP당 N회 같은 단일 제한을 쓰지 말고 account, 가입 후 경과 시간, session/device의 최근 발급 수, 같은 캠페인의 지급 ledger를 분리해 적습니다. 다음으로 30<del>49점에는 발급을 10분 지연하고, 50</del>69점에는 email 재확인 후 지급, 70점 이상에는 24시간 hold와 review로 가는 표를 만드세요. 마지막으로 <code>(account_id, campaign_id)</code> unique key로 중복 지급을 막고, shadow 7일 동안 정상 신규 가입자의 쿠폰 수령률이 기준선에서 얼마나 변하는지 측정합니다. 이 세 가지가 분리되어 있어야 방어 rule을 조정해도 보상 정합성이 무너지지 않습니다.</p>
<h2 id="관련-글">관련 글</h2>
<ul>
<li><a href="/learning/deep-dive/deep-dive-api-rate-limit-backpressure/">API 레이트 리밋과 백프레셔</a></li>
<li><a href="/learning/deep-dive/deep-dive-device-session-registry-revocation-playbook/">Device Session Registry와 강제 로그아웃</a></li>
<li><a href="/learning/deep-dive/deep-dive-step-up-authorization-high-risk-actions-playbook/">고위험 액션 Step-up Authorization</a></li>
<li><a href="/learning/deep-dive/deep-dive-structured-logging/">구조화 로그 설계</a></li>
</ul>
]]></content:encoded></item></channel></rss>