보안·입력 검증 · Web Security

민감정보 화면 앞에 재인증 세션을 둔 방법

민감정보 화면을 열기 전에 비밀번호를 다시 확인하도록 바꿨습니다. 재인증 세션·만료 필터와 비밀번호 재설정 토큰이 맡는 역할도 분리했습니다.

JavaSpringReauthenticationSessionWeb Security

로그인 세션만으로는 충분하지 않았습니다

로그인한 사용자가 회원정보 화면에 접근할 수 있는 구조에서는 세션이 오래 유지되거나 자리를 비운 사이 다른 사람이 브라우저를 사용할 때 민감정보가 노출될 수 있습니다. 로그인되어 있다는 사실은 현재 화면을 보고 있는 사람이 방금 비밀번호를 확인했다는 뜻이 아닙니다.

싱가포르 COMS에서는 민감한 개인정보 화면에 들어가기 전에 비밀번호를 한 번 더 확인하도록 바꿨습니다. 기존 로그인 세션 안에 최근 비밀번호를 다시 확인했다는 별도 상태를 뒀습니다.

세 가지 인증 수단의 책임을 섞지 않았습니다

같은 비밀번호 기능처럼 보여도 로그인 세션, 재인증 세션과 비밀번호 재설정 토큰은 서로 다른 질문에 답합니다.

세 가지 인증 수단의 책임을 섞지 않았습니다 데이터 표
수단확인하는 것사용할 곳
로그인 세션서비스에 로그인한 사용자인가일반 인증 화면
재인증 세션민감정보 접근 전에 비밀번호를 다시 확인했는가회원정보 같은 보호 화면
재설정 토큰비밀번호 재설정 절차를 통과한 요청인가비밀번호 변경 화면

로그인 세션에 재인증 여부를 영구 플래그로 넣으면 한 번 확인한 뒤 세션이 끝날 때까지 보호 효과가 약해집니다. 그래서 재인증 상태에는 만료가 필요했고, 보호 화면의 요청마다 유효한지 확인하는 필터를 추가했습니다.

비밀번호 재설정은 다른 문제였습니다. 계정을 찾는 과정에서 마스킹된 개인정보를 화면에 계속 노출하는 대신 재설정 토큰과 세션·화면 흐름으로 다음 단계를 연결했습니다. 재설정 토큰을 민감정보 화면의 재인증 증거로 사용하지도 않았습니다.

재인증 상태를 한 세션 값으로 관리했습니다

변경 이력에서 확인되는 구현 단위는 비밀번호 확인 유틸리티, 확인 Controller, 재인증 세션 만료 Filter, 확인 JSP와 Filter 등록 설정입니다. 내부 이름을 일반화하면 요청은 다음 순서로 이동합니다.

보호 화면 요청
    │
    ▼
재인증 세션 만료 Filter
    ├─ 유효함 ─────────────▶ 보호 화면
    └─ 없거나 만료됨 ──────▶ 비밀번호 확인 화면
                                  │
                         비밀번호 일치 확인
                                  │
                                  ▼
                         재인증 세션 갱신

Filter가 비밀번호를 직접 검증하지는 않습니다. Filter는 보호 대상 요청인지와 재인증 세션이 유효한지만 판단하고, 실제 비밀번호 비교는 확인 Controller와 검증 유틸리티가 맡습니다. 화면은 입력과 오류 안내를 담당합니다. 이렇게 나누면 보호 화면마다 같은 비밀번호 확인 코드를 복사하지 않아도 됩니다.

만료 여부를 Filter에서 공통으로 확인했습니다

커밋에서 확인되는 클래스 책임을 공개용 의사 코드로 줄이면 다음과 같습니다. 세션 키와 URL, 만료 시간 같은 내부 값은 제외했습니다.

public void doFilter(Request request, Response response, FilterChain chain) {
    if (!protectedPages.matches(request.path())) {
        chain.doFilter(request, response);
        return;
    }

    Reauthentication reauth = sessionStore.get(request.session());
    if (reauth == null || reauth.isExpired(clock.now())) {
        response.redirect(passwordConfirmationPage);
        return;
    }

    chain.doFilter(request, response);
}

보호 화면 Controller 안에서 각각 만료를 검사할 수도 있습니다. 하지만 새 화면을 추가할 때 검사를 빠뜨리기 쉽고, 직접 URL로 접근하는 경우와 메뉴를 거쳐 접근하는 경우가 달라질 수 있습니다. 요청 경계의 Filter에서 공통으로 확인하고, 보호 대상은 설정에서 명시하도록 했습니다.

이 의사 코드는 확인된 세션 관리·만료 Filter의 책임만 보여 줍니다. 실제 세션 키, 보호 URL과 시간값은 공개하지 않았고, 보존 자료에서 확인하지 못한 암호화 방식이나 토큰 만료 정책도 만들어 넣지 않았습니다.

비밀번호 재설정 토큰은 노출을 줄이는 연결 수단이었습니다

기존 비밀번호 찾기 화면에서는 다음 단계를 안내하는 동안 마스킹된 개인정보가 불필요하게 노출될 여지가 있었습니다. 개선 뒤에는 계정 확인 결과와 재설정 화면 사이를 전용 토큰으로 연결하고, 세션과 화면도 이 순서에 맞게 바꿨습니다.

재인증과 재설정을 분리한 이유는 실패 시의 처리도 다르기 때문입니다. 재인증에 실패하면 기존 로그인 세션을 비밀번호 변경 권한으로 바꿔서는 안 되고, 재설정 토큰이 있다고 해서 회원정보 화면을 열어 줘서도 안 됩니다. 두 흐름은 같은 비밀번호 검증 코드를 일부 사용할 수 있지만 승인하는 행위는 다릅니다.

네 가지 보안 개선을 함께 회귀 테스트했습니다

이 시기에는 재인증 외에도 저장 프라이머 검색 입력의 XSS 검증, 비밀번호 강도 검증, 비밀번호 재설정 과정의 마스킹 정보 노출 방지를 함께 개선했습니다. 네 작업의 테스트 표는 모두 22행이었고 전부 통과한 것을 확인한 뒤 운영에 반영했습니다.

22행을 모두 재인증 기능만의 테스트 수로 표현하지 않습니다. 재인증·세션 만료뿐 아니라 XSS, 비밀번호 강도와 재설정 토큰 흐름을 합친 검증 범위입니다. 각 기능의 정상 동작과 기존 회원정보 흐름의 회귀 여부를 함께 확인했습니다.

제가 맡은 구현 범위

제가 수행한 일은 전달받은 개선 항목을 분석해 네 가지 조치를 구현하고 테스트·운영 배포한 범위입니다. 보안 취약점 진단 자체를 수행하거나 싱가포르 COMS 전체 보안을 총괄한 것은 아닙니다.

이 글도 취약점 진단 방법을 다루지 않습니다. 로그인 세션과 최근 재인증을 구분하고, 민감정보 화면 앞에서 그 상태를 일관되게 검사하도록 구현 책임을 나눈 방법에 초점을 맞췄습니다.