I Enabled the AWS WAF SDK Automatic Integration for CloudFront and Checked the Script Tag Insertion into HTML and the Changes in Responses

I Enabled the AWS WAF SDK Automatic Integration for CloudFront and Checked the Script Tag Insertion into HTML and the Changes in Responses

You can use WAF SDK signals for Bot Control and other checks through web ACL settings alone, without embedding tags in your application or CI/CD. It also affects how ETag and Range requests are handled.
2026.11.01

This page has been translated by machine translation. View original

Introduction

On 2026-10-30, AWS WAF announced automatic SDK integration for CloudFront.

https://aws.amazon.com/jp/about-aws/whats-new/2026/10/aws-waf-automatic-sdk-integration-amazon-cloudfront/

When you enable automatic SDK integration on a CloudFront-scope web ACL, a script tag for the AWS WAF JavaScript SDK is inserted into the HTML that CloudFront delivers, and rule groups such as Bot Control (targeted) can use client-side signals to determine the source of a request.

In this article, I enabled automatic SDK integration on a CloudFront distribution whose origin is an ALB with fixed responses and a Lambda target, and checked with curl and headless Chromium how the delivered HTML, the response headers, and the WAF logs change.

Configuring Automatic SDK Integration

Requirements and Defaults

The feature applies to CloudFront-scope web ACLs that include Bot Control (inspection level targeted), ATP, or ACFP. There is no additional charge for using automatic SDK integration.

If you create a new CloudFront-scope web ACL that includes a target rule group in the console after the announcement, automatic SDK integration is enabled. If you create it through the API, or add a target rule group to an existing web ACL, it remains disabled. The settings of web ACLs created before the announcement also do not change.

https://docs.aws.amazon.com/waf/latest/developerguide/waf-challenge-javascript-sdk-injection.html

Enabling via the API

In the API, you specify an empty object for CLOUDFRONT in ChallengeJavascriptSdkInjection in the web ACL's AssociationConfig. For the verification, I saved the following content as artifacts/association-config-on.json.

{
  "ChallengeJavascriptSdkInjection": {
    "CLOUDFRONT": {}
  }
}

update-web-acl replaces the web ACL's mutable settings (rules, default action, AssociationConfig, and so on) with what you pass at call time. Therefore, retrieve the current settings with get-web-acl, add this setting to the AssociationConfig, and then pass the entire configuration.

In the verification, I enabled it with the following command.

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

The contents of artifacts/web-acl-rules.json passed to --rules are listed in the next section, "Verification Setup".

Verification Setup

Item Value
Verification date and region 2026-10-31, us-east-1 (web ACL, ALB, Lambda, and logs all in this region)
Path curl / headless Chromium → CloudFront (HTTPS) → ALB (HTTP 80)
CloudFront Cache policy is Managed CachingDisabled, Compress is false
Origin ALB. / and /json are fixed responses (200). /no-transform, /etag, and /range are Lambda targets
web ACL CloudFront scope. The only rule is AWSManagedRulesBotControlRuleSet (inspection level TARGETED), overridden to Count. Logs go to CloudWatch Logs
AWS CLI aws-cli/2.39.3. ChallengeJavascriptSdkInjection is mentioned in create-web-acl help
curl 8.18.0
Browser Chromium from Playwright 1.55.0 (user agent HeadlessChrome/140.0.7339.16), run in Docker

This is the web ACL rule definition passed to --rules.

web ACL rules (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"
    }
  }
]

The ALB, Lambda, and CloudFront were created with the following CloudFormation template. Because an ALB fixed response cannot specify response headers, I used Lambda targets for the three paths used to verify response headers and the 206 response.

CloudFormation stack definition (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 }

For each path, I changed the end of the URL and fetched it with the following curl. The -i option also displays the response headers.

curl -sS -i -H "Accept-Encoding: identity" https://dxxxxxxxxxxxxx.cloudfront.net/

Changes to the Delivered HTML

Insertion of the script Tag

I fetched the root path with curl before and after enabling automatic SDK integration. The body after enabling it is as follows. The IDs in the script tag's src are masked.

<!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>

The script tag is inserted immediately after <head> without a line break, and everything else is the same as when disabled. The body length went from 115 bytes to 247 bytes.

Response Headers and Per-Path Behavior

For each origin response, I compared the disabled and enabled states.

Origin response Disabled Enabled
text/html (fixed response) 200, content-length 115 200, content-length 247, script tag present
text/html, strong ETag "strong-etag-1" 200, content-length 116, ETag "strong-etag-1" 200, content-length 248, ETag W/"strong-etag-1-waf", script tag present
text/html, Cache-Control: no-transform 200, content-length 116, no script tag 200, content-length 116, no script tag
application/json 200, content-length 11 200, content-length 11
text/plain, request with Range: bytes=0-9 (the origin responds with 206 when it receives Range; 37 bytes in total) 206, content-length 10, content-range: bytes 0-9/37 200, content-length 37, no content-range

In HTML with the script tag inserted, Content-Length increased, and the origin's strong ETag was replaced with a weak validator. HTML that returns Cache-Control: no-transform, and JSON fetched without Range, did not change.

Range requests are what affect content other than HTML. According to the Developer Guide, while automatic SDK integration is enabled, CloudFront removes the Range and If-Range headers from requests that WAF allows before forwarding them to the origin. As a result, the text/plain partial 206 response in the table changed to a full 200 response. The main concern is the impact on clients that perform partial retrieval, such as video and audio players and download managers.

Recording in the WAF Logs

Whether Insertion Occurred

The SDK insertion itself is not recorded in WAF metrics or logs. In the verification as well, there was no difference in the WAF logs between the enabled and disabled states that indicated whether insertion occurred.

Recording of Tokens

With automatic SDK integration enabled, I used headless Chromium to open the same URL twice at a 5-second interval. On the first load, challenge.js was executed and the aws-waf-token cookie was set. The second request was made with this cookie attached. When disabled, there was neither communication with the SDK nor any cookie being set.

The token-related labels in the WAF logs at this time are as follows. The prefix awswaf:managed: is omitted.

Request Automatic integration Token-related labels
curl Enabled token:absent, captcha:absent
Chromium 1st time Enabled token:absent, captcha:absent
Chromium 2nd time (same session) Enabled token:accepted, token:id:<TOKEN_ID>, token:fingerprint:<FINGERPRINT>, captcha:rejected, captcha:rejected:not_solved
Chromium 2nd time (same two steps in a new session) After reverting to disabled token:absent, captcha:absent

token:absent means the token is missing or cannot be read, and token:accepted means a valid token was received. captcha:rejected:not_solved indicates that the CAPTCHA has not been solved, and it is applied separately from the token's validity. For this reason, it can be recorded together with token:accepted. token:id is an identifier for the client session, and token:fingerprint is a fingerprint calculated from browser signals. The fingerprint is not unique to a single client.

https://docs.aws.amazon.com/waf/latest/developerguide/waf-tokens-labeling.html

Summary

When you enable automatic SDK integration, CloudFront inserts the SDK script tag immediately after <head> in HTML responses that meet the insertion conditions. This removes the need to maintain tags on each page you want to protect, and to change application code, builds, or deployments.

Even when it is difficult to embed the script tag in the application or CI/CD, Bot Control (inspection level targeted), ATP, and ACFP can use the SDK's signals. This is effective for detecting bots that are hard to identify from server-side information alone, such as those that spoof the User-Agent.

Automatic SDK integration applies to the entire web ACL and cannot be specified per path. Because the handling of Range requests and ETags changes, I recommend adopting it after evaluating the impact on your existing delivery.

Share this article

AWSのお困り事はクラスメソッドへ