
2024년 5월 일본 DMM Bitcoin에서 약 3.08억 달러가 빠져나갔습니다. 지갑 업체 직원이 받은 가짜 채용 시험이 악성 Python 코드였고, 공격자는 그 직원의 세션 쿠키로 거래를 변조했습니다(코인 해킹 12년 연대기, 국내편 10편). 캐피타(해외편 09편)에서는 백업 관리자 계정이 도메인 관리자 권한을 갖고 있었고, 고권한 계정의 사용을 통제·기록하는 체계가 없었습니다(ICO). 운영 권한이 늘 열려 있으면 직원 노트북 한 대, 세션 하나가 그 권한 전체로 이어집니다.
이 시리즈의 공격을 실제 쇼핑몰 코드에서 직접 재연하고 막아 보는 인프런 강의가 열렸습니다. 실제 해킹 사고로 배우는 Spring Boot·Express 시큐어 코딩 · 이 글과 이어지는 수업: 13-5, 13-6

| 위험 | 어디서 생기나 | 막는 법 한 줄 | OWASP |
|---|---|---|---|
| 누가 했는지 모름 | 여러 사람이 함께 쓰는 운영 계정 | 회사 SSO 개인 계정으로만 운영 화면에 들어온다 | A07 |
| 늘 열린 운영 권한 | 관리자 역할을 계정에 상시 부여 | 사유를 받고 정해진 시간만 권한을 준다 | A01 |
| 세션 하나로 권한 상승 | 로그인만 돼 있으면 되는 권한 요청 | 권한을 올릴 때 다시 인증한다 | A07 |
| 접근 흔적이 없음 | 운영 API에 기록 없음 | 모든 운영 접근을 감사 로그로, 상시 권한 0을 정기 점검 | A01·A09 |
운영 도구에 admin 계정 하나를 두고 여럿이 비밀번호를 나눠 쓰면, 접근 기록에는 admin만 남습니다. 사고가 나도 누가 한 일인지 가릴 수 없습니다. 한 사람이 팀을 떠나면 모두의 비밀번호를 바꿔야 하니 회수가 미뤄집니다. 국내편 10편이 정리한 로닌 브리지 사고에서도 회수되지 않은 권한이 서명 확보에 쓰였습니다.
계정 하나가 닿는 범위도 문제입니다. SGI서울보증에서는 보도에 따르면 관리자 계정 1개가 서버 32대를 관리했습니다(국내편 06편).
특정 회사 코드가 아니라 개념을 보여 주는 예시입니다. 값은 가짜입니다. 위험한 모양은 설정 파일에 공용 계정을 두는 것입니다.
# application.yml (위험한 모양)
spring.security.user:
name: admin
password: FAKE-shared-password
고친 모양은 운영 경로를 회사 SSO(한 번 로그인으로 여러 사내 시스템에 들어가는 통합 인증)의 개인 계정으로만 엽니다.
@Bean
SecurityFilterChain opsChain(HttpSecurity http) throws Exception {
return http.securityMatcher("/ops/**")
.oauth2Login(Customizer.withDefaults()) // 사람마다 다른 계정
.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.build();
}
설정 파일과 비밀 저장소에서 admin, ops, root 같은 공용 이름의 계정을 찾습니다. 운영 접근 기록의 행위자 칸이 모두 개인 계정인지 봅니다.
개인 계정이어도 관리자 역할을 늘 붙여 두면, 그 사람의 노트북이나 세션이 넘어가는 순간 운영 권한도 같이 넘어갑니다. 운영 작업은 하루 중 잠깐인데 권한은 24시간 열려 있습니다. 시간 제한 권한 상승(평소에는 권한이 없고, 필요할 때 사유를 남기고 정해진 시간만 받는 방식)은 열려 있는 시간을 줄입니다.
권한 부여 기록에 만료 시각을 두고, 요청마다 만료되지 않은 기록이 있는지 확인합니다.
public record OpsGrant(String user, String reason, Instant expiresAt) {}
@Component("opsAccess")
public class OpsAccess {
public boolean active(Authentication auth) {
return grants.findActive(auth.getName(), Instant.now()).isPresent();
}
}
@PreAuthorize("@opsAccess.active(authentication)")
@GetMapping("/ops/orders/{id}")
public OrderDetail opsOrder(@PathVariable long id) { ... }
권한은 계정에 저장된 역할이 아니라 만료 시각이 있는 기록에서 나옵니다. 시간이 지나면 따로 회수하지 않아도 다음 요청부터 403입니다.
권한을 받은 뒤 테스트 시계를 만료 시각 너머로 옮기고, 같은 운영 API가 403을 돌려주는지 봅니다.
DMM Bitcoin에서 공격자는 직원의 세션 쿠키로 거래를 변조했습니다. 로그인돼 있기만 하면 권한 상승을 요청할 수 있다면, 세션을 가진 쪽도 권한을 올릴 수 있습니다. 권한 상승처럼 위험한 동작은 방금 인증했는지를 따로 확인해야 합니다.
OIDC 토큰의 인증 시각(auth_time)을 보고, 5분이 지났으면 다시 인증하게 합니다.
@PostMapping("/ops/elevate")
public OpsGrant elevate(@RequestParam String reason, @AuthenticationPrincipal OidcUser user) {
Instant authAt = user.getAuthenticatedAt();
if (authAt == null || authAt.isBefore(Instant.now().minus(Duration.ofMinutes(5)))) {
throw new StepUpRequiredException("reauth required"); // 다시 로그인·MFA로 보낸다
}
OpsGrant g = new OpsGrant(user.getName(), reason, Instant.now().plus(Duration.ofHours(1)));
return grants.save(g);
}
StepUpRequiredException은 설명용 이름입니다. 강의에서 만드는 방어 부품 예시에서는 이 검사를 @RequireStepUp 한 줄로 붙입니다. 5분과 1시간은 예시 값입니다.
인증 시각이 10분 전인 테스트 토큰으로 권한 상승을 요청해 거부되는지, 방금 인증한 토큰으로는 만료 시각이 있는 권한이 생기는지 봅니다.
권한을 시간으로 묶어도, 그 시간에 무엇을 했는지 남지 않으면 사고 뒤에 범위를 정할 수 없습니다. 상시 권한이 다시 생겨도 아무도 모릅니다. 급한 장애 대응 때 붙인 관리자 역할이 그대로 남는 경우가 대표적입니다.
운영 API에는 감사 로그를 붙이고(16편의 @Audited), 상시 권한이 남아 있는지 정기적으로 확인합니다.
@Audited(action = "ops.access") // 강의에서 만드는 방어 부품 예시
@PreAuthorize("@opsAccess.active(authentication)")
@PostMapping("/ops/orders/{id}/refund")
public RefundResult opsRefund(@PathVariable long id) { ... }
@Scheduled(cron = "0 0 9 * * MON")
void reviewStandingAccess() {
List<String> standing = users.findWithPermanentRole("OPS");
if (!standing.isEmpty()) alerts.raise("P2", "standing-ops-access", standing);
}
부여 기록에는 사유가 남고, 접근 기록에는 행위자가 남습니다. 둘을 맞춰 보면 "누가, 왜, 무엇을"이 한 줄로 이어집니다. 팀을 떠난 사람의 계정은 SSO에서 끄는 것만으로 운영 접근도 함께 막힙니다.
테스트 계정에 운영 역할을 상시로 붙인 뒤 점검 작업을 실행해 경보가 생기는지 봅니다. 목표는 상시 운영 권한 0입니다.
공용 계정은 누가 했는지를 지우고, 상시 권한은 노트북 한 대와 세션 하나를 운영 권한 전체로 만듭니다. 운영 권한은 평소에 비워 두고, 개인 계정으로 사유를 남기고 다시 인증한 뒤 정해진 시간만 받습니다. 그 시간에 한 일은 모두 감사 로그에 남기고, 상시 권한이 다시 생기지 않았는지 정기적으로 확인합니다.
오늘 본 사고의 원인은 대부분 서버와 코드의 기본기에서 막을 수 있는 것들이었습니다. 직접 손으로 막아 보고 싶다면 이런 강의가 있습니다.
실제 해킹 사고로 배우는 Spring Boot·Express 시큐어 코딩
서버 보안 기초부터 차근차근 시작하고 싶다면 이 강의도 있습니다.
백엔드 개발자를 위한 서버 보안 실무: 해킹 방어부터 정보유출 방지까지
수치와 사실은 2026년 11월 2일 기준 공개 자료입니다. 이 글은 법률 자문이 아닙니다.
이전 글 오류가 나면 문이 열린다: 실패하면 막는 예외 처리
다음 글 백업이 같이 잠기지 않게: 불변 백업과 복구 훈련
시리즈 전체 목록 해킹 방식 VS 방어 방법 전체 목록