feat(security): SSRF rebinding 완화 + 업로드 저장루트 static 트리 밖 이전
W3 후속 하드닝 2건.
- b1: SsrfSafeFetcher @PostConstruct networkaddress.cache.ttl=30 고정
— 검증~connect 동일 lookup 재사용으로 DNS rebinding TOCTOU 창 최소화.
런타임 반영은 best-effort(InetAddressCachePolicy lazy init); 결정적
보장은 JVM 레벨 java.security 가 정본임을 주석 명시.
- b2: app.upload.game-storage-path=${user.home}/.bibimbap/uploads
— 업로드물을 static 서빙 트리 밖으로 이전(웹서버 직접 서빙 차단,
컨트롤러 권한게이트 경유). gameRoot/profile 자연 정합, @Value 기본값은
IDE 로컬 부작용 회피로 유지(명시 properties 오버라이드).
- 테스트 1건 신규(ttl 값 검증). L1 353/353 GREEN, 회귀 0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
This commit is contained in:
parent
3d10449e67
commit
9041bb7c70
|
|
@ -42,7 +42,8 @@ import org.springframework.stereotype.Component;
|
||||||
*
|
*
|
||||||
* <p>잔여 TOCTOU(검증 시점 IP 와 HttpClient 가 실제 connect 하는 IP 의 불일치 가능성)는 JVM
|
* <p>잔여 TOCTOU(검증 시점 IP 와 HttpClient 가 실제 connect 하는 IP 의 불일치 가능성)는 JVM
|
||||||
* positive DNS 캐시({@code networkaddress.cache.ttl})로 동일 lookup 이 재사용되도록 두어 창을
|
* positive DNS 캐시({@code networkaddress.cache.ttl})로 동일 lookup 이 재사용되도록 두어 창을
|
||||||
* 최소화한다. 추가로 매 홉 connect 직전 재검증을 수행한다.
|
* 최소화한다 (@PostConstruct 로 설정; best-effort — 결정적 보장은 JVM 레벨 {@code networkaddress.cache.ttl=30} 이 정본).
|
||||||
|
* 추가로 매 홉 connect 직전 재검증을 수행한다.
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class SsrfSafeFetcher {
|
public class SsrfSafeFetcher {
|
||||||
|
|
@ -71,6 +72,22 @@ public class SsrfSafeFetcher {
|
||||||
.connectTimeout(CONNECT_TIMEOUT)
|
.connectTimeout(CONNECT_TIMEOUT)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* positive DNS 캐시 TTL 을 30초로 고정한다 (DNS rebinding TOCTOU 창 최소화).
|
||||||
|
*
|
||||||
|
* <p><b>best-effort 한계</b>: {@code sun.net.InetAddressCachePolicy} 는 최초 {@link InetAddress}
|
||||||
|
* 조회 시 Security property 를 lazy 하게 읽는다. 이 @PostConstruct 가 첫 DNS 조회 전에 실행되면
|
||||||
|
* 반영되지만, 다른 빈이 먼저 DNS 조회를 했다면 런타임 반영이 보장되지 않는다.
|
||||||
|
* 결정적 보장이 필요하면 JVM 레벨({@code $JAVA_HOME/conf/security/java.security} 의
|
||||||
|
* {@code networkaddress.cache.ttl=30})이 정본이다.
|
||||||
|
*/
|
||||||
|
@jakarta.annotation.PostConstruct
|
||||||
|
void initDnsCachePolicy() {
|
||||||
|
// 검증 시점 IP 와 connect 시점 IP 가 동일 lookup 을 재사용하도록 positive 캐시를 양수로 고정
|
||||||
|
// (DNS rebinding TOCTOU 창 최소화). 미설정 시 JDK 기본은 구현의존이라 결정적이지 않다.
|
||||||
|
java.security.Security.setProperty("networkaddress.cache.ttl", "30");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 외부 URL 을 SSRF 방어 하에 가져온다.
|
* 외부 URL 을 SSRF 방어 하에 가져온다.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ spring.config.import=optional:classpath:${spring.profiles.active}/db.properties,
|
||||||
app.webgl.asset-origin=
|
app.webgl.asset-origin=
|
||||||
app.webgl.frame-ancestors=self
|
app.webgl.frame-ancestors=self
|
||||||
|
|
||||||
|
# upload storage (static 트리 밖 — 웹서버 직접 서빙 차단, 컨트롤러 권한게이트 경유)
|
||||||
|
app.upload.game-storage-path=${user.home}/.bibimbap/uploads
|
||||||
|
|
||||||
# log
|
# log
|
||||||
mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
|
mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||||
logging.level.org.apache.ibatis=TRACE
|
logging.level.org.apache.ibatis=TRACE
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,17 @@ class SsrfSafeFetcherTest {
|
||||||
assertEquals(html, new String(result.get().body(), StandardCharsets.UTF_8));
|
assertEquals(html, new String(result.get().body(), StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── b1: initDnsCachePolicy ────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void initDnsCachePolicySetsPositiveTtl() {
|
||||||
|
// @PostConstruct 호출 후 Security property "networkaddress.cache.ttl" 이 "30" 인지 검증.
|
||||||
|
// (런타임 InetAddress 반영 여부는 단위테스트 범위 밖 — 값 설정만 검증.)
|
||||||
|
SsrfSafeFetcher fetcher = new SsrfSafeFetcher();
|
||||||
|
fetcher.initDnsCachePolicy();
|
||||||
|
assertEquals("30", java.security.Security.getProperty("networkaddress.cache.ttl"));
|
||||||
|
}
|
||||||
|
|
||||||
// ── helpers ──────────────────────────────────────────────────────────
|
// ── helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void startServer(HttpHandler handler) throws IOException {
|
private void startServer(HttpHandler handler) throws IOException {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue