48. 장수명 연결의 인증·권한 만료 재검증 부재(만료 후 권한 유지) · C#
난이도 중해설 — 장수명 연결의 인증·권한 만료 재검증 부재(만료 후 권한 유지) · C#
난이도: 중
요약
접속 시 인증에서 Authenticated=true 와 TokenExpiresAtUtc 를 저장하지만(A), 이후 보호된 요청 처리(B)는 오직 Authenticated 불리언만 검사하고 만료 시각과 현재 시각을 대조하지 않는다. TCP 연결이 만료 시각을 지나 계속 살아 있으면, 만료된 토큰의 권한이 세션 수명 내내 유효하게 남는다.
문제점
만료 시각을 사용 시점에 재검증하지 않음(check-at-connect only) — 시간제 권한 우회
- 증상: 프리미엄/구독/시간제 접근이 만료된 뒤에도 같은 연결에서 계속 프리미엄 구매가 통과한다. 밴이 시작되거나 권한이 강등돼도 이미 열린 연결은 옛 권한을 유지한다.
- 재현 조건:
- 클라이언트가
ExpiresAtUtc = T+10분인 토큰으로 접속. (A)에서Authenticated=true. - 연결을 끊지 않고 유지(장수명 TCP).
- T+30분에
OnPremiumPurchase호출. (B)는Authenticated==true만 보고 통과 → 만료 20분 지난 권한으로 구매 성공.
- 클라이언트가
- 근본 원인: 인증은 "이벤트(접속 순간)"가 아니라 "시간에 따라 변하는 상태(현재 유효한가)"인데, 코드는 접속 순간의 스냅샷 불리언만 본다.
TokenExpiresAtUtc를 저장해 놓고도 어디서도DateTime.UtcNow와 비교하지 않는다.
수정안
보호된 동작마다 만료를 재검증한다.
private bool IsCurrentlyAuthorized()
=> _session.Authenticated && DateTime.UtcNow < _session.TokenExpiresAtUtc;
public void OnPremiumPurchase(int itemId)
{
if (!IsCurrentlyAuthorized())
{
_session.Authenticated = false; // 만료 확정
Reject("token expired, re-authenticate");
return;
}
PremiumStore.Purchase(_session.AccountId, itemId);
}
더 견고하게는 만료를 능동적으로 처리한다. 세션에 만료 타이머를 걸어 만료 시 권한을 내리고 재인증을 요구하며, 즉시 무효화가 필요한 밴/강등은 별도 통지(펍섭)로 세션 상태를 갱신한다. 토큰이 짧게 만료되고 슬라이딩 갱신이 필요하면, 클라이언트가 주기적으로 갱신 토큰을 제시하고 서버가 새 만료 시각으로 교체한다.
더 나은 설계
- 권한은 매 사용 시점에 평가: "로그인했는가"가 아니라 "지금 이 동작을 할 권한이 있는가"를 검사한다. 만료·밴·강등처럼 시간에 따라 변하는 권한은 특히 그렇다.
- 짧은 만료 + 갱신: 장수명 연결일수록 토큰 만료를 짧게 잡고 갱신을 요구하면 탈취·강등 지연 노출 창이 준다.
- 즉시 무효화 경로: 밴/결제 취소는 만료를 기다리지 않고 반영돼야 하므로, 세션 저장소를 구독해 실시간으로 권한을 회수하는 경로를 둔다.
- 트레이드오프: 매 요청 시각 비교는 비용이 무시할 만하다. 실시간 무효화(펍섭)는 인프라 비용이 있으니 민감한 권한(결제·GM)에만 적용하는 절충이 흔하다.
면접 포인트
- "접속 시 한 번 인증하고 이후 불리언만 본다"는 장수명 연결에서 시간제 권한을 새게 만드는 전형적 결함이다. 권한 결정은 상태가 아니라 "현재 시각 기준 재평가"여야 한다.
- 인증(누구인가)과 인가(지금 무엇을 할 수 있는가)를 분리해서 생각하면, 인가는 매 요청 평가 대상임이 분명해진다.
해설 — 장수명 연결의 권한 만료 재검증 부재(GM 권한 만료 후 유지) · C++
난이도: 중
요약
접속 시 인증에서 만료 시각 authExpireEpoch 를 저장하지만(A), GM 명령 처리(B)는 authExpireEpoch != 0(즉 "인증된 적이 있는가")만 검사하고 nowEpoch() < authExpireEpoch(즉 "지금도 유효한가")를 비교하지 않는다. 장수명 연결이 만료 시각을 지나도 GM 권한이 그대로 남는다.
문제점
만료 시각을 "설정 여부"로만 검사(값이 있으면 통과) — 시간제 권한 우회
- 증상: GM/관리자 권한이 만료된 뒤에도 같은 연결에서 GM 명령이 계속 실행된다.
- 재현 조건:
expiresAtEpoch = now + 600(10분) 토큰으로 접속. (A)에서authExpireEpoch = now+600(비-0).- 연결 유지.
- 30분 뒤
onGmCommand도착. (B)의authExpireEpoch != 0은 여전히 참 → 만료된 권한으로 GM 명령 실행.
- 근본 원인:
authExpireEpoch를 저장해 놓고도 그것을 "만료 시각"이 아니라 "인증 플래그(0/비-0)"로만 사용한다. 저장한 만료 정보를 현재 시각과 대조하는 코드가 없다.
수정안
명령마다 현재 시각과 만료를 비교한다.
bool isCurrentlyAuthorized() const {
return session_.authExpireEpoch != 0 && nowEpoch() < session_.authExpireEpoch;
}
void onGmCommand(const std::string& cmd) {
if (!isCurrentlyAuthorized()) {
session_.authExpireEpoch = 0; // 만료 확정
reject("token expired, re-authenticate");
return;
}
executeGm(session_.accountId, cmd);
}
능동 만료가 필요하면(예: 만료 즉시 연결의 권한을 내리고 재인증 유도) 세션에 만료 타이머(타임휠/우선순위 큐)를 등록하고, 밴·권한 회수 같은 즉시 무효화는 세션 레지스트리 통지로 처리한다. 슬라이딩 갱신은 클라이언트의 주기적 갱신 토큰으로 authExpireEpoch 를 교체한다.
더 나은 설계
- 권한은 매 사용 시점에 평가: 저장된 만료 시각은 "설정됐는지"가 아니라 "현재 시각 기준 유효한지"로 써야 한다. 값의 존재와 값의 유효는 다르다.
- 단조 시계 주의: 만료 판정에
time()(벽시계)을 쓰면 시스템 시계 조정에 취약할 수 있다. 상대적 타임아웃(재인증 주기)에는steady_clock을, 절대 만료(토큰 exp)에는 벽시계를 쓰되 서버 간 시계 동기(NTP)를 전제한다. - 즉시 무효화: 밴/GM 회수는 만료를 기다리지 않고 반영돼야 하므로 세션 저장소 구독 경로를 둔다.
면접 포인트
x != 0로 만료를 검사하는 것은 "만료 시각이 설정됐는가"이지 "만료됐는가"가 아니다. 만료 필드는 반드시 현재 시각과 비교해야 의미가 있다.- 장수명 연결은 "접속 순간의 스냅샷"과 "현재 유효 상태"의 괴리가 커진다. 시간제 권한은 사용 시점 재평가 또는 능동 만료 타이머로 다뤄야 한다.