
CloudFront の AWS WAF SDK 自動統合を有効にして、HTML への script タグ挿入と応答の変化を確認してみた
はじめに
2026-10-30 に、AWS WAF が CloudFront 向けの SDK 自動統合を発表しました。
CloudFront スコープの web ACL で SDK 自動統合を有効にすると、CloudFront が配信する HTML に AWS WAF JavaScript SDK の script タグが挿入され、Bot Control(targeted)などの rule group が、リクエスト元の判定にクライアント側のシグナルを使えるようになります。
この記事では、固定応答と Lambda ターゲットを持つ ALB をオリジンにした CloudFront で SDK 自動統合を有効にし、配信される HTML、応答ヘッダー、WAF ログがどう変わるかを curl とヘッドレス Chromium で確かめました。
SDK 自動統合の設定
要件と既定値
対象は、Bot Control(inspection level が targeted)、ATP、ACFP のいずれかを含む CloudFront スコープの web ACL です。SDK 自動統合の利用に追加料金はかかりません。
発表後に対象の rule group を含む CloudFront スコープの web ACL をコンソールで新規作成すると、SDK 自動統合は有効になります。API で新規作成した場合や、既存の web ACL に対象の rule group を追加した場合は無効のままです。発表前に作った web ACL の設定も変わりません。
API での有効化
API では、web ACL の AssociationConfig にある ChallengeJavascriptSdkInjection の CLOUDFRONT に空のオブジェクトを指定します。検証では次の内容を artifacts/association-config-on.json として保存しました。
{
"ChallengeJavascriptSdkInjection": {
"CLOUDFRONT": {}
}
}
update-web-acl は、web ACL の可変設定(ルール、既定アクション、AssociationConfig など)を呼び出し時に渡した内容で置き換えます。そのため、get-web-acl で現在の設定を取得し、AssociationConfig にこの設定を加えたうえで、設定全体を渡します。
検証では次のコマンドで有効にしました。
aws wafv2 update-web-acl --region us-east-1 \
--name waf-sdk-verify --scope CLOUDFRONT --id <WEB_ACL_ID> --lock-token <LOCK_TOKEN> \
--default-action Allow=\{\} \
--rules file://artifacts/web-acl-rules.json \
--visibility-config SampledRequestsEnabled=true\,CloudWatchMetricsEnabled=true\,MetricName=waf-sdk-verify \
--association-config file://artifacts/association-config-on.json
--rules に渡した artifacts/web-acl-rules.json の内容は、次の「検証構成」の節に載せています。
検証構成
| 項目 | 値 |
|---|---|
| 検証日・リージョン | 2026-10-31、us-east-1(web ACL、ALB、Lambda、ログすべて) |
| 経路 | curl・ヘッドレス Chromium → CloudFront(HTTPS)→ ALB(HTTP 80) |
| CloudFront | キャッシュポリシーは Managed の CachingDisabled、Compress は false |
| オリジン | ALB。/ と /json は固定応答(200)。/no-transform、/etag、/range は Lambda ターゲット |
| web ACL | CloudFront スコープ。ルールは AWSManagedRulesBotControlRuleSet(inspection level は TARGETED)の 1 つだけで、Count に上書き。ログは CloudWatch Logs |
| AWS CLI | aws-cli/2.39.3。create-web-acl help に ChallengeJavascriptSdkInjection の記載あり |
| curl | 8.18.0 |
| ブラウザー | Playwright 1.55.0 の Chromium(ユーザーエージェントは HeadlessChrome/140.0.7339.16)を Docker で実行 |
--rules に渡した web ACL のルール定義です。
web ACL のルール(artifacts/web-acl-rules.json)
[
{
"Name": "BotControlTargeted",
"Priority": 0,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesBotControlRuleSet",
"ManagedRuleGroupConfigs": [
{ "AWSManagedRulesBotControlRuleSet": { "InspectionLevel": "TARGETED" } }
]
}
},
"OverrideAction": { "Count": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "BotControlTargeted"
}
}
]
ALB、Lambda、CloudFront は次の CloudFormation テンプレートで作成しました。ALB の固定応答では応答ヘッダーを指定できないため、応答ヘッダーと 206 応答を検証する 3 つのパスには Lambda ターゲットを使いました。
CloudFormation スタック定義(artifacts/stack.yaml)
AWSTemplateFormatVersion: "2010-09-09"
Description: waf-sdk-verify - fixed-response ALB + CloudFront (verification only, delete after use)
Parameters:
VpcId: { Type: AWS::EC2::VPC::Id }
SubnetIds: { Type: List<AWS::EC2::Subnet::Id> }
WebAclArn: { Type: String }
CloudFrontPrefixListId: { Type: String }
Resources:
AlbSg:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: waf-sdk-verify ALB (HTTP 80 from CloudFront origin-facing only)
VpcId: !Ref VpcId
SecurityGroupIngress:
- { IpProtocol: tcp, FromPort: 80, ToPort: 80, SourcePrefixListId: !Ref CloudFrontPrefixListId }
Alb:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Type: application
Scheme: internet-facing
Subnets: !Ref SubnetIds
SecurityGroups: [!Ref AlbSg]
LambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- { Effect: Allow, Principal: { Service: lambda.amazonaws.com }, Action: "sts:AssumeRole" }
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Fn:
Type: AWS::Lambda::Function
Properties:
Runtime: python3.13
Handler: index.handler
Role: !GetAtt LambdaRole.Arn
Code:
ZipFile: |
HTML = "<!DOCTYPE html>\n<html>\n<head>\n<title>WAF SDK verify</title>\n</head>\n<body>\n<h1>lambda response</h1>\n</body>\n</html>\n"
TEXT = "0123456789abcdefghijklmnopqrstuvwxyz\n"
def resp(status, desc, ctype, body, extra=None):
h = {"Content-Type": ctype}
h.update(extra or {})
return {"statusCode": status, "statusDescription": desc, "headers": h,
"body": body, "isBase64Encoded": False}
def handler(event, context):
path = event.get("path")
headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()}
if path == "/no-transform":
return resp(200, "200 OK", "text/html", HTML, {"Cache-Control": "no-transform"})
if path == "/etag":
return resp(200, "200 OK", "text/html", HTML, {"ETag": '"strong-etag-1"'})
if path == "/range":
rng = headers.get("range", "")
if rng.startswith("bytes="):
s, e = rng[6:].split("-")
s = int(s)
e = int(e) if e else len(TEXT) - 1
return resp(206, "206 Partial Content", "text/plain", TEXT[s:e + 1],
{"Accept-Ranges": "bytes", "Content-Range": "bytes %d-%d/%d" % (s, e, len(TEXT))})
return resp(200, "200 OK", "text/plain", TEXT, {"Accept-Ranges": "bytes"})
return resp(404, "404 Not Found", "text/plain", "not found\n")
FnTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DependsOn: FnPermission
Properties:
TargetType: lambda
Targets:
- Id: !GetAtt Fn.Arn
FnPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt Fn.Arn
Principal: elasticloadbalancing.amazonaws.com
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref Alb
Port: 80
Protocol: HTTP
DefaultActions:
- Type: fixed-response
FixedResponseConfig:
StatusCode: "200"
ContentType: text/html
MessageBody: "<!DOCTYPE html>\n<html>\n<head>\n<title>WAF SDK verify</title>\n</head>\n<body>\n<h1>fixed response</h1>\n</body>\n</html>\n"
RuleJson:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref Listener
Priority: 10
Conditions: [{ Field: path-pattern, Values: [/json] }]
Actions:
- Type: fixed-response
FixedResponseConfig: { StatusCode: "200", ContentType: application/json, MessageBody: '{"ok":true}' }
RuleNoTransform:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref Listener
Priority: 20
Conditions: [{ Field: path-pattern, Values: [/no-transform] }]
Actions: [{ Type: forward, TargetGroupArn: !Ref FnTargetGroup }]
RuleRange:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref Listener
Priority: 30
Conditions: [{ Field: path-pattern, Values: [/range] }]
Actions: [{ Type: forward, TargetGroupArn: !Ref FnTargetGroup }]
RuleEtag:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref Listener
Priority: 40
Conditions: [{ Field: path-pattern, Values: [/etag] }]
Actions: [{ Type: forward, TargetGroupArn: !Ref FnTargetGroup }]
Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
Comment: waf-sdk-verify (delete after use)
HttpVersion: http2
WebACLId: !Ref WebAclArn
Origins:
- Id: alb
DomainName: !GetAtt Alb.DNSName
CustomOriginConfig: { OriginProtocolPolicy: http-only, HTTPPort: 80 }
DefaultCacheBehavior:
TargetOriginId: alb
ViewerProtocolPolicy: redirect-to-https
AllowedMethods: [GET, HEAD]
Compress: false
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac
Outputs:
DomainName: { Value: !GetAtt Distribution.DomainName }
DistributionId: { Value: !Ref Distribution }
AlbDns: { Value: !GetAtt Alb.DNSName }
各パスは、URL の末尾を変えて次の curl で取得しました。-i オプションで応答ヘッダーも表示しています。
curl -sS -i -H "Accept-Encoding: identity" https://dxxxxxxxxxxxxx.cloudfront.net/
配信 HTML の変化
script タグの挿入
SDK 自動統合の有効化前後で、ルートパスを curl で取得しました。有効化後の本文は次のとおりです。script タグの src に含まれる ID はマスクしています。
<!DOCTYPE html>
<html>
<head><script type="text/javascript" src="https://<ID1>.edge.sdk.awswaf.com/<ID1>/<ID2>/challenge.js" defer></script>
<title>WAF SDK verify</title>
</head>
<body>
<h1>fixed response</h1>
</body>
</html>
<head> の直後に改行を挟まず script タグが入り、それ以外の内容は無効時と同じです。本文の長さは 115 バイトから 247 バイトになりました。
応答ヘッダーとパス別の挙動
オリジンの応答ごとに、無効のときと有効のときを比べました。
| オリジンの応答 | 無効 | 有効 |
|---|---|---|
| text/html(固定応答) | 200、content-length 115 | 200、content-length 247、script タグあり |
| text/html、強い ETag "strong-etag-1" | 200、content-length 116、ETag "strong-etag-1" | 200、content-length 248、ETag W/"strong-etag-1-waf"、script タグあり |
| text/html、Cache-Control: no-transform | 200、content-length 116、script タグなし | 200、content-length 116、script タグなし |
| application/json | 200、content-length 11 | 200、content-length 11 |
| text/plain、Range: bytes=0-9 を付けた要求(オリジンは Range を受け取ると 206 で応答、全体 37 バイト) | 206、content-length 10、content-range: bytes 0-9/37 | 200、content-length 37、content-range なし |
script タグが挿入された HTML では Content-Length が増え、オリジンの強い ETag は弱い検証子に置き換わりました。Cache-Control: no-transform を返す HTML と、Range を付けずに取得した JSON には変化がありませんでした。
HTML 以外にも影響するのが Range 要求です。Developer Guide によると、SDK 自動統合が有効な間、CloudFront は WAF が許可したリクエストから Range と If-Range ヘッダーを削除し、オリジンへ転送します。そのため、表の text/plain では 206 の部分応答が 200 の全体応答に変わりました。主に注意が必要なのは、動画や音声のプレーヤー、ダウンロードマネージャーなど、部分取得を行うクライアントへの影響です。
WAF ログの記録
挿入の有無
SDK の挿入自体は WAF のメトリクスやログに記録されません。検証でも、有効時と無効時の WAF ログに挿入の有無を示す差はありませんでした。
トークンの記録
SDK 自動統合を有効にした状態で、ヘッドレス Chromium を使って同じ URL を 5 秒間隔で 2 回開きました。初回の読み込みで challenge.js が実行され、aws-waf-token Cookie が設定されました。2 回目はこの Cookie を付けてリクエストしました。無効時には SDK への通信も Cookie の設定もありませんでした。
このときの WAF ログのトークン関連ラベルは次のとおりです。接頭辞 awswaf:managed: は省略しています。
| リクエスト | 自動統合 | トークン関連のラベル |
|---|---|---|
| curl | 有効 | token:absent、captcha:absent |
| Chromium 1 回目 | 有効 | token:absent、captcha:absent |
| Chromium 2 回目(同じセッション) | 有効 | token:accepted、token:id:<TOKEN_ID>、token:fingerprint:<FINGERPRINT>、captcha:rejected、captcha:rejected:not_solved |
| Chromium 2 回目(新しいセッションで同じ 2 回の手順を実施) | 無効に戻した後 | token:absent、captcha:absent |
token:absent はトークンが無いか読み取れない状態、token:accepted は有効なトークンを受け取った状態です。captcha:rejected:not_solved は CAPTCHA が未解答であることを示し、トークンの有効性とは別に付与されます。このため、token:accepted と同時に記録されることがあります。token:id はクライアントセッションの識別子、token:fingerprint はブラウザーのシグナルから算出したフィンガープリントです。フィンガープリントは単一のクライアントに固有ではありません。
まとめ
SDK 自動統合を有効にすると、CloudFront は挿入条件を満たす HTML 応答の <head> 直後に SDK の script タグを挿入します。これにより、保護したいページごとのタグの保守や、アプリケーションのコード、ビルド、デプロイの変更が不要になります。
アプリケーションや CI/CD に script タグを組み込みにくい場合でも、Bot Control(inspection level が targeted)、ATP、ACFP は SDK のシグナルを利用できます。User-Agent を偽装するなど、サーバー側の情報だけでは見分けにくいボットの判定に有効です。
SDK 自動統合は web ACL 全体に適用され、パス単位では指定できません。Range 要求や ETag の扱いが変わるため、既存の配信への影響を評価したうえでの導入をおすすめします。








