Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# devslab-examples

[English](README.md) · **한국어**

[devslab-kr](https://github.com/devslab-kr) Spring Boot 스타터와 라이브러리들의 실행 가능한 예제 모음.

각 서브디렉토리는 자체 Gradle 빌드를 가진 **독립 Spring Boot 앱**입니다. 골라서 `cd` 하고 `./gradlew bootRun` 만 하면 됩니다.

> 💬 질문, 아이디어, 데모 응용 사례 공유는 [**Discussions**](https://github.com/devslab-kr/devslab-examples/discussions)에서 — 영어/한국어 둘 다 환영, 라이브러리 만든 메인테이너가 직접 답변합니다.

## 예제

| 데모 | 보여주는 것 | Maven Central 좌표 |
| --- | --- | --- |
| [`easy-paging-demo`](easy-paging-demo/) | `@AutoPaginate` 어노테이션 기반 offset 페이지네이션 (Spring Boot + MyBatis + H2) | [`kr.devslab:easy-paging-spring-boot-starter:0.4.0`](https://central.sonatype.com/artifact/kr.devslab/easy-paging-spring-boot-starter) |
| [`easy-paging-keyset-demo`](easy-paging-keyset-demo/) | `@KeysetPaginate` 커서(keyset) 페이지네이션 — composite `(time, id)` 키, 쓰기 중에도 안정, `OFFSET`/`COUNT(*)` 없음 | [`kr.devslab:easy-paging-spring-boot-starter:0.4.0`](https://central.sonatype.com/artifact/kr.devslab/easy-paging-spring-boot-starter) |
| [`easy-paging-postgres-demo`](easy-paging-postgres-demo/) | 동일 스타터를 **실제 PostgreSQL**과 — `bootRun`은 Docker Compose, 테스트는 Testcontainers + `@ServiceConnection`, 로컬 DB 설치 불필요 | [`kr.devslab:easy-paging-spring-boot-starter:0.4.0`](https://central.sonatype.com/artifact/kr.devslab/easy-paging-spring-boot-starter) |
| [`easy-paging-reactive-demo`](easy-paging-reactive-demo/) | Reactive 스택 — **WebFlux + R2DBC**, `R2dbcOffsetPagingSupport` 사용. MVC 데모와 동일한 JSON 봉투를 `Mono<PageResponse<T>>`로 서빙 | [`kr.devslab:easy-paging-spring-boot-starter-reactive:0.4.0`](https://central.sonatype.com/artifact/kr.devslab/easy-paging-spring-boot-starter-reactive) |
| [`ssrf-guard-demo`](ssrf-guard-demo/) | SSRF(Server-Side Request Forgery) 방어를 3종 Spring HTTP 클라이언트(RestClient, RestTemplate, WebClient)에 동시 적용 — 모두 같은 `UrlPolicy`. 15가지 공격 매트릭스 엔드포인트, Micrometer 메트릭 포함 | [`kr.devslab:ssrf-guard:3.0.1`](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard) |
| [`ssrf-guard-springai-demo`](ssrf-guard-springai-demo/) | ⭐ **LLM 에이전트 SSRF 방어.** 모든 Spring AI `ToolCallback`을 자동으로 wrap해서 LLM이 `fetch_url`을 호출하기 전에 URL 인자를 검증. 가짜 LLM 드라이버로 API 키 없이 오프라인 실행 가능 | [`kr.devslab:ssrf-guard-springai:3.0.1`](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard-springai) |
| [`ssrf-guard-feign-demo`](ssrf-guard-feign-demo/) | Spring Cloud OpenFeign `RequestInterceptor` — `@FeignClient` 호출에 동일 `UrlPolicy` 적용. 화이트리스트 / 비화이트리스트 `@FeignClient` 2개로 차단 경로 시연 | [`kr.devslab:ssrf-guard-feign:3.0.1`](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard-feign) |
| [`ssrf-guard-jdkhttp-demo`](ssrf-guard-jdkhttp-demo/) | `java.net.http.HttpClient`(Java 11+) 래퍼 — 라이브러리 자체엔 Spring 의존성 없음. `main()`에서 3줄 wiring | [`kr.devslab:ssrf-guard-jdkhttp:3.0.1`](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard-jdkhttp) |
| [`ssrf-guard-okhttp-demo`](ssrf-guard-okhttp-demo/) | OkHttp `Interceptor` + `Dns` — Spring 필요 없음. `OkHttpClient.Builder`에 3줄 wiring | [`kr.devslab:ssrf-guard-okhttp:3.0.1`](https://central.sonatype.com/artifact/kr.devslab/ssrf-guard-okhttp) |

## 컨벤션

- 각 데모는 **독립 Gradle 프로젝트** — 자체 `settings.gradle.kts`, `build.gradle.kts`, `gradlew`를 가짐. 루트 빌드를 공유하지 않으므로 의존성 버전이나 JDK 타겟이 독립적으로 변할 수 있음.
- 각 데모는 자기가 시연하는 스타터의 **최신 stable 릴리즈**에 의존 (`build.gradle.kts`에서 버전 핀). 새 릴리즈가 나오면 Dependabot이 bump.
- 이 repo는 **버전/태그 안 함** — 데모는 발행되는 아티팩트가 아님. `main`이 진실의 소스.
- 각 데모는 자체 `README.md`에 빠른 시작, 전제조건, 스타터가 하는 일의 가이드 포함.

## 새 데모 추가하기

1. `<starter-shortname>-demo/`를 repo 루트에 생성.
2. 기존 데모 (예: `easy-paging-demo/`) 레이아웃을 템플릿으로 복사.
3. 위 표에 데모 링크 + Maven Central 스타터 링크로 행 추가.
4. CI는 `build.gradle.kts` 존재로 새 데모를 자동 감지 — workflow 변경 불필요.

## CI

PR은 **변경된 파일이 있는 데모만** 빌드. `main` push는 **모든 데모** 빌드 (스타터 버전 bump로 인한 drift 잡기 위해).
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# devslab-examples

**English** · [한국어](README.ko.md)

Runnable examples for [devslab-kr](https://github.com/devslab-kr) Spring Boot starters and libraries.

Each subdirectory is an **independent** Spring Boot application with its own Gradle build. Pick one, `cd` into it, and run `./gradlew bootRun`.

> 💬 Questions, ideas, or sharing your application of the demos? Head to [**Discussions**](https://github.com/devslab-kr/devslab-examples/discussions) — bilingual (English / Korean), maintained by the same folks who write the libraries.

## Examples

| Demo | Showcases | Maven Central coordinates |
Expand Down
163 changes: 163 additions & 0 deletions ssrf-guard-demo/README.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# ssrf-guard-demo

[English](README.md) · **한국어**

[`ssrf-guard`](https://github.com/devslab-kr/ssrf-guard) — JVM용 SSRF(Server-Side Request Forgery) 방어 라이브러리의 실행 가능한 예제.

하나의 Spring Boot 앱에 **3종 Spring HTTP 클라이언트**가 모두 동일 `UrlPolicy`를 통해 wiring됨:

- `RestClient` (Spring 6.1+) — 메타 아티팩트 `kr.devslab:ssrf-guard:3.0.1`
- `RestTemplate` — `kr.devslab:ssrf-guard-resttemplate:3.0.1`
- `WebClient` (WebFlux) — `kr.devslab:ssrf-guard-webclient:3.0.1`

추가로 `/attacks` 엔드포인트는 가드가 차단하는 모든 SSRF 우회 패턴 목록을 각 모듈별 curl 예제와 함께 제공합니다.

## 전제조건

- JDK 21+
- 인터넷 접근 가능한 화이트리스트 호스트 (`httpbin.org` 기본 — 허용 경로가 실제로 네트워크에 닿는 것을 보여주기 위함)

## 실행

```bash
cd ssrf-guard-demo
./gradlew bootRun
```

앱은 `http://localhost:8080`에 뜸.

## 시험해보기

### 허용 — RestClient가 실제 httpbin.org에 도달

```bash
curl 'http://localhost:8080/fetch?url=https://httpbin.org/get' | jq
```

```json
{
"status": "allowed",
"client": "RestClient",
"url": "https://httpbin.org/get",
"bodyPreview": "{\n \"args\": {}, \n \"headers\": { ... }, \n ..."
}
```

### 차단 — AWS 메타데이터 탈취 시도 (canonical SSRF→cloud-takeover)

```bash
curl 'http://localhost:8080/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/' | jq
```

```json
{
"status": "blocked",
"client": "RestClient",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"reason": "blocked_ip_literal",
"message": "IP-literal host blocked (rejectIpLiteralHosts=true): 169.254.169.254"
}
```

### 차단 — 화이트리스트 밖의 호스트

```bash
curl 'http://localhost:8080/fetch?url=https://evil.com/' | jq
# reason: "blocked_host"
```

### 차단 — 십진수로 인코딩된 loopback (`2130706433` == `127.0.0.1`)

```bash
curl 'http://localhost:8080/fetch?url=http://2130706433/' | jq
# reason: "blocked_ip_literal"
```

### 차단 — userinfo (`user:pass@host`)

```bash
curl 'http://localhost:8080/fetch?url=https://user:pass@httpbin.org/get' | jq
# reason: "blocked_userinfo"
```

### 차단 — AWS 메타데이터로의 redirect (4번째 방어 레이어)

```bash
curl 'http://localhost:8080/fetch?url=https://httpbin.org/redirect-to?url=http://169.254.169.254/' | jq
# 2번째 hop에서 잡힘 — 초기 호스트는 화이트리스트지만 redirect 전략이 매 URL 변경을 재검증함
```

### 같은 공격, 다른 HTTP 클라이언트, 같은 결과

```bash
curl 'http://localhost:8080/fetch-resttemplate?url=http://169.254.169.254/'
curl 'http://localhost:8080/fetch-webclient?url=http://169.254.169.254/'
```

동일한 `reason` 필드 — 세 HTTP 클라이언트 모두 같은 `UrlPolicy` 빈으로 래핑됨.

### 전체 공격 매트릭스 (15개)

```bash
curl http://localhost:8080/attacks | jq
```

각 공격 패턴마다 `expectedReason`과 미리 만들어진 `tryRestClient` / `tryRestTemplate` / `tryWebClient` curl 문자열을 반환. 하나를 골라 `bash`로 파이프:

```bash
curl -s http://localhost:8080/attacks \
| jq -r '.attacks[] | select(.name == "aws-metadata-credentials") | .tryRestClient' \
| bash | jq
```

### 관찰성 — Micrometer 메트릭

```bash
# 위 curl 몇 개 실행 후:
curl -s http://localhost:8080/actuator/metrics/ssrf_guard_blocked_total | jq
curl -s http://localhost:8080/actuator/prometheus | grep ssrf_guard
```

`reason` 태그별 카운터 (`blocked_host`, `blocked_ip_literal`, `blocked_private_ip`, `blocked_userinfo`, `blocked_redirect`, ...) + 허용된 요청에 대한 별도 `ssrf_guard_allowed_total`을 볼 수 있음.

## 읽을 만한 파일

| 파일 | 왜 |
| --- | --- |
| `build.gradle.kts` | 표준 스타터 외 의존성은 `kr.devslab:ssrf-guard:3.0.1`, `:ssrf-guard-resttemplate:3.0.1`, `:ssrf-guard-webclient:3.0.1` 셋뿐 — 별도 configuration 클래스 불필요 |
| `application.yml` | 모든 `ssrf.guard.*` 옵션이 한 곳에 주석과 함께 |
| `web/FetchController.java` | RestClient 전체 — 3줄 setup, 가드는 보이지 않게 실행 |
| `web/FetchResttemplateController.java` | RestTemplate 동일 — 레거시 코드 마이그레이션 불필요 |
| `web/FetchWebClientController.java` | Reactive 버전; `SsrfGuardException`이 `Mono.onErrorResume`을 통해 흐르는 방식 시연 |
| `web/AttackDemoController.java` | 각 공격 패턴 + 예상 `BlockReason` 카탈로그 |

## 화이트리스트 풀기 (sanity check)

가드가 실제로 뭐 하는지 확인하고 싶다면? `application.yml` 수정:

```yaml
ssrf:
guard:
enabled: false
```

재시작 후 AWS 메타데이터 curl 다시 — 이제 실제로 `169.254.169.254`에 시도함 (대부분 네트워크에서 타임아웃 나지만, 가드가 더 이상 개입 안 함).

## 빌드 검증

```bash
./gradlew build
```

스모크 테스트 `SsrfGuardDemoApplicationTests`가:

1. 화이트리스트 URL이 통과 (`status=allowed`)
2. 공격 URL이 올바른 `reason` 태그로 차단됨
3. 차단 후 actuator 메트릭 엔드포인트가 `ssrf_guard_blocked_total`을 노출

## 더 읽기

- ssrf-guard 도큐사이트: <https://ssrf-guard.devslab.kr/>
- ssrf-guard repo: <https://github.com/devslab-kr/ssrf-guard>
- Java SSRF 학습 (이 데모의 공격 패턴 출처): <https://github.com/JoyChou93/java-sec-code>
- OWASP SSRF Top 10: <https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/>
2 changes: 2 additions & 0 deletions ssrf-guard-demo/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# ssrf-guard-demo

**English** · [한국어](README.ko.md)

Runnable example for [`ssrf-guard`](https://github.com/devslab-kr/ssrf-guard) — SSRF (Server-Side Request Forgery) protection for the JVM.

One Spring Boot app shows **all three Spring HTTP clients** wired through the same `UrlPolicy`:
Expand Down
44 changes: 44 additions & 0 deletions ssrf-guard-feign-demo/README.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# ssrf-guard-feign-demo

[English](README.md) · **한국어**

[`ssrf-guard-feign`](https://github.com/devslab-kr/ssrf-guard) — Spring Cloud OpenFeign 클라이언트용 SSRF 방어 예제.

선언적 `@FeignClient` 인터페이스 2개가 하나의 `UrlPolicy`를 공유:
- `HttpBinClient` — `https://httpbin.org` 가리킴 (화이트리스트) — 호출 성공
- `EvilClient` — `https://evil.com` 가리킴 (화이트리스트 밖) — Feign `RequestInterceptor`에서 차단됨, HTTP 트래픽이 JVM을 떠나지 않음

## 실행

```bash
cd ssrf-guard-feign-demo
./gradlew bootRun
```

## 시험해보기

```bash
# 화이트리스트 호스트 — 실제 httpbin.org 호출
curl http://localhost:8080/feign/legit | jq

# 화이트리스트 밖 — SSRF 가드 인터셉터에서 차단
curl http://localhost:8080/feign/evil | jq
# → { "status": "blocked", "reason": "blocked_host", ... }
```

## 읽을 만한 파일

| 파일 | 왜 |
| --- | --- |
| `build.gradle.kts` | `kr.devslab:ssrf-guard-feign:3.0.1` + `spring-cloud-starter-openfeign` |
| `HttpBinClient.java` / `EvilClient.java` | 평범한 `@FeignClient` 인터페이스 2개 — 가드 코드 없음 |
| `FeignDemoController.java` | `SsrfGuardException` catch (Feign이 한 단계 wrap — 컨트롤러가 unwrap) |
| `application.yml` | `ssrf.guard.exact-hosts: [httpbin.org]` — 그 한 줄이 화이트리스트 |

Feign 인터셉터는 자동 등록됨 — `ssrf-guard-feign-3.0.1`이 Spring 자동설정으로 `feign.RequestInterceptor` 빈을 publish하고, Spring Cloud OpenFeign이 모든 `@FeignClient`에 적용.

## 빌드 검증

```bash
./gradlew build
```
2 changes: 2 additions & 0 deletions ssrf-guard-feign-demo/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# ssrf-guard-feign-demo

**English** · [한국어](README.ko.md)

Runnable example for [`ssrf-guard-feign`](https://github.com/devslab-kr/ssrf-guard) — SSRF protection for Spring Cloud OpenFeign clients.

Two declarative `@FeignClient` interfaces share one `UrlPolicy`:
Expand Down
72 changes: 72 additions & 0 deletions ssrf-guard-jdkhttp-demo/README.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# ssrf-guard-jdkhttp-demo

[English](README.md) · **한국어**

[`ssrf-guard-jdkhttp`](https://github.com/devslab-kr/ssrf-guard) — JDK 표준 `java.net.http.HttpClient` (Java 11+)용 SSRF 방어 예제.

**라이브러리 자체엔 Spring 필요 없음.** 이 데모는 REST 엔드포인트 노출을 위해서만 Spring Boot를 사용합니다. 실제 SSRF Guard wiring은 `SsrfGuardJdkHttpDemoApplication.java`의 3줄입니다.

## 실행

```bash
cd ssrf-guard-jdkhttp-demo
./gradlew bootRun
```

## 시험해보기

```bash
# 허용
curl 'http://localhost:8080/fetch?url=https://httpbin.org/get' | jq

# AWS 메타데이터 — URL 단계 IP-리터럴 체크에서 차단
curl 'http://localhost:8080/fetch?url=http://169.254.169.254/' | jq

# 십진수 인코딩 loopback
curl 'http://localhost:8080/fetch?url=http://2130706433/' | jq

# 화이트리스트 밖 호스트
curl 'http://localhost:8080/fetch?url=https://evil.com/' | jq
```

## 읽을 만한 파일

| 파일 | 왜 |
| --- | --- |
| `build.gradle.kts` | 의존성 하나: `kr.devslab:ssrf-guard-jdkhttp:3.0.1` |
| `SsrfGuardJdkHttpDemoApplication.java` | 전체 스토리: `HostPolicy` → `UrlPolicy` → `HttpClient` wrap |
| `JdkHttpDemoController.java` | 평범한 `client.send(req, ...)` — 호출부에서 wrap은 보이지 않음 |

## Spring 없이 사용

위 wiring은 Spring 의존적이지 않습니다 — 정책 클래스 (`HostPolicy`, `UrlPolicy`, `SsrfGuardedHttpClient`)는 POJO:

```java
HostPolicy hostPolicy = new HostPolicy(
List.of("api.partner.com"), // exactHosts
List.of() // suffixes
);
UrlPolicy urlPolicy = new UrlPolicy(
Set.of("https"),
Set.of(-1, 443),
hostPolicy,
true, // rejectIpLiteralHosts
true, // rejectUserInfo
NoOpSsrfGuardMetrics.INSTANCE
);
SsrfGuardedHttpClient safe = new SsrfGuardedHttpClient(
HttpClient.newHttpClient(), urlPolicy, true);

// java.net.http.HttpClient처럼 그대로 사용
HttpResponse<String> resp = safe.send(
HttpRequest.newBuilder(URI.create("https://api.partner.com/")).build(),
HttpResponse.BodyHandlers.ofString());
```

Lambda, AWS SDK 소비자, Quarkus 앱, CLI 도구 — `java.net.http`를 Spring 없이 쓰는 모든 환경에 유용.

## 빌드 검증

```bash
./gradlew build
```
2 changes: 2 additions & 0 deletions ssrf-guard-jdkhttp-demo/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# ssrf-guard-jdkhttp-demo

**English** · [한국어](README.ko.md)

Runnable example for [`ssrf-guard-jdkhttp`](https://github.com/devslab-kr/ssrf-guard) — SSRF protection for the JDK standard `java.net.http.HttpClient` (Java 11+).

**No Spring needed for the library itself.** This demo uses Spring Boot only to expose a REST endpoint for curl. The actual SSRF Guard wiring is three lines in `SsrfGuardJdkHttpDemoApplication.java`.
Expand Down
Loading
Loading