[Spring Security]Builder 패턴에서 체이닝 중간 단계의 타입 변경 문제

Mando·3일 전
2

오픈소스

목록 보기
3/3

발견한 문제

StrictFirewallHttpRequest request = getRequest();

// 문제: header() 호출 후 타입이 바뀜
var result = request.mutate()
    .header("X-Custom", "value")  // StrictFirewallBuilder → DefaultBuilder
    .build();

처음 반응: "이게 뭐가 문제야?"

솔직히 처음엔 이렇게 생각했다.

"어차피 마지막에 build()를 쓰고 이걸 사용할텐데, 중간에 타입이 바뀌면 어때? 최종 결과물만 제대로 나오면 되지!"

그래서 이 이슈를 그냥 무시하려고 했다. 그런데...

계속 생각하게 만든 것: 메인테이너의 PR

제가 "별거 아니네"라고 넘어가려던 순간, 프로젝트 메인테이너가 이 문제를 수정하는 PR을 올렸다.

"어? 메인테이너가 이걸 중요하게 생각하네? 내가 뭘 놓치고 있는 거지?"

첫 번째 깨달음: 보안은 체인의 모든 고리가 중요하다

// StrictFirewallBuilder의 실제 구현
@Override
public Builder header(String name, String value) {
    // 이 순간! 바로 여기서 보안 검사가 일어난다
    if (containsMaliciousPattern(value)) {
        throw new SecurityException("악의적인 패턴 감지!");
    }
    return super.header(name, value);
}

"아, header()를 호출하는 그 순간에 보안 검사가 일어나는구나! build()까지 기다리는 게 아니라!"

두 번째 깨달음: build()는 단순 조립공이다

처음엔 build()가 마법처럼 모든 보안을 적용해줄 거라 생각했습니다:

// 내 착각
.header("Evil", "../../etc/passwd")  // 일단 넣고
.build();  // 여기서 보안 마법이 일어날 거야!

// 실제 build() 구현
public ServerHttpRequest build() {
    // 그냥... 조립만 합니다
    return new MutatedServerHttpRequest(this.headers, this.cookies);
}

"build()는 레고 조립하듯이 지금까지 쌓인 조각들을 합치기만 하는구나. 추가 검증은 없어!"

세 번째 깨달음: 타입은 계약이다

// 다른 개발자가 이 코드를 볼 때
StrictFirewallHttpRequest request = getRequest();
var builder = request.mutate();  // "오, 보안 적용된 빌더구나!"

// 하지만 실제로는...
builder = builder.header("X", "Y");  // 이제 일반 빌더로 변신!
// 개발자는 여전히 보안 빌더라고 착각 중...

"타입은 단순한 라벨이 아니라 '이 객체가 뭘 할 수 있는지'에 대한 약속이구나!"

네 번째 깨달음: 디버깅 지옥을 만들 수 있다

6개월 후 프로덕션에서 보안 이슈가 터졌을 때:

// 로그 분석
DEBUG: Initial type: StrictFirewallHttpRequest ✓
DEBUG: After mutate(): StrictFirewallBuilder ✓
DEBUG: After header(): DefaultBuilder// 여기가 문제!
DEBUG: Final result: MutatedServerHttpRequest ?

// 팀원들의 반응
"코드 보면 StrictFirewall 쓰고 있는데요?"
"로그도 처음엔 StrictFirewall이네요?"
"그럼 왜 보안 검사가 안 된 거지...?"

"타입 불일치는 '조용한 버그'를 만든다. 겉으로는 정상인데 실제론 다르게 동작하니까!"

다섯 번째 깨달음: 실제 공격이 가능해진다

// 해커가 이런 요청을 보낼 때
request.mutate()
    .header("X-Include", "../../etc/passwd")     // 파일 접근 시도
    .header("X-Command", "; DROP TABLE users;")  // SQL 인젝션
    .build();

// StrictFirewallBuilder였다면 → 즉시 차단
// 일반 Builder로 바뀐 후 → 그대로 통과

깨달음: "작은 타입 변경이 실제 보안 구멍을 만들 수 있구나!"

마무리

처음엔 "뭐 이런 걸로..."라고 생각했던 이슈가, 결국 API 설계와 보안에 대한 중요한 통찰을 주었다.

  • Builder 패턴에서는 각 단계가 즉시 실행된다.
  • 타입 이름은 단순한 라벨이 아니라 그 객체의 능력과 책임을 나타내는 계약이다. StrictFirewallBuilder라는 이름은 "나는 보안 검사를 한다"는 약속이다.
  • 좋은 API는 개발자가 실수하기 어렵게 만들어야 한다. 타입 일관성은 이를 위한 중요한 도구이다.

1개의 댓글

comment-user-thumbnail
3일 전

평소에 당연하게 여겨 생각하지 못했던 부분인데 매우 중요하겠네요... 앞으로 저도 조심히 접근해봐야겠습니다. 좋은 글 감사합니다.

답글 달기