x.com のブックマークレットでCSPを回避して外部スクリプトを実行する
x.com 用に公開されている一括ツイ消しツールというブックマークレットが、通常はCSPによって素直に実現できない(実質的な)外部スクリプトの取得と実行を行っていたため、気になって調べたメモ。
Content Security Policy (CSP)
CSP は Content-Security-Policy
ヘッダを指定することでブラウザ側に読み込むことを許可するコンテンツを伝えるセキュリティーのための機能。XSSの対策などに有効。
x.com における CSP とブックマークレット
x.com における CSP の設定は以下のようになっている。
connect-src 'self' blob: ... https://*.twimg.com ...;
script-src 'self' 'unsafe-inline' ... 'nonce-XXXXXXXXXX';
connect-src
ディレクティブは fetch()
や XMKHttpRequst
などで読み込むことができる URL を制限するためのもの。この指定があるために、 x.com 上ではブックマークレットを用いて外部の JavaScript などを取得することはできない。
次に、 script-src
ディレクティブ。これは有効な JavaScript のソースを指定することができる。これによってブックマークレットにおける eval()
や追加した <script>
要素などが実行されなくなっている。
スクリプトの画像化による外部スクリプトの取得
先ほどの connect-src
の指定をよく確認すると、 twimg.com が許可されている。そのため、x へ投稿された画像であれば fetch()
などで取得できるということになる。
connect-src 'self' blob: ... https://*.twimg.com ...;
これを利用し、事前にスクリプトを画像化して投稿しておき、それをデコードすることで外部スクリプトを取得することができる。
実際にテキストと画像を相互に変換できるようにしたデモサイト:
文字列から画像への変換
デモサイトでは、 canvas を利用して 200px x 200px の画像を作成し、文字列の unicode コードポイント(U+1234
のようなもの)の数値を RGB の値に上下 8bit ずつに分割して変換することで 200*200*3/2=6万文字程のスクリプトが保存できるようになっている。
生成された画像
事前にこの画像を x に投稿しておき、ブックマークレットの中の fetch()
で取得、 canvas に描画して1ピクセルごとにRGBの値を読み取ってそれをRG,BR,GBのように繋げてunicodeコードポイントに変換することでデコードする。上で生成したものを読み込んだ際のスクリーンショット。正しくデコードできていることが確認できる。
画像から文字列への変換
nonce の偽装
スクリプトの画像化によって実質的に connect-src
の制限は回避することができるようになった。外部スクリプトの取得まではできたので、あとはそれを実行することができればCSPが回避できたといえるだろう。
ここで再度 script-src
の指定を確認すると以下のようになっており、 unsafe-inline
が許可されているため、任意のインラインスクリプトが動作するように思ってしまう。しかし、モダンブラウザにおいては CSP nonce や hash が存在する場合には unsafe-inline
の指定は無視されるので、実際には有効な nonce を持たないスクリプトは実行できない[1]。
script-src 'self' 'unsafe-inline' ... 'nonce-XXXXXXXXXX';
そのため、取得したスクリプトを実行するには nonce を偽装する必要がある。
意図的に SecurityPolicyViolationEvent
を発火させ、 event.originalPolicy
を通して CSP の指定を読み取り、 nonce を取得する。
以下のようなスクリプトをブックマークレットとして実行すると、 script-src でインラインスクリプトの実行が許可されていないために eval("")
が CSP 違反となり SecurityPolicyViolationEvent
が発火し、CSPの指定が取得できる。
ここから適当に nonce-
の値を取得し、それを実行したいスクリプトの nonce として指定することで実行できるようになる。
const func = async (event: SecurityPolicyViolationEvent) => {
console.log(event.originalPolicy)
}
document.addEventListener("securitypolicyviolation", func);
eval("")
おわりに
さて、これで一括ツイ消しツールがどのようにして connect-src
と script-src
の制限を回避しているかがわかった。CSP についてはこれまで調べたことがなくあまりよくわかっていなかったので学びになった。
Discussion