{"id":50100071,"url":"https://github.com/devslab-kr/ssrf-guard","last_synced_at":"2026-05-23T07:05:09.159Z","repository":{"id":316255191,"uuid":"1062612636","full_name":"devslab-kr/ssrf-guard","owner":"devslab-kr","description":"SSRF protection for Spring Boot — whitelist-based outbound HTTP guard with private-network blocking, redirect validation, and TOCTOU mitigation.","archived":false,"fork":false,"pushed_at":"2026-05-23T06:02:30.000Z","size":900,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-23T06:20:14.057Z","etag":null,"topics":["java","owasp","security","spring-boot","spring-boot-starter","ssrf","web-security"],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devslab-kr.png","metadata":{"files":{"readme":"README.ko.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.ko.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-23T13:42:19.000Z","updated_at":"2026-05-23T06:02:11.000Z","dependencies_parsed_at":"2025-09-23T16:09:16.376Z","dependency_job_id":"6000f0d8-a82d-4ca7-848b-343941449096","html_url":"https://github.com/devslab-kr/ssrf-guard","commit_stats":null,"previous_names":["jlc488/ssrf-guard","devslab-kr/ssrf-guard"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/devslab-kr/ssrf-guard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devslab-kr%2Fssrf-guard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devslab-kr%2Fssrf-guard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devslab-kr%2Fssrf-guard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devslab-kr%2Fssrf-guard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devslab-kr","download_url":"https://codeload.github.com/devslab-kr/ssrf-guard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devslab-kr%2Fssrf-guard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33386079,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T04:15:53.637Z","status":"ssl_error","status_checked_at":"2026-05-23T04:15:53.242Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java","owasp","security","spring-boot","spring-boot-starter","ssrf","web-security"],"created_at":"2026-05-23T07:05:08.333Z","updated_at":"2026-05-23T07:05:09.153Z","avatar_url":"https://github.com/devslab-kr.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ssrf-guard\n\n[English](README.md) · **한국어**\n\n\u003e JVM용 SSRF(Server-Side Request Forgery) 방어 — 화이트리스트 기반 outbound HTTP 가드. 사설망 차단, 리다이렉트 재검증, TOCTOU 완화, 그리고 **Spring AI 툴 URL 검증**으로 LLM 에이전트 SSRF 표면까지 차단.\n\n[![Maven Central](https://img.shields.io/maven-central/v/kr.devslab/ssrf-guard.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard)\n[![CI](https://github.com/devslab-kr/ssrf-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/devslab-kr/ssrf-guard/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/gh/devslab-kr/ssrf-guard/branch/main/graph/badge.svg)](https://codecov.io/gh/devslab-kr/ssrf-guard)\n[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)\n[![Java](https://img.shields.io/badge/Java-21+-orange.svg)](https://openjdk.org/projects/jdk/21/)\n[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.5+-green.svg)](https://spring.io/projects/spring-boot)\n\n📖 **[문서 → ssrf-guard.devslab.kr](https://ssrf-guard.devslab.kr/ko/)**\n\n## 모듈 매트릭스 (v3.0.0)\n\n쓰는 HTTP 클라이언트에 맞는 모듈만 고르세요. `ssrf-guard-core`는 transitive로 따라옴.\n\n| 모듈 | 용도 | Spring? |\n|---|---|---|\n| **`ssrf-guard`** | 메타 — RestClient + HttpClient5 (v2.0.0 호환) | ✅ |\n| `ssrf-guard-restclient` | Spring 6.1+ `RestClient` | ✅ |\n| `ssrf-guard-resttemplate` | Spring `RestTemplate` | ✅ |\n| `ssrf-guard-webclient` | Spring WebFlux `WebClient` | ✅ |\n| `ssrf-guard-feign` | Spring Cloud OpenFeign | ✅ |\n| **`ssrf-guard-springai`** ⭐ | Spring AI `ToolCallback` URL 검증 — LLM 에이전트 SSRF 차단 | ✅ |\n| `ssrf-guard-httpclient5` | Apache HttpClient 5 직접 | — |\n| `ssrf-guard-jdkhttp` | `java.net.http.HttpClient` | — |\n| `ssrf-guard-okhttp` | OkHttp | — |\n\n## 무엇을 하나\n\n서비스의 모든 outbound HTTP 호출이 소켓 열리기 전 4단계 SSRF 필터를 통과합니다:\n\n1. **URL 단계 체크 (최전선)** — 스킴 / 호스트 / 포트 / IP 리터럴 / userinfo, 가장 저렴한 게이트에서 DNS 전에 거부. 난독화된 IP 우회 (`http://2130706433/` → `127.0.0.1`) 차단.\n2. **DNS 시점 화이트리스트 재검증** — 호스트 정책을 한 번 더 적용.\n3. **사설망 IP 필터** — loopback, RFC-1918, link-local (AWS 메타데이터 `169.254.169.254` 포함), CGNAT, IPv6 ULA, **IPv4-mapped IPv6 + 6to4 unmapping** (`::ffff:10.0.0.5`와 `2002:0a00::`을 사설로 정확 분류).\n4. **리다이렉트 재검증** — 3xx 홉마다 동일 룰. 공격자가 `example.com` 화이트리스트 후 `169.254.169.254`로 redirect 못 시킴.\n\nresolver가 검증한 동일 `InetAddress` 배열이 HttpClient의 `Socket.connect()`에 전달됨 — TOCTOU 윈도우 닫힘.\n\n## Spring AI 툴 콜 — 새로운 SSRF 표면\n\nLLM 에이전트가 URL을 툴 인자로 받으면 기본적으로 SSRF 벡터:\n\n```java\n@Tool(\"Fetch a URL\")\nString fetchUrl(String url) {\n    return restClient.get().uri(url).retrieve().body(String.class);\n    //          ↑ 공격자가 URL 컨트롤 — SSRF 한 줄\n}\n```\n\n`ssrf-guard-springai`가 모든 `ToolCallback`을 감싸 URL 형식 인자를 정책 검증 후에만 실행되도록 막고, 거부 시 LLM이 해석/복구할 수 있는 구조화된 에러 문자열 반환.\n\n```java\nToolCallback[] raw = ToolCallbacks.from(new MyTools());\nToolCallback[] safe = SsrfGuardedToolCallbacks.wrap(raw, urlPolicy);\n```\n\n자동 설정으로 — 모든 `@Bean ToolCallback`이 `BeanPostProcessor`로 자동 wrap.\n\n## 설치\n\n### Maven\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003ekr.devslab\u003c/groupId\u003e\n    \u003cartifactId\u003essrf-guard\u003c/artifactId\u003e\n    \u003cversion\u003e3.0.0\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n### Gradle (Kotlin DSL)\n\n```kotlin\nimplementation(\"kr.devslab:ssrf-guard:3.0.0\")\n```\n\n\u003e **v2.0.0에서 업그레이드?** 메타 `kr.devslab:ssrf-guard:3.0.0`이 v2.0.0 API 유지 — `-core`, `-httpclient5`, `-restclient`를 transitive로 끌어옴. `kr.devslab.ssrfguard.security.*`를 직접 import한 코드는 업데이트 필요 — [v3.0.0 changelog](CHANGELOG.md#300--multi-module--llm-agent-ssrf-defense) 패키지 매핑 참고.\n\n## 설정\n\n```yaml\nssrf:\n  guard:\n    enabled: true                          # 마스터 스위치\n    allowed-schemes: [ \"http\", \"https\" ]\n    allowed-ports:  [ -1, 80, 443 ]        # -1 = 스킴 기본 포트\n    block-private-networks: true\n    reject-ip-literal-hosts: true          # NEW v3.0.0 — http://127.0.0.1, http://2130706433 등 차단\n    reject-user-info: true                 # NEW v3.0.0 — https://user:pass@host/... 차단\n    follow-redirects: true\n\n    # 정확 일치 화이트리스트\n    exact-hosts:\n      - api.partner.com\n      - billing.example.org\n\n    # 서픽스 화이트리스트 — `partner.com`은 `partner.com` 및 그 서브도메인 매치,\n    # 단 `badpartner.com`은 매치 안 됨 (라벨 경계 매치).\n    suffixes:\n      - partner.com\n      - example.org\n\n    connect-timeout: 5s\n    read-timeout: 10s\n```\n\n스타터가 classpath에 있으면 Spring Boot가 만들어주는 모든 `RestClient`가 자동으로 정책 적용 — 사용자가 별도 와이어업 안 해도 됨.\n\n## 사용\n\n```java\n@Service\npublic class PartnerApi {\n\n    private final RestClient client;\n\n    public PartnerApi(RestClient.Builder builder) {\n        this.client = builder.build();\n    }\n\n    public Customer fetch(long id) {\n        // 화이트리스트 호스트 → 통과. 리스트 밖이면 연결 열리기 전에\n        // SsrfGuardException 발생.\n        return client.get()\n                .uri(\"https://api.partner.com/customers/{id}\", id)\n                .retrieve()\n                .body(Customer.class);\n    }\n}\n```\n\n화이트리스트 아닐 때:\n\n```text\nkr.devslab.ssrfguard.core.SsrfGuardException: Host not allowed: evil.com\n    (reason=blocked_host, scheme=https, host=evil.com)\n    at kr.devslab.ssrfguard.core.UrlPolicy.reject(...)\n```\n\n`SsrfGuardException extends SecurityException` — v2.0.0의 `catch (SecurityException e)` 코드는 그대로 동작. 새 타입으로 catch하면 `e.reason()` (`BlockReason` enum) 접근 가능.\n\n## 관찰성 (Micrometer 자동)\n\n```\nssrf_guard_blocked_total{reason=\"blocked_private_ip\", scheme=\"http\"} 42\nssrf_guard_allowed_total{scheme=\"https\"} 13042\n```\n\n차단마다 구조화된 WARN 로그:\n\n```\nWARN k.d.s.core.UrlPolicy : ssrf-guard: Host not allowed: evil.com (reason=blocked_host, scheme=https, host=evil.com)\n```\n\n태그가 bounded — Prometheus / Datadog / CloudWatch 문제 없음.\n\n## 자동 구성이 등록하는 빈 (RestClient 모듈)\n\n`ssrf.guard.enabled=true`(기본값)이면 RestClient 자동설정이 활성화되어 다음을 등록:\n\n- `SafeDnsResolver` — 화이트리스트 + 사설 IP 필터. Apache HttpClient 5 connection manager에 연결\n- `CloseableHttpClient` — resolver와 (리다이렉트 활성 시) `SafeRedirectStrategy` 와이어업\n- `HttpComponentsClientHttpRequestFactory` — connect/read timeout 적용\n- `UrlPolicy` — 최전선 URL 단계 게이트 (스킴, 호스트, 포트, IP 리터럴, userinfo)\n- `SsrfGuardClientHttpRequestInterceptor` — Spring `ClientHttpRequestInterceptor`, 정책에 위임\n- `SsrfGuardMetrics` — `MeterRegistry` 있으면 Micrometer 기반, 없으면 no-op\n- `RestClientCustomizer` (`ssrfRestClientCustomizer`) — factory + interceptor를 Spring Boot 자동 `RestClient.Builder`에 핀\n\n모듈마다 자기 자동설정 보유 — `SsrfGuardRestTemplateAutoConfiguration`, `SsrfGuardWebClientAutoConfiguration`, `SsrfGuardFeignAutoConfiguration`, `SsrfGuardSpringAiAutoConfiguration`. 동일 `UrlPolicy`와 `SsrfGuardMetrics` 빈 공유. 모든 빈이 `@ConditionalOnMissingBean` — 일부만 교체 가능.\n\n## 요구사항\n\n- Java 21+\n- Spring Boot 3.5+ (Spring 기반 모듈)\n- Spring AI 1.0+ (`springai` 모듈)\n- Spring Cloud 2024.0+ (`feign` 모듈)\n- Apache HttpClient 5 (`-httpclient5`, `-restclient`, `-resttemplate`이 transitive로 끌어옴)\n\n## 라이선스\n\nApache License 2.0 — [LICENSE](LICENSE), [NOTICE](NOTICE) 참고.\n\n---\n\n[Devslab](https://devslab.kr) 제작 · [DevsLab 오픈소스 툴킷](https://github.com/devslab-kr) 일부.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevslab-kr%2Fssrf-guard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevslab-kr%2Fssrf-guard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevslab-kr%2Fssrf-guard/lists"}