[アップデート]AWS WAFがURIフラグメントを検査条件に使えるようになりました

[アップデート]AWS WAFがURIフラグメントを検査条件に使えるようになりました

Clock Icon2025.03.28

初めに

先日AWS WAFの検査条件にURIフラグメントが使えるようになったとの告知がありました。

https://aws.amazon.com/about-aws/whats-new/2025/03/aws-waf-uri-fragment-field-matching/

URLフラグメントはhttps:/example.com/index.html#homeにおけるhome部分のように#文字以降の部分になり、これを判定に用いることができるようになったようです。

https://docs.aws.amazon.com/waf/latest/developerguide/logging-fields.html
fragment
The part of a URL that follows the "#" symbol, providing additional information about the resource, for example, #section2.

これを用いることでWebサイト側が意図していないリンクでのジャンプを制限できる...と思っていたのですがもしかすると異常なアクセスを弾く目的かもしれません。

今回の検証で一応使えることは確認したのですが普通使わないような経路になっている気もするので、本記事は検証と一応このケースではマッチするよ!の参考としていただければと思います。

https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-fields-list.html#waf-rule-statement-request-component-uri-fragment
Uri Fragment inspection is available only for CloudFront distributions and Application Load Balancers.

なおURIフラグメントを検査条件に活用できるのはCloudFrontおよびALBに結びつけた場合のみとなります。

通常URLフラグメントはサーバへのリクエスト内に含まれない

さて、まずURLフラグメント(URL Fragment)とはなんぞや?ですが、前述の通りURIでは#以降の文字列部分を示し、これはRFC3986(特にセクション3.5)で定義されています。合わせてRFC7230も読むと理解が深まります。

該当RFC内では二次リソース(Secondary resource)の指定として定義されており、id属性名を指定することでページ遷移後に特定の箇所への遷移に利用される印象があるあの項目です。

今回あれ?と思ったのは、URIフラグメントは通常HTTPリクエストからは除外される値となっているということです。

改めて定義を確認してみましたがHTTP/1.1の定義であるRFC7230のセクション5.1ではこの値はターゲットURIから除外する値となっていますし、HTTP/1.1およびHTTP/2の定義となるRFC 9110でも含まれる値ではないとしてます。また、わかりやすいところとしてセクション17.11(リダクレイト後のURIフラグメントの取扱)でもあくまでこの値はクライアント側の処理ためでありサーバ側に送信されない値とされています。

https://datatracker.ietf.org/doc/html/rfc7230#section-5.1
identifier for the "target resource", which a user agent would resolve to its absolute form in order to obtain the "target URI". The target URI excludes the reference's fragment component, if any, since fragment identifiers are reserved for client-side processing ([RFC3986], Section 3.5).

https://datatracker.ietf.org/doc/html/rfc9110#section-7.1
A URI reference is resolved to its absolute form in order to obtain the "target URI". The target URI excludes the reference's fragment component, if any, since fragment identifiers are reserved for client-side processing ([URI], Section 3.5).

https://datatracker.ietf.org/doc/html/rfc9110#section-17.11
Although fragment identifiers used within URI references are not sent in requests, implementers ought to be aware that they will be visible to the user agent and any extensions or scripts running as a result of the response.

ついでに今回のリリース記事にも"最初に"ではありますがリクエストが走らない旨が記載されています。

URI fragment is often used to identify specific sections or anchors within a web page and is not typically sent to the server during the initial request.

...どうやって判定処理するのでしょうか。

いろいろ検証

とりあえず触ってみればわかるかもしれないのでいろいろやってみます。

デフォルトアクションをAllowとして、#homeがついた時のみ拒否(Deny)とする設定にしておきます。

deny-access-when-uri-fragment-is-home

https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-fields-list.html#waf-rule-statement-request-component-uri-fragment
Rule statement requirements
You must provide a fallback behavior for this rule statement. The fallback behavior is the match status that you want AWS WAF to assign to the web request if URI is missing the fragment or associated service is not Application Load Balancer or CloudFront. If you choose to match, AWS WAF treats the request as matching the rule statement and applies the rule action to the request. If you choose to not match, AWS WAF treats the request as not matching the rule statement.

また、値がなかった場合(URIフラグメントが未定義)の場合のアクションも定義する必要がありますが、NOT MATCH(=今回の場合はデフォルトアクションになるので許可)としておきます。

ブラウザからのリクエスト

とりあえずシンプルにリクエストを投げてみましょう。
以下の様なHTMLを用意しこれをS3においてブラウザ(Firefox)でアクセスしてみます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <div>
        <a href="#hoge">#hoge</a>
        <a href="#home">#home</a>
    </div>
</body>
</html>

ある意味想定通りですが#homeが含まれているのにも関わらず正常に表示され、またリクエスト内にもhomeが含まれてそうな痕跡はありません。一応リンクをクリックしますが当然クライアント内の挙動で収まるようで特にリクエストも発生しません(一応遷移時にログが消滅しないよう「永続ログ」をONにしてます)。

WAF専用の処理用のコンテンツが渡されているわけではなく、Cookie経由で何か付与されるのかな?と思ったらこちらも空でした。

uri-fragment-is-home-browser-access

uri-fragment-no-cookie

この際のWAFログは以下の様になります。本来であればfragmentに値が入っている想定なのですが空文字...飛んでないので当然ではあるのですがWAF側に認識すらされていない様です。

{
    "timestamp": 1743088503442,
    "formatVersion": 1,
    "webaclId": "arn:aws:wafv2:us-east-1:xxxxx:global/webacl/flagment-test-waf/xxxxx",
    "terminatingRuleId": "Default_Action",
    "terminatingRuleType": "REGULAR",
    "action": "ALLOW",
    "terminatingRuleMatchDetails": [],
    "httpSourceName": "CF",
    "httpSourceId": "xxx",
    "ruleGroupList": [],
    "rateBasedRuleList": [],
    "nonTerminatingMatchingRules": [],
    "requestHeadersInserted": null,
    "responseCodeSent": null,
    "httpRequest": {
        "clientIp": "xxx.xxx.xxx.xxx",
        "country": "JP",
        "headers": [
            {
                "name": "host",
                "value": "xxxxx.cloudfront.net"
            },
            {
                "name": "user-agent",
                "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:136.0) Gecko/20100101 Firefox/136.0"
            },
            {
                "name": "accept",
                "value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
            },
            {
                "name": "accept-language",
                "value": "ja,en-US;q=0.7,en;q=0.3"
            },
            {
                "name": "accept-encoding",
                "value": "gzip, deflate, br, zstd"
            },
            {
                "name": "upgrade-insecure-requests",
                "value": "1"
            },
            {
                "name": "sec-fetch-dest",
                "value": "document"
            },
            {
                "name": "sec-fetch-mode",
                "value": "navigate"
            },
            {
                "name": "sec-fetch-site",
                "value": "cross-site"
            },
            {
                "name": "priority",
                "value": "u=0, i"
            },
            {
                "name": "pragma",
                "value": "no-cache"
            },
            {
                "name": "cache-control",
                "value": "no-cache"
            },
            {
                "name": "te",
                "value": "trailers"
            }
        ],
        "uri": "/index-nowaf.html",
        "args": "",
        "httpVersion": "HTTP/2.0",
        "httpMethod": "GET",
        "requestId": "xxxx",
        "fragment": "",
        "scheme": "https",
        "host": "xxxxx.cloudfront.net"
    },
    "ja3Fingerprint": "0e76c7e9d06fa0e211b1827687dd8f43",
    "ja4Fingerprint": "t13d1717h2_5b57614c22b0_e6dcd7ae0a9e"
}

一応値がなかった場合の処理をMATCH(今回の場合BLOCKになる)にしてみましたが、これは正常にブロックされたのでURIフラグメントは認識されていないという想定で間違いなさそうです。

うーん、Firefox特有の何かが悪さしてるのでしょうか。

cURLでリクエスト

別のクライアントという意味でcURLも投げてみましたが変わらず反応せずという形になりました。

% curl https://xxxxxx.cloudfront.net/index-nowaf.html#home --verbose
*   Trying xxx.xxx.xxx.xxx:443...
* Connected to xxxxxx.cloudfront.net (xxx.xxx.xxx.xxx) port 443 (#0)
* ALPN: offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES128-GCM-SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=*.cloudfront.net
*  start date: Jul 30 00:00:00 2024 GMT
*  expire date: Jul  3 23:59:59 2025 GMT
*  subjectAltName: host "xxxxxx.cloudfront.net" matched cert's "*.cloudfront.net"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M01
*  SSL certificate verify ok.
* using HTTP/2
* h2 [:method: GET]
* h2 [:scheme: https]
* h2 [:authority: xxxxxx.cloudfront.net]
* h2 [:path: /index-nowaf.html]
* h2 [user-agent: curl/8.1.2]
* h2 [accept: */*]
* Using Stream ID: 1 (easy handle 0x13c011400)
> GET /index-nowaf.html HTTP/2
> Host: xxxxxx.cloudfront.net
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/2 200
< content-type: text/html
< content-length: 187
< date: Thu, 27 Mar 2025 15:37:21 GMT
< last-modified: Thu, 27 Mar 2025 15:13:47 GMT
< etag: "3e649baa032a14ec76b4503902e3a1c1"
< x-amz-server-side-encryption: AES256
< accept-ranges: bytes
< server: AmazonS3
< x-cache: Miss from cloudfront
< via: 1.1 xxxxxx.cloudfront.net (CloudFront)
< x-amz-cf-pop: KIX56-C1
< x-amz-cf-id: xxxxxx
<
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <div>
        <a href="#hoge">#hoge</a>
        <a href="#home">#home</a>
    </div>
</body>
</html>
* Connection #0 to host xxxxxx.cloudfront.net left intact

JavaScript統合を利用したリクエスト

サーバ側に飛ばないのであればブラウザ側で処理させられそうな場所を当たってみます。

AWS WAFにはJavaScript統合と呼ばれる機能が用意されています。

https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/waf-javascript-api.html

いわゆるSPA向けアクセスへの提供として所定のスクリプトを埋め込むことでAWSの提供するfetch()のラッパースクリプトが利用することが可能となっております。

その一環でCAPTCHA統合を利用したのが以下の記事です。

https://dev.classmethod.jp/articles/aws-waf-captcha-not-required-redirect-by-captcha-integration/

現時点では本機能に対応していある旨が英語ページを含め見当たららないものの、現状の発想内ではこれが唯一クライアント側に干渉できるAWS WAF要素なので念のため試してみます。

まぁ当然ですが機能的にサポートしてないのでこれもダメ(200 OK)ですね。

javascript-integration-fetch

hyperでリクエスト

ここでのhyperはRustのHTTP接続用のライブラリとなります。

https://docs.rs/hyper/latest/hyper/

hyperはRustにおけるHTTP接続用のライブラリの一種であり、短い記法でサクッとリクエストを投げるというより低レイヤーを触って自由をきかせられるライブラリとなります。

以前一部S3の検証の際一部のURIの標準仕様を無視してアクセスするために利用しました。

https://dev.classmethod.jp/articles/s3-download-failed-only-management-console/

これであれば比較的クライアント個別の仕様を受けづらいと思いこちらでアクセスしてみました。

以前のコードを使いまわしたかったのでちょっとバージョンが古いのですが以下のコードで実行しています(最新だとhyper::Clientがなくなってて確認に時間がかかりそうでした)。

Cargo.toml
[dependencies]
hyper = { version = "0.14", features = ["full"] }
hyper-tls = "0.5"
tokio = { version = "1", features = ["full"] }
main.rs
use hyper_tls::HttpsConnector;
use hyper::{Client, Uri};

#[tokio::main]
async fn main() {
    let url = "https://xxxx.cloudfront.net/index.html#home";
    let https = HttpsConnector::new();
    let client = Client::builder().build::<_, hyper::Body>(https);

    let res = client.get(Uri::from_static(url));
    println!("{:?}", res.await.unwrap());
}

うーんこれも許可されてしまうのでURIフラグメント判定に乗っていない様です(ログにも記載なし)。

% cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.20s
     Running `target/debug/rust-base`
Response { status: 200, version: HTTP/1.1, headers: {"content-type": "text/html", "content-length": "623", "connection": "keep-alive", "date": "Thu, 27 Mar 2025 16:03:21 GMT", "last-modified": "Thu, 27 Mar 2025 10:38:36 GMT", "etag": "\"f5ecfb0a8ef5abd5e217fc442d0eff04\"", "x-amz-server-side-encryption": "AES256", "accept-ranges": "bytes", "server": "AmazonS3", "x-cache": "Miss from cloudfront", "via": "1.1 xxxxx.cloudfront.net (CloudFront)", "x-amz-cf-pop": "KIX56-C1", "x-amz-cf-id": "xxxxx=="}, body: Body(Streaming) }

opensslコマンドによる接続(反応した)

ここまでくると何かが間違ってる気もしますがoepnsslコマンドを使って生のHTTPでCloudFrontと対話してみましょう(今回はHTTPSで通信しているためover opensslとしていますがHTTPであればtelnet等でも可能です)。

HTTP/2はちょっと面倒なのでHTTP/1.1で通信します。

GET /index.html#home HTTP/1.1
Host: xxxx.cloudfront.net

おや、403 Forbiddenで拒否されました。

% openssl s_client -connect xxxxx.cloudfront.net:443
...
GET /index.html#home HTTP/1.1
Host: xxxx.cloudfront.net

HTTP/1.1 403 Forbidden
Server: CloudFront
Date: Thu, 27 Mar 2025 16:15:55 GMT
Content-Type: text/html
Content-Length: 919
Connection: keep-alive
X-Cache: Error from cloudfront
Via: 1.1 xxxx.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: KIX56-C1
X-Amz-Cf-Id: xxxxxx-IQ==

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD><BODY>
<H1>403 ERROR</H1>
<H2>The request could not be satisfied.</H2>
<HR noshade size="1px">
Request blocked.
We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
<BR clear="all">
If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
<BR clear="all">
<HR noshade size="1px">
<PRE>
Generated by cloudfront (CloudFront)
Request ID: xxxxxx==
</PRE>
<ADDRESS>
</ADDRESS>
</BODY></HTML>

...低めのレイヤーを触っているので一応URIフラグメントがないパターンも見てみましたが成功(200 OK)となるのでうまく判定されていそうです。

GET /index.html HTTP/1.1
Host: xxxxx.cloudfront.net

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 623
Connection: keep-alive
Date: Thu, 27 Mar 2025 16:17:26 GMT
Last-Modified: Thu, 27 Mar 2025 10:38:36 GMT
ETag: "f5ecfb0a8ef5abd5e217fc442d0eff04"
x-amz-server-side-encryption: AES256
Accept-Ranges: bytes
Server: AmazonS3
X-Cache: Miss from cloudfront
Via: 1.1 xxxxx.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: KIX56-C1
X-Amz-Cf-Id: xxxxx==

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <script type="text/javascript" src="https://xxxxx.edge.captcha-sdk.awswaf.com/xxxxx/jsapi.js" defer></script>
</head>
<body>
    <div>
        <form action="login.html" method="post">
            <p><label>ユーザ名</label> <input id="username" name="password" /></p>
            <p><label>パスワード</label> <input type="password" id="password" name="password" /></p>
            <button type="submit">送信</button>
        </form>
        <a href="#hoge">#hoge</a>
        <a href="#home">#home</a>
    </div>
</body>
</html>

ログを見てみましたが確かにfragment: homeが入っておりうまく取れたのでこれでいいんでしょうか。

{
    "timestamp": 1743092230474,
    "formatVersion": 1,
    "webaclId": "arn:aws:wafv2:us-east-1:xxxxx:global/webacl/flagment-test-waf/xxxxx",
    "terminatingRuleId": "fragment-test",
    "terminatingRuleType": "REGULAR",
    "action": "BLOCK",
    "terminatingRuleMatchDetails": [],
    "httpSourceName": "CF",
    "httpSourceId": "xxxxx",
    "ruleGroupList": [],
    "rateBasedRuleList": [],
    "nonTerminatingMatchingRules": [],
    "requestHeadersInserted": null,
    "responseCodeSent": null,
    "httpRequest": {
        "clientIp": "xxx.xxx.xxx.xxx",
        "country": "JP",
        "headers": [
            {
                "name": "Host",
                "value": "xxxxx.cloudfront.net"
            }
        ],
        "uri": "/index.html#home",
        "args": "",
        "httpVersion": "HTTP/1.1",
        "httpMethod": "GET",
        "requestId": "-xxxxx==",
        "fragment": "home",
        "scheme": "https",
        "host": "xxxxx.cloudfront.net"
    },
    "ja3Fingerprint": "caf4ec11cce2d9dba8e2575080fc4dfb",
    "ja4Fingerprint": "t13d301100_1d37bd780c83_ef4b9b248d72"
}

HTTP/2とHTTP/1.1の違いが悪さしてる?と思ってcurlでやってみましたが相変わらずこちらは条件にマッチしないのでURIフラグメントがサーバ側に飛びさえすれば反応しそうです。

% curl  -I https://xxxxx.cloudfront.net/index.html#home --http1.1
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 623
Connection: keep-alive
Date: Thu, 27 Mar 2025 16:37:38 GMT
Last-Modified: Thu, 27 Mar 2025 10:38:36 GMT
ETag: "f5ecfb0a8ef5abd5e217fc442d0eff04"
x-amz-server-side-encryption: AES256
Accept-Ranges: bytes
Server: AmazonS3
X-Cache: Miss from cloudfront
Via: 1.1 xxxxx.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: xxxxx
X-Amz-Cf-Id: xxxxx==

終わりに

新しく追加されたAWS WAFのURIフラグメントの条件判定を利用してみました。

少なくともOpenSSLでURIフラグメントごとサーバサイドに投げることで反応してるので、WAFの機能で特別に何かすることはなく投げれば判定されるというので間違いはないと思うんですがこれでいいのかという感じはあります。

ざっと標準見る限りMUST NOTまでとは言われる記載は見当たらないのですがあまり好ましくなさそうな記載はありますし、実際通常あり得そうな経路だとそもそもリクエストすることが難しそう(値があるだけで相当怪しい)という気はしますが、特定の言語のライブラリとかになると実は普通にできるよ!とかあるんでしょうか?

自分が知らないだけでCookie等特定のHTTPヘッダに含めることで実は代替できるのがHTTPの仕様に...等もあるかと思いますので知見ある方いたら情報いただけますと大変助かります。
(Twitterで本記事のURL含めて呟いてもらえるとしばらくの間であれば頑張って拾います)

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.