5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OutSystemsでクライアントの「正しい」送信元IPアドレスを特定したい

5
Posted at

はじめに

こんにちは、樋口と申します。
NTT東日本 デジタル革新本部 デジタルデザイン部に所属しており、現在はOutSystemsのCoE(Center of Excellence)活動の一環として、開発標準化や共通機能の開発などに取り組んでいます。

これは OutSystems Advent Calendar 2025 の17日目の記事です。

監査やセキュリティの観点で、アプリケーションにアクセスしてきたクライアントの送信元IPアドレスを取得し、ログに残したいケースはよくあると思います。

OutSystems 11(以下、O11)のService Centerで見られる標準ログ(General LogやRequest Log)にはクライアントの送信元IPを記録する項目があるため、最初は「ここを見ればわかるだろう」と考えていました。しかし、実際に確認してみると、期待したクライアントのIPアドレスが記録されていないことが分かりました。

私自身もこの挙動に悩み検証を行いました。皆さんが同じ落とし穴にはまらないよう、本記事でその知見を共有します。

時間がない人のためのサマリ

  1. Service Centerの標準ログは、構成によってはLBのIPや、偽装可能なIPを記録してしまう可能性がある。
  2. X-Forwarded-Forの仕様を理解し、信頼できるクライアントのIPアドレスを取得する必要がある。
  3. GetIP アクションは環境により挙動が異なるため可能性があるため、必ず検証すべき。

環境情報

  • OutSystems 11(OutSystems Cloud)
  • Service Studio Version: 11.55.50

1. 標準ログの仕様とX-Forwarded-Forの仕組み

まずは標準ログに出力されているクライアントの送信元IPアドレスの情報を見ていきます。 例えば、Traditional Web Requests Logの Client_IP Attribute について、公式ドキュメントには以下のような仕様が記載されています。

Client_IP
IP of the end user as identified by the platform. This the client IP, if there is a X-FORWARD it appears with the first (Client) and last IP (last proxy IP).

(訳:プラットフォームによって識別されたエンドユーザーのIP。これはクライアントIPですが、X-FORWARDがある場合は、最初(クライアント)と最後(最後のプロキシIP)のIPが表示されます。)

出典: Log data reference - OutSystems

何やら難しいことが書いてありますね、、、実際に出力されている値を見てみましょう。
image.png
※一部マスクしています。

IPアドレスに加えて、 X-Forwarded-For という値が記録されていることが分かるかと思います。
このログの意味を理解するために、まず X-Forwarded-For ヘッダーの仕組みを整理しましょう。

※他のログテーブルにも同様にクライアントのIPアドレスを記録する項目があります。ドキュメントには詳細な仕様の記載はありませんが、検証した限りでは同様の仕様になっていました。

X-Forwarded-For (XFF) とは

プロキシやロードバランサーを経由した際に、接続元のIPアドレスを記録するためのHTTPヘッダーです。

X-Forwarded-For (XFF) ヘッダーは、 HTTP プロキシーサーバーを通過してウェブサーバーへ接続したクライアントの、送信元 IP アドレスを特定するために事実上の標準となっているヘッダーです。

(出典: MDN Web Docs: X-Forwarded-For)

値はカンマ区切りで、クライアントのIPが先頭(左側)に追加され、経由したプロキシが順に後ろ(右側)に追加されます。

X-Forwarded-For: <client>, <proxy1>, <proxy2>
  • <client>: クライアントの IP アドレス。
  • <proxy1>, <proxy2>: リクエストが通過した各プロキシの IP アドレス。

ちなみに、XFFはあくまで「事実上の標準(デファクトスタンダード)」です。RFCでは Forwarded ヘッダーが標準とされていますが、歴史的な背景もあり、未だにXFFヘッダーが使い続けられています。

OutSystems Cloud での挙動

OutSystems Cloudのネットワーク構成は、Webサーバ(フロントエンドサーバ)の前段にLBが配置されています。

この構成では、Webサーバーが直接通信するのはLBであるため、通信レベルでの接続元IP(Remote Address)は常にロードバランサーのアドレスになります。そのため、XFFヘッダーを頼りにクライアントIPを特定する必要があります。

LBがリクエストを受け取ると、「接続元のIPアドレス」をXFFヘッダーの末尾に追記して、フロントエンドサーバに転送します。

フロントエンドサーバから見ると、Remote Addressは常にLBのアドレス(10.0.0.99)になりますが、XFFヘッダーからLBにアクセスしてきたクライアントIPの情報がわかります。


2. セキュリティ上の懸念:IP偽装のリスク

ここで重要なのが、「X-Forwarded-Forヘッダーの値を無条件に信用してはいけない」ということです。 なぜなら、XFFヘッダーは単なるHTTPリクエストヘッダーであり、クライアント側で自由に設定(偽装)が可能だからです。

MDNの「セキュリティとプライバシー」セクションでも以下のように警告されています。

セキュリティーとプライバシーの懸念
X-Forwarded-For ヘッダーはクライアントとサーバーの間に信頼できるリバースプロキシー(例: ロードバランサーのような)が存在しない場合は信頼できません。もし、クライアントと全てのプロキシーが悪意がなく正しく振る舞う場合、ヘッダー内の IP アドレスのリストは ディレクティブ 項で説明された意味を持ちます。しかし、クライアントもしくはプロキシーに悪意がある、または設定ミスがある場合、ヘッダーの一部(もしくは全て)がなりすましの可能性があります。(もしくは、リストではないか IP アドレスを全く含まないか)

(出典: MDN Web Docs: X-Forwarded-For)

つまり、XFFヘッダーのリストに含まれるIPのうち、信頼できるプロキシ(自身が管理するLBなど)が正しく追加したIPアドレスだけが信頼に足る情報だということです。

OutSystems Cloudの構成において、唯一信頼できるのは「フロントエンドサーバの直前にいるLBが書き込んだ値」、すなわち「リストの最後(末尾)のIPアドレス」のみです。

標準ログの問題点

ここで標準ログの話に戻ります。

もうお気づきかもしれませんが、前述の通りOutSystemsのログ仕様は「X-Forwarded-Forの最初の値を記録する」となっているため、偽装されたヘッダーが送られてくると、そのまま偽装IPがログに記録されてしまいます。

以下に悪意のある攻撃者がIPスプーフィング(なりすまし)をしてきた場合のフローを図示します。

また、先頭以外のIPアドレスも記録されていないため、例えばクライアント側でZscalerなどのクラウドプロキシを利用していて、XFFヘッダーが既に追加されていても、その情報が取得できなかったりします。

そのため、私たちのチームでは、標準ログの代わりに自前のロジックで正しいIPアドレスを取得し、ログに明示的に出力する方針にしました。


3. 正しいIPアドレスを取得する方法

ここからは具体的な実装方法をご紹介します。

方法1:GetIPアクションを使う(要検証)

OutSystems標準の HTTPRequestHandler エクステンションに含まれる GetIP アクションを利用する方法です。
GetIP - OutSystems 11 Documentation

一見これが正解に見えますが、検証してみたところ、環境によって挙動(返却される値)が異なる場合があるようです。具体的には、社内で使っているOutSystems Cloud環境とPersonal Environmentで出力値の挙動が違いました。

検証項目 OutSystems Cloud (Enterprise) Personal Environment (PE)
期待するGetIPの戻り値(正しいクライアントIP) X-Forwarded-Forの末尾 X-Forwarded-Forの末尾
確認されたGetIPの返り値 X-Forwarded-Forの末尾 Remote Address (グローバルIP)
クライアントのIPアドレス
FE Serverが見る
Remote Address
LBのプライベートIP (恐らく)フロントエンドサーバの手前にいるCDNのグローバルアドレス

考察:
本来はどちらの環境でも「XFFヘッダーの末尾(=クライアントIP)」を返してほしいところですが、結果は上記の通りでした。

OutSystems Cloudでは、LBの存在を正しく考慮してヘッダー解析が行われているようです。一方、Personal Environmentではフロントエンドサーバが認識している Remote Address がそのまま返されました。

「なぜPersonal EnvironmentではRemote AddressがグローバルIPになるのか」の考察 Personal Environmentはマルチテナント型の特殊な環境のため、標準的なOutSystems Cloudとは異なるサーバ/ネットワーク構成が起因しているのではないかと考えられます。

参考:Personal Environment Hosting Infrastructure Under the Hood

GetIP は内部ロジックがブラックボックス(ビルトインのアクションであるためソースコード非公開)であり、ドキュメント上も詳細な判定ロジックが明記されていません。「Remote AddressがプライベートIP帯域の場合のみXFFを参照し、グローバルIPの場合はRemote Addressを優先する」といったロジックが含まれている可能性も考えられますが、詳細は不明です。

そのため、使用する際は必ずご自身の環境でどの値が返るか検証することを強くお勧めします。

方法2:自前でロジックを実装する

GetIP で要件が満たせない場合や、多段プロキシやロードバランサーを使っている構成の場合は、自分でヘッダーを解析するのが確実です。

実装方針

HTTPRequestHandlerGetRequestHeader アクションを使用し、XFFヘッダーの値を取得してパースします。

実装手順

  1. 依存関係の追加
    Service Studioの「Manage Dependencies」を開き、以下の要素を利用可能にします。

    • HTTPRequestHandler: GetRequestHeader アクション
    • Text: String_Split アクション
  2. Actionの作成
    クライアントIPを取得するためのServer Actionを新規作成し、戻り値としてIPアドレスを返すText型のOutput Parameterを設定します。
    image.png

  3. ヘッダーの取得
    作成したフロー内で GetRequestHeader アクションを配置し、引数(HeaderName)に "X-Forwarded-For" を指定します。
    image.png

  4. 文字列の分割
    取得したヘッダー値を String_Split アクションに渡し、Delimitersに "," (カンマ) を指定してリスト化します。
    image.png

  5. 対象IPの抽出
    List.Last を使用して分割したリストの 「最後の要素(Last)」 を取得し、Output Parameterに割り当てます。
    image.png

    実装のポイント
    上記はHeaderが必ず取得できる前提のフローですが、実際にはヘッダーが存在しない(空である)ケースや、リストが空になるケースも考慮する必要があります。
    取得できなかった場合は GetIP の値を採用したり、デフォルト値を設定するなど、フォールバックのロジックを適宜検討してください。

⚠️ 多段でプロキシやロードバランサーを利用している場合
例えば、LBの前段にさらにCDNを利用している場合、XFFの末尾には「CDNのエッジサーバのIP」が追加されることがあります。その場合、「後ろから2番目」が正しいクライアントIPになるケースもあります。
クライアントIPとして取得する位置は、自社のネットワーク構成図(信頼できるプロキシの段数)と照らし合わせて決定する必要があることに注意してください。

ODC (OutSystems Developer Cloud) の場合

現在、ODCでは GetIP アクションが提供されていないため、上記のように自前でロジックを実装する必要があるようです。GetRequestHeader アクションは提供されているため、同様の実装方針で実現できます。


4. ログ出力の実装例

作成したIP取得ロジック(Server Action)を利用して、画面アクセス時にクライアントIPを監査ログとして出力する実装例を紹介します。
今回はReactive Web Appの画面で、Data Actionを用いてサーバーサイドでログ出力を行うパターンです。

実装手順

  1. Data Actionの作成
    対象の画面(Screen)で新規のData Actionを作成します。
    ※Client ActionではなくData Actionを利用することで、確実にサーバーサイドで処理を実行させます。

  2. IP取得アクションの呼び出し
    作成したData Actionのフロー内で、先ほど作成したIP取得用のServer Actionを配置します。

  3. ログの出力
    Systemモジュールの LogMessage アクションを配置し、General Logに情報を出力します。

    • Message: 取得したIPアドレスを記載したログメッセージ
    • ModuleName: GetEntryEspaceName() (ユーザのリクエストの起点となるモジュール名 = この画面があるモジュールの名前)

image.png

最後にData ActionはOutput Parameterを少なくとも1つ設定しないとエラーが出てしまうため、適当に設定しておきます。(取得したクライアントIPを出力しておくのが良いと思います。)


まとめ

本記事のポイントは以下の3点です。

  • 標準ログは偽装可能: OutSystemsの標準ログはXFFの先頭を記録する仕様のため、IP偽装のリスクがあります。
  • 正しいIPは自前で取得: 信頼できるLBが付与したXFFの「末尾(または適切な位置)」を特定し、明示的にログ出力しましょう。
  • 環境検証の徹底: GetIP の挙動やCDNの有無など、環境により正解が異なるため、必ず実機での検証が必要です。

是非参考にしていただき、セキュアなアプリケーション運用に繋げて頂けたら幸いです。


※記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?