サイト内検索

Backlog へのうっかりアップロードを止める拡張機能 backlog-toolkit を作った — UI 抑止と declarativeNetRequest を併用し、ブロック要因を帰属させる

重岡 正 · Mon, October 26, 2026

Backlog の添付ファイル機能は、課題・コメント・Wiki・共有ファイルの各所から呼び出せ、ドラッグドロップや画像ペーストでも反応します。この入り口の多さは、通常業務では便利な一方で、「本来は添付してはいけない資料をうっかり落としてしまう」事故が起こりやすい面でもあります。アクセス権や DLP の設定が整うまでの間に、「人為的な取り違えをブラウザ側で先に止める」ための PoC が欲しくなり、Chrome と Microsoft Edge 向けの非公式拡張機能として backlog-toolkit を作りました。PR は codenote-net/backlog-toolkit#1 です。

拡張機能は Nulab 非公認の個人プロジェクトで、現時点では proof of concept の位置づけです。本記事ではこの PoC を設計する際に判断した点をまとめます。

追加した機能の概要

  • 設定済み Backlog スペースのみで動作する、Manifest V3 の Chrome / Edge 拡張機能
  • 動的に差し込まれる input[type="file"] を含めて、ファイル入力・添付ボタンを非表示かつ無効化する
  • ドラッグドロップとファイル/画像ペーストを横取りする。テキストのみのペーストは通す
  • /api/v2/space/attachment・/AddAttachmentAsync.action・/AddWikiAttachmentAsync.action・/r/files/addSharedFile の POST を declarativeNetRequest でブロックする
  • declarativeNetRequestFeedback と onRuleMatchedDebug を使い、「拡張機能自身のルールが実際にマッチしたこと」だけを通知の根拠にする
  • config.local.json と dist/ を .gitignore に入れ、プライベートなスペース名をコミットに残さない
  • ローカルの Chrome demo(127.0.0.1:4173)、設定バリデーション、自動テスト、docs/chrome-verification.md の検証記録を同梱する

UI 抑止と通信ブロックを別レイヤで両方かける

Backlog のアップロード導線は、ファイル入力要素だけではなく、独自の添付ボタンや、エディタ内のドロップゾーン、クリップボードからの画像ペーストと複数あります。UI 側だけを抑止しても、ドラッグドロップやキーボードショートカットから別経路でアップロードが走る可能性があり、通信側だけをブロックしても「見た目は添付したつもりで、サーバーには届いていない」という紛らわしい挙動が残ります。

PoC では、両方のレイヤを別々にかける構成にしました。

  • UI レイヤ: content script で input[type="file"] と設定済みの添付コントロールを非表示・無効化し、ドロップ・ペースト・クリック・change を addEventListener(type, handler, true) の capture フェーズで止める
  • 通信レイヤ: Manifest V3 の declarativeNetRequest で、設定されたオリジンと uploadPaths の組に POST が来たら block する

content script 側では、Backlog の SPA 的な差し替えでファイル入力が後から挿入されるケースに合わせ、MutationObserver で DOM 変更を監視します。ただし、observer のマイクロタスク内で DOM を書き戻すと再入が起きやすいので、requestAnimationFrame 側に出し、「不変の DOM を再書き換えしない」ことも明示しました。

ドロップとペーストは、DataTransfer.types に Files が入るか、DataTransferItem.kind が file のアイテムがあるときだけブロックします。クリップボードがテキストとファイルの混在だった場合も、ファイルを含む時点でペーストイベント全体をブロックします。

declarativeNetRequest の regexFilter と requestMethods で POST だけ塞ぐ

Manifest V3 では、以前の webRequest によるブロッキングではなく、宣言的な declarativeNetRequest ルールを使います。本 PoC では、scripts/config.mjs でオリジンごと・パスごとにルールを生成しました。

export function makeRules(config) {
  return config.origins.flatMap((origin) => config.uploadPaths.map((path) => ({
    id: 0,
    priority: 1,
    action: { type: 'block' },
    condition: {
      regexFilter: `^${escapeRegex(origin)}${escapeRegex(path)}/?(\\?.*)?$`,
      isUrlFilterCaseSensitive: true,
      requestMethods: ['post'],
    },
  }))).map((rule, index) => ({ ...rule, id: index + 1 }));
}

ここで意識したのは次の 3 点です。

  • regexFilter を使い、オリジンとパスを別々にエスケープして組み立てる。urlFilter のパターン言語は便利ですが、クエリ文字列の有無やトレーリングスラッシュを一度に捌くには regex のほうが見通しがよい
  • requestMethods: ['post'] を付けて GET のダウンロード経路を塞がない。添付のダウンロードや Wiki のサムネイル取得は通したい
  • isUrlFilterCaseSensitive: true を明示する。Backlog の API パスは実測で大文字小文字が固定で、曖昧マッチにする理由がない

初期の設定例には /api/v2/space/attachment の公式 API だけを入れていましたが、実際の Chrome 検証で課題・コメント・Wiki・共有ファイルの各経路が別の非公開パスを叩くことが分かり、/AddAttachmentAsync.action・/AddWikiAttachmentAsync.action・/r/files/addSharedFile を実測で追加しました。この経路は Nulab の公開 API ではないため、将来の UI 変更で変わる可能性があります。本 PoC が「完全なアップロード防止」ではないと README で明記したのは、この種の経路変化を根拠にしています。

オリジンの検証を 1 スペース・HTTPS・パスなしに絞る

拡張機能の対象スペースは、config.local.json に明示する契約にしました。scripts/config.mjs のバリデーションは次を要求します。

  • HTTPS であること
  • ホスト名が backlog.com・backlog.jp・backlogtool.com のいずれかのサブドメイン 1 段であること
  • 正規化後の url.origin が入力文字列と完全一致すること
  • ポート指定がないこと(demo の http://127.0.0.1:4173 を除く)
  • uploadPaths が / から始まり、.. を含まないパスだけであること
const backlog = url.protocol === 'https:' &&
  /^[a-z0-9-]+\.(backlog\.(com|jp)|backlogtool\.com)$/.test(url.hostname);
const local = demo && origin === 'http://127.0.0.1:4173';
if ((!backlog && !local) || url.origin !== origin || url.port && !local) {
  throw new Error(`Unsupported origin: ${origin}. ...`);
}

ワイルドカードや末尾スラッシュを受け取り、警告なしに正規化して通す経路は入れていません。正規化後のオリジンが入力と一致しなければ例外にします。利用者には明示的な URL を要求し、config.local.json と dist/ は .gitignore でリポジトリから除外しています。これは、OSS のテンプレートリポジトリでスペース名が意図せずコミットされるのを避けるためです。

onRuleMatchedDebug で自分のルールのマッチだけを帰属させる

拡張機能側で「アップロードがブロックされました」という通知をユーザーに見せるとき、どの事象を根拠にするかで挙動が変わります。素朴に実装すると、ネットワークエラー全般やサーバー側の 400 系まで「拡張機能が止めた」と誤って見せかねません。

本 PoC では、declarativeNetRequestFeedback 権限と onRuleMatchedDebug イベントを使い、「拡張機能自身の upload_blocking ruleset がマッチしたとき」だけを通知の根拠にしました。

chrome.declarativeNetRequest.onRuleMatchedDebug?.addListener((info) => {
  if (info.rule.rulesetId !== 'upload_blocking' || info.request.tabId < 0) return;
  chrome.tabs.sendMessage(info.request.tabId, {
    type: 'backlog-toolkit:upload-blocked',
  }, { frameId: Math.max(info.request.frameId, 0) }).catch(() => {
    // ...
  });
});

onRuleMatchedDebug は Chrome のドキュメントに明記のとおり、unpacked 拡張機能でのみ発火します。この PoC が「完全な通知の帰属」を保証できるのは unpacked でロードしているときに限る、という点も README とコメントに書きました。Chrome Web Store 配信版で同等の帰属を実現するには、別の通知設計が必要になります。

content script 側では、ルールマッチの通知を 500 ms 以内の二重発火でまとめ、通知のトーストと、添付コントロール脇の恒常的な説明文の両方を更新します。「別のタブ」や「拡張機能のインストール前に開かれていた画面」など listener がいないフレームに対する sendMessage は、.catch(() => {}) で握りつぶす設計にしました。

「サーバー認可の代替」ではないことを README と権限で明示する

このツールは、あくまでブラウザ側の人為的事故の低減が目的で、サーバー側の認可の代替ではありません。README では、無効化できないセキュリティ境界ではないことを明示しています。

  • 拡張機能の無効化・削除、別ブラウザや curl 等の直接 API クライアントの利用、モバイルアプリ経由では当然ながら素通りする
  • 未リストのアップロード経路、非 POST のアップロードプロトコル、独自エディタ、shadow DOM、クロスオリジンのフレームは、カバー対象外または保証対象外
  • 既存の添付ファイルは削除しない

拡張機能が要求する権限も最小化しています。declarativeNetRequestWithHostAccess と declarativeNetRequestFeedback、および config.local.json に書かれた host permissions のみを要求し、バックエンドサーバー・テレメトリ・ストレージ・API 認証情報は持ちません。service worker が転送するのは、確認済みのルールマッチ通知だけで、URL やファイルの中身は送りません。network rule 側もリクエストボディを検査しない設計です。

Chrome 検証と検証記録を docs/chrome-verification.md に残す

PoC といえど、「本物の Backlog スペースで動くか」を検証しない限り、UI 抑止のセレクタや通信ブロックの対象 URL が実環境と合っていることは確認できません。認可されたテストプロジェクトで、Chrome 検証を実施しました。検証記録は docs/chrome-verification.md に残しています。

主なチェックは以下です。

  • ローカル demo の 9 チェックが全てパスし、拡張機能を無効化すると期待どおり落ちる
  • 課題の添付通信(/AddAttachmentAsync.action)が Chrome の Network で (blocked:other) 0 B になり、課題は送信されない
  • コメント添付も同じ経路で 0 B になり、コメントは送信されない
  • Wiki の /AddWikiAttachmentAsync.action と共有ファイルの /r/files/addSharedFile を実測で発見し、設定へ追加した上で、拡張機能自身のルールマッチ通知で確認する
  • ネイティブのファイル入力、設定済みの課題・コメント・Wiki・共有ファイルのコントロールが非表示かつ無効化されている
  • 課題テキストのペーストとコメントのプレビューは通常どおり動く。画像の clipboard ペーストはブロック通知が出て挿入されない
  • ナビゲーションとリロード後も Wiki・共有ファイルのコントロールは制限されたまま
  • 無害な共有ファイルフィクスチャはダウンロードでき、バイト列が一致する
  • 設定していない Backlog スペース(および production ビルドでの localhost)には影響しない
  • 自動回帰テスト 8 件がパスする。4 つのパス、オリジン・パスの境界、POST のみブロック、通知のルーティングを検証する

ネットワーク傍受の確認は、UI 抑止と独立に成立することを示すために、生成物側だけを一時的に書き換え、「ネイティブのファイルコントロールを残したまま通信ルールとルールマッチ通知だけが効く」状態で行いました。ソース側の防護は変更していません。確認後は通常ビルドを戻し、拡張機能を再読み込み、課題ページも再読み込みしています。

テスト中に作成した一時的な添付オブジェクト(課題フォーム上の未保存添付、Wiki と共有ファイルの発見用の無害なテキストフィクスチャ)は、検証後に削除しました。課題フォーム上の未保存添付については、サーバー側のオブジェクトクリーンアップそのものは検証していません。

残課題

本 PoC は「完全なアップロード防止」ではなく、以下は明示的に未検証または未対応です。

  • 課題の実際の作成・編集、コメント投稿、Wiki 本文の保存までの end-to-end。現在の実スペース検証はいずれも送信前で止めてある
  • 実 UI の一般的な通信失敗が「拡張機能によってブロックされた」と誤帰属されないことの手動確認。通知ソースと無関係ルールの挙動は自動テスト側でカバー済み
  • 設定していない別の認証済み Backlog スペース。exact-origin の分離は自動テストと localhost での検証でカバー済み
  • ネイティブ OS のドラッグドロップ。実スペースでは著者の手動確認のみで、自動化側では独立に確認していない。ローカル demo の合成ドロップ検証はパス済み
  • ドキュメントのアップロード経路、追加のアップロード経路、Microsoft Edge 向けの別途検証

設定は、検証に使ったプロジェクトに限らず、設定したスペース全体に適用されます。今回の PoC はこの範囲での「うっかり防止」のためのツールです。

以上、Chrome 拡張機能 backlog-toolkit で Backlog の誤アップロードを UI と通信の両方で止める PoC を実装した、現場からお送りしました。

参考情報