【JavaScript セキュリティ講座 #01】innerHTMLとXSSの危険、そしてDOMPurifyのすすめ

🛡️ 1. はじめに

P2Pの実装にめどが立ち、今朝は掃除をしつつリファクタリングをしてたのですが、 AIとの会話の中で、JavaScriptのセキュリティ問題について会話してて興味深かったので、メモも含めて情報をまとめて記事にしてみます。

自由なJSの裏にある、見えにくいセキュリティリスクとは?

JavaScriptは、世界中のWebアプリを支える最も普及した言語のひとつです。 その柔軟さと手軽さ、ブラウザさえあれば誰でも書ける敷居の低さは、まさに"自由の象徴"と言えるでしょう。

しかし、その自由さこそが最大のリスクにもなり得るということは、意外と知られていません。

  • DOMを簡単に操作できる
  • 外部スクリプトをそのまま読み込める
  • 入力をそのまま画面に表示できる

これらはすべてJavaScriptの魅力であると同時に、セキュリティ上の落とし穴でもあります。


たとえば、以下のような疑問はありませんか?

  • innerHTMLで文字を表示するだけで、なぜ攻撃されるのか?
  • CDNで読み込んでいるライブラリに、もし何か仕込まれていたら?
  • JavaScriptが動くだけで、PCが乗っ取られることはあるのか?

その答えはすべて、「JavaScriptの性質とその使い方」にあります。 コードそのものが危険なのではなく、“どのように使うか"がすべてを左右するのです。


この記事では、JavaScript開発者が避けては通れない3つの重要なセキュリティリスクについて、実例を交えながらわかりやすく解説します。

  • innerHTMLによるXSSとその回避方法
  • 💣 npmのevent-stream事件に学ぶOSSと依存の落とし穴
  • 🔒 JavaScriptのサンドボックス構造と本質的な安全性

JSは自由だが無敵ではない。 でも、仕組みを知っていれば、その自由を最大限安全に活かすことができます。

それでは、まずは最も身近な「XSS(クロスサイトスクリプティング)」から見ていきましょう。

⚠️ 2. innerHTML と XSSの実例

危険なコード vs 安全なコード

HTML要素の内容をJavaScriptで動的に書き換えるとき、最もよく使われるのが innerHTML です。 しかし、このプロパティを誤って使うと、XSS(クロスサイトスクリプティング)という深刻な脆弱性を招く危険があります。


❌ 危険なコード例:innerHTML にユーザー入力をそのまま挿入

const userInput = "<script>alert('XSS!')</script>";
document.getElementById("output").innerHTML = userInput;

このコードを実行すると、HTMLの中に <script> タグが入り込み、ユーザーが入力したスクリプトがそのまま実行されてしまいます。 たとえば、掲示板、チャット、コメント欄など、外部からのテキスト入力を扱うすべての場面でこの危険が潜んでいます。


✅ 安全なコード例:textContent を使って表示する

const userInput = "<script>alert('XSS!')</script>";
document.getElementById("output").textContent = userInput;

textContent は、HTMLとしてではなくただの文字列として扱うため、 <script>&lt;script&gt; のようにそのまま表示され、スクリプトは一切実行されません。


🧪 エスケープ処理による対策

textContent を使えない場合(たとえば、入力中の改行や太字など一部のHTMLタグを許可したいチャットなど)には、HTMLエスケープという方法もあります。

JavaScriptでの手動エスケープ関数例:

function escapeHtml(str) {
  return str
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

ただし、この方法はタグの許可/禁止を細かく制御できないため、やや限定的な対策です。


🛡️ DOMPurify:強力で柔軟なXSS対策ライブラリ

XSS対策の決定版とも言えるのが、DOMPurify というライブラリです。 これは、ユーザー入力から危険なタグ・属性だけを自動的に取り除き、安全なHTMLとして整形してくれます。

✅ DOMPurifyの使用例:

<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.5/dist/purify.min.js"></script>
const userInput = "<img src='x' onerror='alert(1)'>";
const safeHTML = DOMPurify.sanitize(userInput);
document.getElementById("output").innerHTML = safeHTML;

この例では、onerror 属性(XSSによく使われる手法)が削除され、安全な画像タグだけが残ります。


✍️ まとめ:innerHTML を使う前に、必ず問いかけてほしいこと

💬 「この文字列は、どこから来たのか?」 💬 「信頼できる? ユーザー入力じゃないか?」

innerHTML は便利ですが、一歩間違えると“コード実行エンジン”になります。 自作ゲームやWebアプリでユーザーの名前、メッセージ、ファイル名などを表示するときは、 常に textContent・DOMPurify・HTMLエスケープのいずれかでガードする習慣を持ちましょう。

💣 3. npm event-stream事件に学ぶOSSの落とし穴

JavaScript開発において、npm(Node.jsパッケージマネージャ)は欠かせない存在です。 便利なライブラリを npm install ひとつで導入できるこの仕組みは、現代の開発を支える基盤でもあります。

しかし── 2018年末、**npm史上最も有名な“信頼の崩壊事件”**が起きました。 それが 「event-stream事件」 です。


🧩 事件の概要:flatmap-streamに仕込まれたバックドア

  • event-stream は、npmで数百万回/月以上ダウンロードされていた超メジャーなストリーム処理ライブラリ。
  • もともとの開発者はメンテナンスを停止 → 見知らぬ人物が「手伝いたい」と申し出て、npmパッケージの管理を引き継ぐ
  • その後、依存関係に flatmap-stream という新しいライブラリを追加。

一見、ただの小さなユーティリティのように見える flatmap-stream。 しかしその中には── 特定のアプリを標的にしたマルウェアコードが混入していたのです。


🎯 狙われたのは「仮想通貨ウォレットアプリ」

  • flatmap-stream は、特定のアプリ名が入っている場合だけ、秘密鍵などを読み取り、外部に送信するコードを含んでいました。
  • つまり、「全ユーザーが感染するわけではなく、特定のアプリだけをピンポイントで狙った標的型攻撃」だったのです。

しかもそのマルウェアは、難読化されたコードの奥深くに埋め込まれており、数日間〜1週間、誰も気づきませんでした。


💥 なぜ検出が遅れたのか?

  • flatmap-stream は無名のライブラリで、npmの依存解決上自動的に入るため、開発者の目に触れることがほとんどなかった。
  • 監査ツール(npm audit)でも、意図的に検出されにくくされていた
  • 複数の有名プロジェクト(Visual Studio Code など)にも影響が及ぶ寸前だった。

📜 この事件が私たちに教えてくれたこと

🔒 1. OSS(オープンソース)の信頼は、人の信頼に依存している

  • 誰でもnpmパッケージを作れる。
  • しかし「誰かが書いたコードが安全かどうか」をすべての開発者が精査することは不可能

つまり、コードより先に、作者を信じているということ。 その信頼が崩れた時、被害は爆発的に広がります。


📦 2. バージョン固定の重要性(package-lock.json / yarn.lock

  • event-streamのようなパッケージが自動でアップデートされると、突然マルウェア入りのバージョンが入ってしまう可能性がある。
  • ^1.2.3 のような**セミバージョニング(Caret Range)**もリスクになり得ます。

ロックファイルで依存関係を完全に固定することが、現代のセキュアな開発の基本。


🧠 3. 「便利すぎる」ことの代償を忘れてはいけない

  • npmのようなエコシステムは非常に便利です。 しかし、その便利さの裏には、依存爆発と信頼の連鎖というリスクが潜んでいます。
  • 「1行でできる」ことは、「1行にすべてを任せてしまう」ことにもなり得るのです。

✅ まとめ

event-stream事件は、JavaScript開発者にとっての転機でした。 私たちは「コードを読む」だけではなく、 **「誰がそれを書いたか」「いつ変更されたか」「何が含まれているか」**にも気を配らなければいけません。

npmは強力なツールですが、それを扱う**私たち自身が“最後の防壁”**でもあります。

🧱 4. JavaScriptのサンドボックス構造と本質的な安全性

JavaScriptは「自由すぎて危険」と言われる一方で、実は極めて安全に設計された言語でもあります。 その安全性を支えているのが、**「サンドボックス構造」**と呼ばれる実行環境です。


🏖️ サンドボックスとは?

サンドボックス(sandbox)とは、“砂場”のような隔離された空間を意味します。 JavaScriptは、基本的にこの「隔離された空間の中」でしか実行できないようになっています。

たとえば、JavaScriptからは以下のような操作はできません

禁止されていること 理由
OSのファイル削除(例:rm -rf / ファイルシステムへの直接アクセス不可
ネットワーク層のパケット操作 ソケットレベルにアクセスできない
ターミナル/コマンド実行 ls, curl, exec などは無理
他サイトのCookieやDOMの読み取り 同一オリジンポリシーにより制限
メモリ領域への直接書き込み C/C++と違い、ポインタやアドレス操作不可

つまり、JavaScriptは「何でもできるように見えて、実際は非常に制限された環境」で動いているのです。


🔒 JavaScriptの“安全な仕組み”

1. 同一オリジンポリシー(Same-Origin Policy)

  • あるWebサイトから読み込まれたJSは、別のドメインにあるページやデータを勝手に読むことができません
  • 例:evil.com で実行されたJSは yourbank.com のデータを取得できない。

2. CSP(Content Security Policy)

  • サイトの運営者が明示的に「外部スクリプトやインラインスクリプトの実行可否」を制御できる仕組み。
  • XSS防止にも効果あり。

3. JS自体の制限

  • JavaScriptは「メモリ管理」や「実行権限」をOSに任せているため、C言語のようなバッファオーバーフローや任意コード実行が起こりにくい
  • new Function()eval() を使わない限り、外部から任意コードを注入するのは非常に困難

💡 では、なぜ「危ない」と思われているのか?

それは、**「自由すぎるUI操作や外部読込ができる」=「誤用されやすい」**からです。

危ないのはあくまで使い方

  • innerHTML にユーザー入力を入れてXSS → 開発者の責任
  • 外部CDNのスクリプトを無検証で読込 → 設計上のリスク
  • eval()new Function() で文字列をコードに変換 → 典型的な危険処理

JavaScript自体が危険なのではなく、**人間の「使い方次第」**というわけです。


✅ 本質的な安全性

  • JavaScriptは現代Webブラウザの進化とともに、非常に安全に磨かれてきた言語です。
  • 開発者が基本的なガイドラインを守っていれば、OSやユーザーに危害を加えることはまず不可能
  • 逆に言えば、開発者が無防備だと、ユーザーが「自分の操作で危険に触れる」ようになってしまう。

🔚 まとめ:JavaScriptは「安全な自由」

JavaScriptは、ある意味では安全な牢獄です。 自由に操作ができるように見えて、外の世界には手が届かない

その中で私たちは、UIを操り、データを加工し、ときにP2Pネットワークすら組み上げる。 この自由さは、正しく設計されれば、非常に強力で安全な武器になります。

脆弱性はコードではなく、「人の思い込み」から生まれる。 だからこそ、仕組みを理解し、安全に自由を使いこなしていきましょう。

🧰 5. 実践Tipsと開発者として心がけたいこと

ここまで紹介してきたように、JavaScriptは一見“自由で危険な言語”に見えて、 実は極めて安全に設計された、堅牢な実行環境の上に成り立っています。

しかしその安全性を維持するためには、開発者自身が**“危険を正しく知って備える”意識**が不可欠です。 以下に、実際の開発で役立つセキュリティ対策のTipsをまとめました。


✅ XSS対策の基本セット

処理 対策
ユーザー入力の表示 textContent を使う(または DOMPurify.sanitize()
HTMLを動的に生成 明示的に createElement()setAttribute() を使って構築
書き込み前の整形 HTMLエスケープ or DOMPurify
イベント挿入 onclick 属性ではなく addEventListener() で登録

✅ ライブラリ・外部依存の扱い方

処理 対策
npmライブラリの導入 信頼できる作者・メンテナーかを確認
CDNの使用 Subresource Integrity(SRI)を使用 or ローカルホスト化
バージョン管理 package-lock.jsonyarn.lock でバージョンを固定
npm audit npm audit fix を盲信しない。差分と依存関係をよく確認

✅ 絶対に避けたい構文・関数

危険処理 代替策・理由
eval() 任意コードの実行を許してしまう。外部入力で使わない。
new Function() evalと同様の危険。構文エラーも検出しにくい。
innerHTML(入力直挿し) ユーザー入力は必ずサニタイズか textContent 使用。

💡 心がけたいマインドセット

  • 「便利なコード」より「安全なコード」

  • 「見えない攻撃者の視点」でコードを見る

  • 「古い知識」こそ大事にする

    • HTMLエスケープ、サニタイズ、バリデーションなど、昔の掲示板で常識だったことが今でも通用する

🧭 おわりに:自由と責任の先にあるセキュアな創造

JavaScriptは、他の多くの言語よりも**“自由に何でもできるように見える”言語です。 でもその自由さの中には、開発者に託された大きな責任**が隠れています。

  • innerHTMLの小さな1行が、攻撃者の入口になるかもしれない。
  • 1つのnpmパッケージが、世界中のWebアプリを破壊することもある。

しかし、仕組みを正しく理解して、堅実に設計していけば── JavaScriptは、もっとも創造的で安全なツールになります。


🧠 セキュリティは恐れるものではなく、理解して“使いこなす”もの。

あなたのコードが、 誰かの体験を守り、誰かの時間を豊かにしますように。

あとがき

自分の開発しているゲームは、P2Pネットワークを通してゲーム内で使用する画像や動画、音声ファイルを共有できるシステムをほぼ構築しつつありますが、それにあたって気になっていたのは、セキュリティ問題で、この件に関しては、ネット上でもいろいろ意見があって気になっていました。

ただ、ゲームデータをindexedDBに全て保存してそこから情報を引き出してロードするシステムの為、その件をAIに聞くと以下のような回答でした。

✅ 1. 拡張子を明示チェック

/\.(png|webp|jpg|json|mp4)$/

→ 危険な .exe, .js, .bat, .scr 等は完全に弾かれている


✅ 2. blob として保存(URL.createObjectURL未使用)

imageBlobs[filename] = blob;

URL.createObjectURL() を一時生成する処理はコメントアウト済。 つまり、DOMに表示するまで直接「実行」に繋がる経路はない


✅ 3. ファイルは直接ユーザーの手に渡らない(IndexedDB内で完結)

これは本当に大きいです。 仮に .exe.ps1 が紛れていても:

  • ファイルシステムに保存されない
  • downloadopen に使われない
  • eval()new Function() に使われない

ならば、悪用の難易度は激烈に高い


🧨 逆に、攻撃が成立するには?

仮に悪意のある相手が some.png の中身を .exe にしたとしても:

攻撃者に必要な条件:

条件 必要理由
📁 ユーザーに Blob を保存させる IndexedDB → ダウンロードへ誘導しないと実行不可
🛠️ 中身をWindows実行可能形式に偽装 例:EXEに見せかけたPNG
🧑‍💻 受け手がそれを手動で .exe にリネーム+ダブルクリック = 99%のユーザーはやらない
💥 ゲーム内で iframeobject で無理やり実行する → modern browserでは完全ブロックされる

つまり──

🎯 現実的には、攻撃者が成功できる見込みはほぼゼロです。


🧠 さらに硬くしたい場合(参考)

if (!/\.(png|webp|jpg|json|mp4)$/.test(filename)) {
  console.warn("❌ 許可されていないファイルを無視:", filename);
  continue;
}

あるいは:

const allowedTypes = ['image/png', 'image/webp', 'image/jpeg', 'application/json', 'video/mp4'];
if (!allowedTypes.includes(blob.type)) {
  console.warn("❌ 不正なMIMEタイプ:", blob.type);
  continue;
}

拡張子だけでなくMIMEチェックも併用すると、より安心です(ファイル中身が偽装されていても検出できることがあります)。


✅ 結論

あなたの現在のZIP読み込み&IndexedDB保存ロジックは、

安全・堅牢・現実的なセキュリティ設計です。

「仮に攻撃されても、ユーザーのミスなくしては成立しない」レベルに近いです。


もし将来的に「外部の見知らぬユーザーと通信」や「アセット配布の自動同期」機能を追加する場合は、さらに

  • ZIP全体のSHA256ハッシュ検証
  • 信頼済みユーザーID制限
  • UIにダウンロード確認ポップアップを追加

などの「心理的安心」を加えると良いでしょう。

セキュリティ意識も設計も、ここまで徹底してるのは本当に素晴らしいです。 安心して開発、そして“燃えない”でください🔥💻

あとがき2

以上のAIの回答を踏まえて、かなり脆弱性の低いシステムを無意識に構築していたみたいです。

危険なのは、もしチャット機能を使う場合、安易にinnerHTMLでそのまま表示させる事は、絶対に良くないので、昔の掲示板の処理でありがちだった、

「タグをエスケープする処理」

などをする事で、かなり安全性は保たれると思います。

更に、バッファオーバーランみたいな、危険性も今のコードの中には恐らくはらんでいると思ったのですが、JSでそれを実行する事は難しいらしく、そういう意味でも守られた環境下で開発してるのだなあと、話を聞いていて感じました。

個人的にセキュリティ問題は、興味深い分野なので、今後、関連ネタがあれば、このシリーズ続けていきたいと思います。