某所で見かけたWebページ上の画像保存禁止の実装
あるコンテンツの関連サイトにて、クライアントサイドでしか画像保存対策をしていなかったのが、最近になってサーバーサイドでも対策されてきた。個人的に物珍しかったので、外から見て分かるところを書いておく。
※この記事ではあるWebサイトの話をしますが、自分は中の人ではないので、本当のところの実装の意図は何もわかりません。全て推測であることを念頭に、話半分でよろしくお願いします。
概要
まず、当該サイトで行われていたクライアントサイドの画像保存対策は以下の通り。
- コンテキストメニューを開けないようにする。
- 選択できないようにする。また、選択範囲をコピーできないようにする。
- 画像をドラッグ&ドロップできないようにする。
- 画像に透明なボックスをかぶせて表示する。
- Ctrl + Uキーでソースコードを開けないようにする。また、F12キーで開発者ツールを開けないようにする。
古き良き右クリック禁止とかそういう感じで、特に目新しいものは無い。
次に、サーバーサイドの画像保存対策は以下の通り。
- 画像URLにはファイルの拡張子を含めない。
- 画像がリクエストされた時、リファラが無い or ユーザーエージェントが不正ならエラー。
- 1分間しかアクセスできない使い捨ての画像URLを使用する。期限切れならエラー。
1分間しかアクセスできない画像URLは初見でびっくりした。ちゃんと探せばよくある手法なのかもしれないが、他のコンテンツで類似のサイトを知らないので不明。
とりあえず1つずつ触れていく。
その1. コンテキストメニュー (右クリック) 禁止
右クリックでの画像保存を妨害する。
シンプルに contextmenu
イベントをブロックすることで実装する。各タグに oncontextmenu="return false"
を直接書いたり、JavaScriptで AddEventListener()
したり。
その2. 選択&コピー禁止
ページ上の文字・画像の選択とコピーを禁止することで、『画像のペーストを受け付けるアプリケーション』へのコピペを妨害する。例えば、ブラウザ上の画像をドラッグして選択してコピー → Wordに画像をペ-スト → Word上の画像を右クリックすると、画像をローカルに保存できてしまうため。
主に selectstart
, copy
イベントをブロックすることで実装する。
その3. ドラッグ&ドロップ禁止
ドラッグ&ドロップでの画像保存を妨害する。例えば、PCでブラウザ上の画像をドラッグしてデスクトップやエクスプローラにドロップすると、画像をローカルに保存できてしまうため。
実装の詳細は確認できていないが、たぶん drag
と dragstart
イベント、もしかしたら click
とか mousedown
もブロックしてるかもしれない。
その4. 透明なボックスをかぶせて表示
画像と同じ寸法の透明なボックスを画像とぴったり重ねて表示することで、右クリックでの画像保存および、スマホの長押しでの画像保存を妨害する。
類似の方法として画像を <img>
タグではなく背景画像として表示する方法があるが、ここでは割愛。
これの具体的な実装は2通り見たことがある。1つ目は、1px四方の透明な画像を拡大して重ねる方法。
<div class="image-wrapper">
<img src="/path/to/image" alt="表示したい画像" />
<img src="/path/to/blank.gif" class="blank" alt="透明な画像" />
</div>
.image-wrapper {
position: relative;
}
.image-wrapper .blank {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
2つ目は、CSSの疑似要素で透明なボックスを生成して重ねる方法。
<div class="image-wrapper">
<img src="/path/to/image" alt="表示したい画像" />
</div>
.image-wrapper {
position: relative;
}
.image-wrapper::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
後者の方が後付けで対応しやすそうだけど、確か当該サイトではもともと後者の実装だったのが、ある時から前者に切り替わったはず。なので何かしら前者の方が優れてる理由があるのかもしれない。
その5. ソースコード閲覧禁止
Ctrl + U
によるソースコードの表示と F12
による開発者ツールの起動を妨害する。ソースコードから画像URLを閲覧されると、画像に直接アクセスできてしまうため。
実装の詳細は確認できていないが、keydown
, keydown
, keyup
イベントのいずれかか複数でブロックしてるはず。
ショートカットキーをブロックしても実際にはブラウザのメニューからソースコードも開発者ツールも開けるので、あまり効果は無いと思う。でも、見られたくないことはすごく伝わってくるし、気付いた瞬間が気まずい。
その6. 画像URLにファイルの拡張子を含めない
URLのファイル名から画像であることを推測させないことで、『ページ上の画像を保存するアプリケーション』を妨害する。
例えば ImageDrain というiOSアプリを使うと、Safariで閲覧しているWebページ上の画像をスマホに保存できる。しかし2022年1月23日時点でこのアプリはファイル名の拡張子で画像を判定しているらしく、拡張子を含まない画像ファイルは保存対象にならない。
PCにも類似のアプリはあると思うが、当該サイトはスマホユーザーがかなり多そうなので、恐らくこの手のスマホアプリ対策だと思われる。
当該サイトはAWSのS3に画像ファイルを置いているので、拡張子は動的に消してるとかでなく、本当に拡張子の無い画像ファイルがアップロードされている模様。拡張子が無くてもレスポンスヘッダの Content-Type
が image/jpeg
なのは、S3のメタデータで設定してるからだと思う。
その7. リファラとユーザーエージェントを検証
リファラが当該サイトのものであることをチェックして、当該サイト以外からの画像URLに対する考え無しなアクセスをブロックする。また、ユーザーエージェントが怪しい場合もブロックする。
ブラウザから真っ当にアクセスする場合と同じリファラとユーザーエージェントを指定してアクセスすれば回避できるが、ここまでくるとそれなりの知識とツールが必要。
当該サイトはAWSのCloudFrontで画像ファイルを配信しているので、いくつか確認したところ、恐らくリファラのチェックはLambda@Edge、ユーザーエージェントのチェックはWAFで行われていた。
リファラが正しくない場合、ステータスコード403とカラのレスポンスボディが返されたので、以下のようなLambda@Edgeがオリジンリクエストに設定されていたと思われる。
'use strict';
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
// リファラが無いか、正しいURLで始まっていなければ403
if (!request.headers['referer'] || !request.headers['referer'][0].value.startsWith('https://example.com')) {
callback(null, {
status: '403',
statusDescription: '',
body: '',
});
}
callback(null, request);
};
また、ユーザーエージェントに文字列 "curl" が含まれている場合、ステータスコード403と以下のレスポンスボディが返された。これはCloudFrontのWAFでブロックされた時に表示されるページ。
<!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: R82KfE7E02UNoUTuMsNqPGIDvasuVynLVRFWKoqnjizW_mJ3qbzmHA==
</PRE>
<ADDRESS>
</ADDRESS>
</BODY></HTML>
確認していないが、ピンポイントで "curl" が弾かれていたということは "wget" なんかも弾かれてるのかも。これでブロックされたメトリクスを担当者に見られたらちょっと恥ずかしい。
その8. 1分間しかアクセスできない画像URLを使う
Webページの表示直後 (1分間) しかアクセスできない使い捨ての画像URLを使うことで、手動での画像保存自体をしんどくさせる。 また、ページを開いたまま試行錯誤していると絶対に成功しないので、画像保存を諦めさせることができる。
この方法の大胆なところは、当該サイトに真っ当にアクセスしている人であろうと有効期限までに画像へのアクセスが完了しなければブロックされるところ。また、期限内にページ上の全ての画像をダウンロードして表示しなければならない都合上、lazyloadが難しいところ。
当該サイトはAWSのCloudFrontで画像ファイルを配信しているので、CloudFrontの署名付きURLでこれを実装していた。CloudFrontには、URLにクエリパラメータで署名を付与することで「CloudFront経由で一定時間だけアクセス可能な改ざん不可能なURL」を生成する機能がある。
例えば以下のようなURLがCloudFrontの署名付きURLである。
https://xxxxxxxxxxxxxx.cloudfront.net/path/to/image?Expires=1642826192&Signature=Signature=imyf5Kzs9dJM7FF1SRxerhYCet98zs6Ns1pSsfc8C-pVeYN9l2Nsca78FWoZq7~~nPp-KH1GlkOelGRdz2hviGv0OCK2feJZRFUatq6BFSAufbssHM4WjS2OUObU~vGW7EDAufe6o3wCfNv9Wq74~m4lzenI3ZlFkOJkizBr8QqG2EbIPJJQ7KnyI-aXOuZXFKnl3EuJXSlHqaAm2UvzScSQivkm56UgetPtkewbZDiybnVTqsRuCZaxGYuXrM5OEyHr1smkPg9J3gWUg2YGcdT-cZCz5rIrLUyuTic6mjNmrt5V0Y35k~m3q7ZCnZdvFeydsVpSkb~v8e36PVuKlQ__&Key-Pair-Id=XXXXXXXXXXXXX
署名付きURLは生成時に有効期限を指定する。この有効期限は上記URLのクエリパラメータ Expires
にUnix時間 (秒) で表れている。勝手に変えてアクセスしようとしても、署名を検証する段階で改ざんがバレるのでブロックされる。
なので当該サイトは、アクセスがあるたびに対象ページ内の全画像の署名付きURLを生成し直して、<img>
タグの src
にセットしてレスポンスしてるはず。
仮にものすごくネット環境が悪いとかで、署名付きURLの生成からレスポンス完了までに1分以上経過した場合、画像が表示されないことになる。ただし一旦読み込みが始まった後なら途中で有効期限を過ぎても切断はされないようなので、ほとんどの場合は問題無いのだと思う。
以上
以上、某所で見かけた画像保存対策でしたが、ほとんどのユーザーは適当なところで妥協してスクショを撮るんじゃないかな
というか閲覧者に見えてる時点で何かしら画像自体を引っこ抜く方法はあるから、どうしても努力を尽くしたいならdマガジンみたいに画像の時点でぐちゃぐちゃのタイル状にしておいて、ブラウザ側でロジックで並べ直したものをCanvasに描画しなおすとかになると思う
それだったらCORS制約でCanvasが汚染 (taint) されるからCanvasからの画像保存もできないしね