電通総研 テックブログ

電通総研が運営する技術ブログ

[CDK] BucketDeployment のついでにスクリプトの SRI ハッシュもパラメータに保存する

こんにちは。X(クロス)イノベーション本部 ソフトウェアデザインセンター セキュリティグループの耿です。

CDK にはローカルファイルを S3 バケットにデプロイできる BucketDeployment という便利なコンストラクトがあり、静的ファイルの配信などの用途に利用できます。
これを使って JavaScript ファイルを CDK リポジトリからデプロイする時に、ついでにファイルの SRI ハッシュも計算して、Web アプリから参照できるように SSM パラメータストア or Secrets Manager に保存してみました。

SRI とは

参考:サブリソース完全性 (MDN)
サブリソース完全性 (Subresource Integrity) のことであり、取得したリソースが改ざんされていないかを検出するブラウザの仕組みです。
Webサイトが配信する <script> タグの integrity 属性にリソースのハッシュ値を付加することで、ブラウザは実際に取得したリソースのハッシュ値と一致するかどうかを確認し、一致しなかった場合にはリソースは実行されません。

<script src="https://static.my-domain.com/myscript.js" crossorigin="anonymous" integrity="sha384-mY5hfCTEoihIw/5Wlnjpg/00npMlmKiMCZFMqFRYQRbvhDpcA/JkTv7utYUF+/kA"></script>

これにより、例えば JavaScript の中身が改ざんされた場合、悪意のあるコードが実行されることを防ぐことができます。

想定している構成例

Web アプリケーションとは別ドメインからスクリプトファイルをロードする構成を想定します。スクリプトファイルは自組織が管理する S3 バケットに配置され、CloudFront ディストリビューション経由で配信されているとします。

CDK で S3 バケットスクリプトをデプロイする時に、ついでにスクリプトファイルの SRI ハッシュを計算し、SSM パラメータストア or Secrets Manager に保存しておきます。Web アプリケーションのデプロイ時などにパラメータにアクセスし、読み込むスクリプト<script> タグの integrity 属性に埋め込むように構成すれば、S3 バケットもしくは CloudFront ディストリビューションが配信するファイルが改ざんされた場合にはスクリプトは実行されません。

※ Web アプリのデプロイ時に SRI ハッシュを取得する場合、配信するスクリプトファイルを更新したら Web アプリも再デプロイする必要があることにご注意ください。

構成図

図で赤枠に囲まれている、スクリプトファイルとハッシュ値のデプロイが、この記事で紹介したい部分です。

自前で管理している S3 バケットではどれほどの効果があるのか

SRI はパブリックな CDN が配信するリソースに対して利用されることが多い印象です。そのようなリソースは攻撃の対象として狙われやすく、改ざんが成功した時の影響範囲が広いため、SRI をサポートしているリソースの場合は是非 Web アプリで対応しておきたいです。

一方で今回想定しているような、自前で管理している S3 バケットからリソースを配信する場合、SRI を利用することで防げることは比較的限定されます。いくつかの攻撃パターンに対する SRI の効果を考えてみます。

  • 意図しない第三者AWS の強いアクセス権を獲得してしまった
    • SRI はあまり意味がない。Web アプリが付与する SRI ハッシュ値を書き換えるか、integrity 属性を完全に削除してしまえばスクリプトは改ざんし放題
    • そもそもスクリプトの改ざん以外にも、直接データベースの中身を盗んだりいろいろできてしまう
  • CloudFront / S3 の設定ミスで、意図せずスクリプトが外部から改ざんできる状態になっていた
    • SRI の効果がある。CloudFront と S3 は一般アクセスを許可する構成を取れるため、基本プライベートな SSM パラメータストアや Secrets Manager、Webアプリのデプロイパイプラインよりも外部から侵入できる状態になっているリスクが高い
  • CloudFront / S3 自体のバグで、意図せずスクリプトが外部から改ざんできる状態になっていた
    • SRI の効果がある

自前で管理している S3 バケットに対しての効果は限定的とはいえ、プログラミング言語で IaC を書ける CDK であれば、さほど難しくない実装で SRI ハッシュの計算と保存をすることができます。効果が限定的だからこそ、悩まず簡単に実装できるようにブログ記事に書き残しておく意味はあるんだろうなと思っています。

ちなみに S3 の設定ミスによる外部公開を防ぐために、パブリックアクセスブロック設定を利用しましょう。OAC を使えば CloudFront のオリジンとなっている S3 バケットにもパブリックアクセスブロックを設定できます。

実装例

ここから実装例を紹介していきます。

BucketDeployment で静的ファイルを S3 にデプロイ

BucketDeployment コンストラクトを利用し、 ./static フォルダの中身を S3 バケットにデプロイするコンストラクト例です。S3 バケットと CloudFront ディストリビューションは別コンストラクトで作成済みの前提です。distribution プロパティに CloudFront ディストリビューションを渡すことで、デプロイ時に CloudFront のキャッシュの無効化もやってくれます。

Cache-Control ヘッダーに no-store を設定するのがポイントで、これによりクライアント(ブラウザ)側でリソースがキャッシュされなくなり、スクリプトを更新した時にキャッシュされた古いスクリプトと新しい SRI ハッシュの整合性が取れなくなる事態を回避できます。

import { StackProps } from "aws-cdk-lib";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3deployment from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";

export interface MyConstructProps extends StackProps {
  bucket: s3.Bucket;
  distribution: cloudfront.Distribution;
}

export class MyConstruct extends Construct {
  constructor(scope: Construct, id: string, props: MyConstructProps) {
    super(scope, id);

    new s3deployment.BucketDeployment(this, "MyBucketDeployment", {
      destinationBucket: props.bucket,
      sources: [s3deployment.Source.asset("./static")],
      prune: false,
      retainOnDelete: false,
      distribution: props.distribution,
      cacheControl: [s3deployment.CacheControl.noStore()],
      contentType: "text/javascript",
    });
  }
}

ファイルの SRI ハッシュを計算する

TypeScript では次のような関数で、ファイルの SRI 用のハッシュ値を計算できます。

import * as crypto from "crypto";
import * as fs from "fs";

export const sriHash = (filePath: string): string => {
  const data = fs.readFileSync(filePath);
  const digest = crypto.createHash("sha384").update(data).digest("base64");
  return `sha384-${digest}`;
};

SRI ハッシュを SSM Parameter Store か Secrets Manager に登録

上に記載した SRI ハッシュを計算する関数は同期処理なので CDK スタック内から普通に呼び出せます。

new secretsmanager.Secret(this, "MySRISecret", {
  secretName: "MySRI",
  secretStringValue: SecretValue.unsafePlainText(sriHash("./static/myscript.js")),
  removalPolicy: RemovalPolicy.DESTROY,
});
new ssm.StringParameter(this, "MySRIParameter", {
  parameterName: "/MY_SRI",
  stringValue: sriHash("./static/myscript.js"),
});

(おまけ) aws-actions/aws-secretsmanager-get-secrets

パラメータとして登録された SRI ハッシュを、Web アプリのデプロイ時に取得して <script> タグに埋め込む方法はいろいろあると思いますが、GitHub Actions では aws-actions/aws-secretsmanager-get-secrets という AWS 公式の Action を利用するのが便利です。SSM パラメータストアには使えず Secrets Manager 限定ですが、次の例のようにシークレット名 MySRI を取得し、MY_SCRIPT_SRI という環境変数に格納してくれます。

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::111122223333:role/my-deployment-role
          aws-region: ap-northeast-1
      - name: Get secrets by name and by ARN
        uses: aws-actions/aws-secretsmanager-get-secrets@v1
        with:
          secret-ids: |
            MY_SCRIPT_SRI, MySRI
          parse-json-secrets: false

私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。

セキュリティエンジニア

執筆:@kou.kinyo、レビュー:@kano.nanami
Shodoで執筆されました