Presigned-URLの発行
S3にPUTできるPresigned-URLを発行する。
発行はAWS SDK for JavaScript v3 (AWS Signature V4)にて行うこととする。
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const client = new S3Client({
region: 'xxxx',
});
const command = new PutObjectCommand({
ExpectedBucketOwner: '123456789012',
Bucket: 'example',
Key: 'foo/bar.png',
ContentType: 'image/png',
ContentLength: 1234,
ContentMD5: 'abc123abc/123abc123abc==',
});
const url = await getSignedUrl(client, command, {
expiresIn: 60,
});
console.log(url);
当然、問題無くPUTできる。
curl -v \
-X PUT \
-H 'content-type: image/png' \
-H 'content-length: 1234' \
-H 'content-md5: abc123abc/123abc123abc==' \
--upload-file 'bar.png' \
'https://example.s3.xxxx.amazonaws.com/foo/bar.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIXXX&X-Amz-Date=20230101T123456Z&X-Amz-Expires=60&X-Amz-Security-Token=XXXXX&X-Amz-Signature=abc123&X-Amz-SignedHeaders=content-length%3Bcontent-md5%3Bhost&x-amz-expected-bucket-owner=123456789012&x-id=PutObject'
Presgined-URLの署名対象
Presgined-URLのQUERY_STRINGに注目してみると、
X-Amz-SignedHeaders=content-length%3Bcontent-md5%3Bhost
となっている。
ヘッダーにおいてはcontent-length
、content-md5
、host
に対してしか署名されていない雰囲気が伝わってくる……。
PUTヘッダー値の操作
ということで、
- その他ヘッダーを付与する
-
content-type
値を任意のものにする
ことが可能であるため、
curl ... \
-H 'cache-control: no-cache' \
-H 'content-type: image/jpeg' \
...
などとヘッダー情報を書き換えても問題無くPUTでき、PUTされたS3オブジェクトのメタデータは実際にその通りになる。
※注:x-amz-*
ヘッダーは署名しなければ追加できない。
特に影響が大きいケース
このS3バケットをCloudFront経由もしくは直接的に公開している場合は、
- CloudFrontのキャッシュ挙動が、CloudFront側キャッシュポリシーとS3オリジン(当S3オブジェクト)側キャッシュ関連ヘッダーの状態に合わせて変わること
- 閲覧ユーザー(クライアント)へのレスポンスにそのまま適用されること
などの理由により、例示のヘッダー操作影響は特に大きくなる。
調整方法
パターン1
ヘッダーと値をセットしつつ、署名対象ヘッダーを指定する。
これにより、ヘッダー情報を書き換えてPUTしようとすると失敗することになる。
...;
const command = new PutObjectCommand({
...,
CacheControl: 'max-age=60',
ContentType: 'image/png',
...,
});
const url = await getSignedUrl(client, command, {
...,
signableHeaders: new Set([
'cache-control',
'content-type',
]),
});
...;
X-Amz-SignedHeaders=cache-control%3Bcontent-length%3Bcontent-md5%3Bcontent-type%3Bhost
パターン2
ヘッダー情報を書き換えたPUTは許容するが、PUTされた後のS3オブジェクトを、メタデータを上書き(別キー or 同キーでのオブジェクトコピー)してから利用する。
MetadataDirective.REPLACE
が文字通りメタデータ全体を「REPLACE」するため、他にも独自メタデータがあった場合は、それらも全て列挙する必要がある(そう言われるとMetadataDirective.COPY
を使いたくなるかもしれないが、強制上書きにならないので力不足)。
import { S3Client, CopyObjectCommand } from '@aws-sdk/client-s3';
const client = new S3Client({
region: 'xxxx',
});
const command = new CopyObjectCommand({
ExpectedBucketOwner: '123456789012',
CopySource: 'example/foo/bar.png',
Bucket: 'example',
Key: 'foo/baz.png',
MetadataDirective: 'REPLACE',
CacheControl: 'max-age=60',
ContentType: 'image/png',
});
const ret = await client.send(command);
console.log(ret);
おまけ:ファイルPUT
import { createReadStream } from 'node:fs';
import fetch from 'node-fetch';
const res = await fetch(url, {
method: 'PUT',
headers: {
'cache-control': 'max-age=60',
'content-type': 'image/png',
'content-length': 1234,
'content-md5': 'abc123abc/123abc123abc==',
},
body: createReadStream('bar.png'),
});
const body = await res.text();
console.log({
status: res.status,
statusText: res.statusText,
body: body,
});
パラメーターを色々と変更しながら、Presigned-URL発行からファイルPUTまで一気に実行してみると、挙動が分かり易い。