diff --git a/src/main/java/com/pandoli365/bibimbap/security/SsrfSafeFetcher.java b/src/main/java/com/pandoli365/bibimbap/security/SsrfSafeFetcher.java index 8602fe3..bbedc15 100644 --- a/src/main/java/com/pandoli365/bibimbap/security/SsrfSafeFetcher.java +++ b/src/main/java/com/pandoli365/bibimbap/security/SsrfSafeFetcher.java @@ -42,7 +42,8 @@ import org.springframework.stereotype.Component; * *

잔여 TOCTOU(검증 시점 IP 와 HttpClient 가 실제 connect 하는 IP 의 불일치 가능성)는 JVM * positive DNS 캐시({@code networkaddress.cache.ttl})로 동일 lookup 이 재사용되도록 두어 창을 - * 최소화한다. 추가로 매 홉 connect 직전 재검증을 수행한다. + * 최소화한다 (@PostConstruct 로 설정; best-effort — 결정적 보장은 JVM 레벨 {@code networkaddress.cache.ttl=30} 이 정본). + * 추가로 매 홉 connect 직전 재검증을 수행한다. */ @Component public class SsrfSafeFetcher { @@ -71,6 +72,22 @@ public class SsrfSafeFetcher { .connectTimeout(CONNECT_TIMEOUT) .build(); + /** + * positive DNS 캐시 TTL 을 30초로 고정한다 (DNS rebinding TOCTOU 창 최소화). + * + *

best-effort 한계: {@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 방어 하에 가져온다. * diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 351d00b..3207daa 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -22,6 +22,9 @@ spring.config.import=optional:classpath:${spring.profiles.active}/db.properties, app.webgl.asset-origin= app.webgl.frame-ancestors=self +# upload storage (static 트리 밖 — 웹서버 직접 서빙 차단, 컨트롤러 권한게이트 경유) +app.upload.game-storage-path=${user.home}/.bibimbap/uploads + # log mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl logging.level.org.apache.ibatis=TRACE diff --git a/src/test/java/com/pandoli365/bibimbap/security/SsrfSafeFetcherTest.java b/src/test/java/com/pandoli365/bibimbap/security/SsrfSafeFetcherTest.java index 290ccde..4be8783 100644 --- a/src/test/java/com/pandoli365/bibimbap/security/SsrfSafeFetcherTest.java +++ b/src/test/java/com/pandoli365/bibimbap/security/SsrfSafeFetcherTest.java @@ -324,6 +324,17 @@ class SsrfSafeFetcherTest { 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 ────────────────────────────────────────────────────────── private void startServer(HttpHandler handler) throws IOException {