Puppeteerでalert関数の呼び出しを検知したい

目次

イマドキなWebアプリはクライアントでDOMを生成するケースが多い。 例えばXSSのペイロードをパラメータに入れたとして、text/htmlのレスポンスを見てもXSSが発火するかどうかの判断がつきにくく、結局ブラウザだとこうなるので、みたいな情報が欲しくなる。

Playwrightを触っていたところ、どうやらこの手のE2Eテストツールは「alert関数のようなダイアログにhookしてなにかする」みたいな機能を備えているらしい。「これを使えば検知できるんじゃないか!」となったので、JuiceShopあたりのDOM XSSを実験台にして雑に素振りしてみる。

調べてみると、同じようなアプローチをしているツールは普通にあるので、全く画期的な何かではない(まぁ、ですよね)。

セットアップ

最近はDenoにお熱なので、なんとなくDenoで実装してみる(依存関係含めペライチで完結するのがいいですね)。

ブラウザ操作用のライブラリはPuppeteerを使う。Playwrightでも良かったけど、あとでChrome Recorderと組み合わせてみたかったので、そのあたりのサポートがよさそうなPuppeteerで。

事前にPuppeteerで使うブラウザをインストールする必要がある(たぶん)。

deno run --allow-all npm:puppeteer browsers install chrome

alert関数にhookしてみる

さっそくalert関数が呼ばれたときになにかしてみる。

ブラウザコンテキスト周りのコードは https://zenn.dev/mizchi/articles/practical-await-using を参考にした。await usingを使うとスコープから抜けるときに自動でリソース開放してくれる(リソース開放しないとブラウザが終了してくれない)。

実装はこれだけ。

import * as puppeteer from "npm:puppeteer@23.10.1";

async function useBrowserContext() {
  const browser = await puppeteer.launch({ headless: false });
  const page = (await browser.pages())[0];

  page.on("dialog", (dialog) => {
    if (dialog.type() === "alert") console.log(dialog.message());
    dialog.dismiss();
  });

  return {
    browser,
    page,
    async [Symbol.asyncDispose]() {
      await browser.close();
    },
  };
}

if (import.meta.main) {
  const payload = encodeURIComponent("<img src=x onerror=alert(`xss`)>");
  await using ctx = await useBrowserContext();
  await ctx.page.goto("http://localhost:3000/#/search?q=apple" + payload, {
    waitUntil: "networkidle0",
  });
}

console.log(dialog.message());でalertされた文字がログされるようにしている。deno run -A main.tsで実行すると、ブラウザでalertが発火していることを確認できる。

2024-12-09-19-47-07.png

コンソールにxssの文字が確認できたので、検知もできそう。おわり。