オープンリダイレクト

オープンリダイレクト

説明

オープンリダイレクトは、十分な検証をせずにユーザー入力から HTTP の移動先を決める場合に生じます。攻撃者は、信頼されているアプリケーションのリダイレクトを通じて、フィッシングやマルウェア配布などのサイトへユーザーを誘導できます。クライアントが操作できるパラメーター、Host ヘッダー、URL の一部を http.Redirect の外部移動先の決定に使うと危険です。

想定される影響

  • 信頼されるドメインを経由することで、フィッシングリンクを信用させやすくなります。
  • 悪意のあるサイトへの移動により、マルウェアやブラウザーの脆弱性を悪用した攻撃を受けるおそれがあります。
  • リダイレクトの悪用によって、元のサービスへの信頼が損なわれる可能性があります。

対処方法

  • http.Redirect へ渡す前に、特に scheme(http、https)、host、port が許可された値か確認してください。
  • 外部へのリダイレクトが必要な場合は、許可リストにあるドメインだけを認めてください。
  • できるだけ /path/... のような相対パスに限定し、http://、https:// で始まる絶対 URL や //host を拒否してください。
  • クライアントが操作できる req.Host をそのまま使わず、サーバーで設定した固定ホストを使ってください。
  • url.Parse や ResolveReference で URL を解析・解決し、最終的な host と scheme が想定どおりか確認してください。

例

変更前

go
package main

import (
    "net/http"
)

// 変更前: ユーザー指定の Host と URL パスをそのまま使用
func redirectHandler(w http.ResponseWriter, r *http.Request) {
    // クライアントは r.Host と r.URL.Path を操作できる
    target := "https://" + r.Host + r.URL.Path
    if len(r.URL.RawQuery) > 0 {
        target += "?" + r.URL.RawQuery
    }

    // 入力から組み立てた URL に直接リダイレクトするため危険
    http.Redirect(w, r, target, http.StatusTemporaryRedirect)
}

func main() {
    http.HandleFunc("/go", redirectHandler)
    http.ListenAndServe(":8080", nil)
}

変更後

go
package main

import (
    "net/http"
    "net/url"
    "strings"
)

var allowedHosts = map[string]bool{
    "example.com":      true,
    "www.example.com":  true,
}

// 変更後: ドメインを固定し、相対パスだけを許可
func safeRedirectHandler(w http.ResponseWriter, r *http.Request) {
    // 1) サーバーで決めた固定ホストを使用
    const baseURL = "https://www.example.com"

    // 2) 移動先のパスをパラメーターで受け取る
    //    例: /go?next=/products/123
    next := r.URL.Query().Get("next")
    if next == "" {
        next = "/" // 既定のパス
    }

    // 3) 以下の絶対 URL とプロトコル相対 URL の接頭辞を拒否
    if strings.HasPrefix(next, "http://") ||
        strings.HasPrefix(next, "https://") ||
        strings.HasPrefix(next, "//") {
        http.Error(w, "invalid redirect url", http.StatusBadRequest)
        return
    }

    // 4) パスの先頭を '/' にする
    if !strings.HasPrefix(next, "/") {
        next = "/" + next
    }

    // 最終的なリダイレクト URL を作成
    u, err := url.Parse(baseURL)
    if err != nil {
        http.Error(w, "server error", http.StatusInternalServerError)
        return
    }

    u.Path = next

    // 5) 追加の対策として最終ホストを許可リストで確認
    if !allowedHosts[u.Host] {
        http.Error(w, "invalid redirect host", http.StatusBadRequest)
        return
    }

    http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect)
}

func main() {
    http.HandleFunc("/go", safeRedirectHandler)
    http.ListenAndServe(":8080", nil)
}

説明:

  • 変更前: クライアントが操作できる r.Host、r.URL.Path、r.URL.RawQuery を連結して移動先 URL を作っています。
  • この例で Host ヘッダーを evil.com にすると、その外部ホストが移動先になります。
  • サーバーは移動先を検証せず、組み立てた値を http.Redirect へ渡します。
  • その結果、アプリケーションを信頼するユーザーがフィッシングやマルウェア配布サイトへ誘導されるおそれがあります。
  • 変更後: 次のように移動先を制御します。
  • サーバー側の baseURL を固定し、r.Host を信頼しません。
  • ユーザーが指定できるのはパス next だけとし、先頭が / になるようにします。
  • http://、https://、// で始まる入力を拒否し、許可した入力も url.URL.Path に設定して、固定ホスト上のパスとして扱います。
  • 最後に host を許可リストと照合し、意図しない外部移動を防ぐ追加の確認を行います。

ユーザーが操作できる範囲を限定することで、任意の外部サイトへのリダイレクトを防ぎます。

参考資料