Spring Boot 4.1でSSRF対策:InetAddressFilterでRestClient/WebClientを安全にする

Spring Boot 4.1では、HTTP Clientまわりのセキュリティ強化として InetAddressFilter が追加されました。

これは、Spring Bootアプリケーションから外部URLへHTTPリクエストを送るときに、接続してよい宛先IPアドレスを制限するための仕組みです。Spring Boot公式ドキュメントでも、HTTP Clientが呼び出せるリモートアドレスを制限する方法として InetAddressFilter が紹介されており、SSRF対策に有効な手段として説明されています。

Spring Bootアプリでは、次のようなコードを書くことがあります。

String html = restClient.get()
        .uri(userInputUrl)
        .retrieve()
        .body(String.class);

一見よくある外部API呼び出しですが、userInputUrl にユーザー入力が入る場合は注意が必要です。

例えば、攻撃者が次のようなURLを指定したらどうなるでしょうか。

http://localhost:8080/admin
http://127.0.0.1:8080/internal
http://169.254.169.254/latest/meta-data/
http://192.168.0.10/private-api

アプリケーションサーバーがそのURLへアクセスしてしまうと、本来外部から触れないはずの内部サービス、管理画面、メタデータエンドポイントなどに到達できてしまう可能性があります。

このような攻撃が SSRF です。

SSRFとは何か

SSRFは Server-Side Request Forgery の略です。

簡単にいうと、攻撃者がサーバーに対して「このURLを取りに行って」と指示し、サーバー側から本来アクセスしてはいけない場所へリクエストさせる攻撃です。

通常、攻撃者のブラウザからは社内ネットワークや localhost にはアクセスできません。

しかし、バックエンドサーバーは社内ネットワーク上にいたり、クラウド環境のメタデータサービスへアクセスできたりします。

そのため、ユーザーが指定したURLをサーバー側でそのまま取得する機能は危険になりがちです。

たとえば、次のような機能です。

・URLを指定してOGP情報を取得する
・URLを指定して画像をダウンロードする
・Webhook URLへ通知する
・外部APIのURLを画面から登録できる
・CSVやJSONのURLを指定して取り込む

どれも業務アプリでありそうな機能です。
だからこそ、SSRF対策はかなり実務寄りのテーマです。

URL文字列チェックだけでは不十分

SSRF対策として、最初に思いつくのはURL文字列のチェックです。

if (url.startsWith("http://localhost")) {
    throw new IllegalArgumentException("localhost is not allowed");
}

ただ、この方法だけでは弱いです。

理由は、危険な宛先の表現方法がいろいろあるからです。

・localhost
・127.0.0.1
・0.0.0.0
・IPv6の ::1
・プライベートIPアドレス
・リンクローカルアドレス
・DNSで内部IPに解決されるドメイン
・リダイレクト先が内部IPになるURL

つまり、URLの文字列だけを見て「これは安全」と判断するのは危険です。

Spring Boot 4.1の InetAddressFilter は、HTTP Clientが接続しようとするアドレスをIPアドレスベースでフィルタリングできます。InetAddressFilterInetAddress を対象に判定する関数型インターフェースで、externalAddresses()internalAddresses()、CIDR指定、andNot() などの組み合わせが用意されています。

Spring Boot 4.1で追加されたInetAddressFilter

Spring Boot 4.1では、blocking系とreactive系のHTTP Clientに対して、InetAddressFilter を設定できるようになりました。Spring公式のリリースハイライトでも、reactive / blocking HTTP clients に InetAddressFilter を設定し、特定アドレスへの送信リクエストをブロックできると紹介されています。

代表的には、次のようなHTTP Clientで使うイメージです。

・RestClient
・RestTemplate
・WebClient
・HTTP Interface Client

Spring Boot 4.1のリリースでも、HTTP Client SSRF Mitigation with InetAddressFilter は主な新機能の1つとして挙げられています。

一番わかりやすい使い方は、外部IPアドレスだけを許可する設定です。

import org.springframework.boot.http.client.InetAddressFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class HttpClientSecurityConfig {

    @Bean
    public InetAddressFilter httpClientInetAddressFilter() {
        return InetAddressFilter.externalAddresses();
    }
}

これにより、Spring Bootが自動構成するHTTP Client Builderに対して、外部アドレスのみ許可するフィルタを適用できます。公式ドキュメントでも、InetAddressFilter@Bean として定義すると、auto-configured HTTP client builders 全体に適用できると説明されています。

ポイントは、RestClientの呼び出し側で毎回チェックを書くのではなく、HTTP Clientの設定としてまとめて守ることです。

RestClientでSSRF対策を入れる

まずは RestClient を使う例です。

build.gradle は通常のWebアプリであれば、まずは以下のような構成です。

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

外部URLの情報を取得するServiceを作ります。

package com.example.demo.preview;

import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class UrlPreviewService {

    private final RestClient restClient;

    public UrlPreviewService(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder
                .build();
    }

    public String fetch(String url) {
        return restClient.get()
                .uri(url)
                .retrieve()
                .body(String.class);
    }
}

このServiceは、指定されたURLへHTTP GETします。

Controllerは次のようにします。

package com.example.demo.preview;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class UrlPreviewController {

    private final UrlPreviewService urlPreviewService;

    UrlPreviewController(UrlPreviewService urlPreviewService) {
        this.urlPreviewService = urlPreviewService;
    }

    @GetMapping("/api/preview")
    String preview(@RequestParam String url) {
        return urlPreviewService.fetch(url);
    }
}

このままだと、次のようなリクエストが来た場合に内部向けURLへアクセスしてしまう可能性があります。

curl "http://localhost:8080/api/preview?url=http://localhost:8080/internal"

そこで、InetAddressFilter をBeanとして定義します。

package com.example.demo.config;

import org.springframework.boot.http.client.InetAddressFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class HttpClientSecurityConfig {

    @Bean
    public InetAddressFilter httpClientInetAddressFilter() {
        return InetAddressFilter.externalAddresses();
    }
}

InetAddressFilter.externalAddresses() は、外部の非プライベートアドレスを許可するフィルタです。Javadocでは、外部アドレスはroutableであり、multicastやspecial purposeではないアドレスとして説明されています。

この設定により、localhost やプライベートIPアドレスへのリクエストを防ぎやすくなります。

許可する範囲をカスタマイズする

すべての外部アドレスを許可するのではなく、「このネットワークだけ許可したい」という場合もあります。

その場合は、InetAddressFilter.of(...) を使えます。

@Bean
public InetAddressFilter httpClientInetAddressFilter() {
    return InetAddressFilter.of("203.0.113.0/24");
}

CIDRで範囲指定できます。InetAddressFilter.of(String...) は、完全なIPアドレスやCIDR表記のIPアドレスブロックを指定でき、IPv4とIPv6の両方に対応すると説明されています。

一部だけ除外したい場合は、andNot() を組み合わせます。

@Bean
public InetAddressFilter httpClientInetAddressFilter() {
    return InetAddressFilter
            .of("203.0.113.0/24")
            .andNot("203.0.113.10");
}

公式ドキュメントにも、InetAddressFilter.of("192.168.1.0/24").andNot("192.168.1.1", "192.168.1.10") のように、許可範囲から一部を除外する例が掲載されています。

実務では、以下のように考えると整理しやすいです。

外部URL取得機能:
  InetAddressFilter.externalAddresses()

社内APIだけ呼ぶ機能:
  InetAddressFilter.of("10.0.0.0/8") など

特定の外部APIだけ呼ぶ機能:
  可能ならURLやホストのallowlistも併用

危険な宛先を明示的に除外:
  andNot(...) を使う

ただし、IPアドレスだけで外部APIを管理すると、相手側のIP変更に弱くなることがあります。
そのため、IP制限だけでなく、呼び出し先URLのallowlist、認証、ネットワーク制御も組み合わせるのが現実的です。

WebClientでも考え方は同じ

Reactiveアプリで WebClient を使っている場合も、考え方は同じです。

package com.example.demo.preview;

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Service
public class ReactiveUrlPreviewService {

    private final WebClient webClient;

    public ReactiveUrlPreviewService(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.build();
    }

    public Mono<String> fetch(String url) {
        return webClient.get()
                .uri(url)
                .retrieve()
                .bodyToMono(String.class);
    }
}

InetAddressFilter のBeanを定義しておけば、Spring Bootが自動構成するHTTP Client Builderに適用できます。Spring Boot 4.1のリリースノートでは、blocking / reactive の両方のHTTP Clientに InetAddressFilter を設定できると説明されています。

ただし、注意点があります。

RestClient.create()WebClient.create() のように、自分で直接クライアントを作る場合、Spring Bootの自動構成が効かない可能性があります。

できるだけ、Spring Bootが用意するBuilderをDIして使いましょう。

public UrlPreviewService(RestClient.Builder restClientBuilder) {
    this.restClient = restClientBuilder.build();
}
public ReactiveUrlPreviewService(WebClient.Builder webClientBuilder) {
    this.webClient = webClientBuilder.build();
}

これなら、共通設定やフィルタを乗せやすくなります。

ブロック時のエラーハンドリング

InetAddressFilter によって宛先が拒否された場合、そのままだとアプリケーション内部の例外が500エラーとして返ることがあります。

Spring BootのAPIには、InetAddressFilter によってホストが使われなかった場合に投げられる例外として FilteredHostException が用意されています。Javadocでも、FilteredHostExceptionInetAddressFilter によってホストが使用されなかった場合の例外と説明されています。

ユーザーに返すレスポンスは、@RestControllerAdvice で整えるとよいです。

package com.example.demo.config;

import org.springframework.boot.http.client.FilteredHostException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class HttpClientExceptionHandler {

    @ExceptionHandler(FilteredHostException.class)
    public ProblemDetail handleFilteredHost(FilteredHostException ex) {
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
                HttpStatus.FORBIDDEN,
                "指定されたURLにはアクセスできません。"
        );
        problemDetail.setTitle("Blocked destination");
        return problemDetail;
    }
}

内部向けURLが指定された場合に、利用者へ分かりやすく403を返せます。

{
  "title": "Blocked destination",
  "status": 403,
  "detail": "指定されたURLにはアクセスできません。"
}

エラーメッセージに、ブロックされたホスト名や内部ネットワーク情報をそのまま出しすぎない点も大切です。
攻撃者に内部構成のヒントを与えないようにしましょう。

application.ymlで合わせて設定したいこと

SSRF対策では、宛先制限だけでなく、タイムアウトやリダイレクト設定も重要です。

spring:
  application:
    name: ssrf-sample

  http:
    clients:
      connect-timeout: 2s
      read-timeout: 3s
      redirects: dont-follow

Spring Boot公式ドキュメントでは、すべての自動構成HTTP Clientに共通設定を適用する方法として、spring.http.clients.connect-timeoutread-timeoutredirects の設定例が紹介されています。

特にリダイレクトは要注意です。

最初のURLが安全そうに見えても、リダイレクト先が内部IPアドレスになる可能性があります。

https://example.com/safe
  ↓ redirect
http://localhost:8080/internal

そのため、ユーザー指定URLを扱う機能では、まずはリダイレクトを自動追跡しない設定にするのが安全寄りです。

実務でのおすすめ構成

SSRF対策は、InetAddressFilter だけ入れれば完璧というものではありません。

実務では、複数の対策を重ねるのがおすすめです。

1. URL入力値をバリデーションする
2. http / https 以外のスキームを拒否する
3. 呼び出し先ホストのallowlistを持つ
4. InetAddressFilterで内部IPへの接続を防ぐ
5. リダイレクトを無制限に追わない
6. connect-timeout / read-timeoutを短めに設定する
7. エラー時に内部情報を返しすぎない
8. ログにURLやレスポンス本文を出しすぎない
9. ネットワークレベルでも不要な宛先へ出られないようにする

特に、ユーザーがURLを自由入力できる機能では、allowlist方式が強いです。

例えば、Webhook送信先を自由登録させる場合でも、業務上必要な送信先が限られているなら、許可ドメインを管理画面や設定ファイルで制御したほうが安全です。

app:
  allowed-webhook-hosts:
    - api.example.com
    - hooks.example.net

そして、アプリケーション側でホスト名のallowlistチェックを行い、そのうえで InetAddressFilter によるIPアドレス制限も行います。

private void validateAllowedHost(URI uri) {
    Set<String> allowedHosts = Set.of("api.example.com", "hooks.example.net");

    if (!allowedHosts.contains(uri.getHost())) {
        throw new IllegalArgumentException("host is not allowed");
    }
}

大事なのは、文字列チェックだけに頼らないことです。
アプリ側の業務ルールとしてのallowlistと、HTTP Client側のIPアドレスフィルタを組み合わせると守りが固くなります。

テスト観点

SSRF対策を入れたら、必ずテストしておきたいです。

最低限、次のようなケースを確認します。

許可したい:
・https://spring.io のような外部サイト
・業務上許可された外部API

拒否したい:
・http://localhost:8080
・http://127.0.0.1
・http://[::1]
・http://192.168.0.1
・http://10.0.0.1
・http://172.16.0.1
・http://169.254.169.254

InetAddressFilter.internalAddresses() は、loopback、IPv4のプライベートアドレス範囲、IPv6 Unique Local Addressesなどを内部アドレスとして扱うと説明されています。

単体テストでは、フィルタそのものの挙動も確認できます。

import org.junit.jupiter.api.Test;
import org.springframework.boot.http.client.InetAddressFilter;

import java.net.InetAddress;

import static org.assertj.core.api.Assertions.assertThat;

class InetAddressFilterTest {

    @Test
    void externalAddressesはlocalhostを許可しない() throws Exception {
        InetAddressFilter filter = InetAddressFilter.externalAddresses();

        InetAddress localhost = InetAddress.getByName("127.0.0.1");

        assertThat(filter.matches(localhost)).isFalse();
    }

    @Test
    void internalAddressesはプライベートIPを許可する() throws Exception {
        InetAddressFilter filter = InetAddressFilter.internalAddresses();

        InetAddress privateAddress = InetAddress.getByName("192.168.0.10");

        assertThat(filter.matches(privateAddress)).isTrue();
    }
}

HTTP Clientの結合テストでは、ブロック対象URLを指定したときに FilteredHostException が発生すること、またはControllerとして403を返すことを確認するとよいです。

よくある注意点

まず、InetAddressFilter はHTTP Clientの宛先制御です。
入力値チェック、認証、認可、ネットワーク制御の代わりではありません。

また、自動構成されたBuilderを使っていないHTTP Clientには、設定が乗らない可能性があります。

// 共通設定が乗らない可能性がある
RestClient restClient = RestClient.create();

できるだけ以下のようにします。

// Spring Bootの自動構成を活かしやすい
public MyService(RestClient.Builder builder) {
    this.restClient = builder.build();
}

さらに、SSRF対策ではリダイレクトも見落としがちです。
最初のアクセス先だけでなく、リダイレクト先も危険になり得るため、ユーザー入力URLを扱う機能では redirects: dont-follow を検討しましょう。

最後に、内部APIを呼ぶためのHTTP Clientと、ユーザー指定URLを取得するHTTP Clientは分けたほうが安全です。

社内API呼び出し用 RestClient:
  固定baseUrl
  認証あり
  内部ネットワーク向け

ユーザー指定URL取得用 RestClient:
  外部アドレスのみ許可
  タイムアウト短め
  リダイレクト制限
  レスポンスサイズ制限

同じ RestClient を何でも屋にすると、設定ミスが起きやすくなります。

まとめ

Spring Boot 4.1の InetAddressFilter を使うと、RestClientWebClient などのHTTP Clientに対して、接続してよい宛先IPアドレスを制限できます。

特に、ユーザーが指定したURLへサーバー側からアクセスする機能では、SSRF対策としてかなり重要です。

この記事のポイントは以下です。

・SSRFはサーバーに意図しないURLへアクセスさせる攻撃
・ユーザー入力URLをそのままRestClient/WebClientに渡すのは危険
・URL文字列チェックだけでは不十分
・Spring Boot 4.1ではInetAddressFilterでHTTP Clientの宛先を制限できる
・externalAddresses() を使うと外部アドレスのみ許可しやすい
・@Beanで定義すると自動構成HTTP Client Builderへ適用できる
・FilteredHostExceptionは@RestControllerAdviceで扱うと親切
・timeout、redirect制御、allowlist、ネットワーク制御も併用する

SSRF対策は、セキュリティ専門の人だけが考えるものではありません。

URLプレビュー、Webhook、外部ファイル取り込み、外部API連携など、Spring Bootの業務アプリでも普通に出てくるテーマです。

Spring Boot 4.1の InetAddressFilter は、こうした機能を安全に作るための新しい選択肢になります。
これからSpring Boot 4.1以降へ移行するなら、外部HTTP通信をしている箇所を一度棚卸しして、SSRF対策を見直しておくのがおすすめです。

是非フォローしてください

最新の情報をお伝えします

類似投稿