[JavaScript講座] セキュリティの基本

当ページのリンクには広告が含まれています。
目次

セキュリティの基本スタンス

「入力はすべて疑え」

  • URLパラメータ
  • フォーム入力値
  • LocalStorage に保存された値
  • Cookie
  • 外部APIからのレスポンス

これらは全部「信頼できない入力」 とみなします。

「この画面は自分しか使わないし」
「この値は自分のコードが書き込んだだけだし」

と思っていても、

  • 将来仕様が変わる
  • 別の人が別の場所から書き込む
  • 攻撃者が無理やりパラメータを差し込む

ことは普通に起こりえます。

「ブラウザ側だけで防御しない」

  • JSでのバリデーションはユーザ体験向上用(早めのフィードバック)の意味合いが強い
  • 最終防衛線は必ずサーバ側 で行う(Node.jsならサーバコード側)

フロントエンド:
⇒ 入力ミスを早めに教える、軽いチェック

サーバ:
⇒ 信頼できない入力を最後にチェック・フィルタ・サニタイズ

両方大事ですが、本物の防御は常にサーバ側と覚えておくと安全です。

XSS(クロスサイトスクリプティング)の基本

XSS とは何か

ざっくりいうと、「攻撃者が仕込んだスクリプトを、被害者のブラウザ上で実行させる攻撃」です。

よくあるイメージ:

  1. アプリが「ユーザーの入力をそのままHTMLに差し込む」
  2. 攻撃者が <script>alert("XSS")</script> みたいな文字列を送る
  3. それが画面に差し込まれたとき、HTMLとして解釈されてJSが実行される

結果としてできること:

  • Cookieを盗む
  • フォームに勝手に別の入力欄を混ぜる
  • 勝手にAPI叩かせる(CSRFと組み合わせなど)

典型的な危険コード(innerHTML

// NG例:ユーザー入力をそのままinnerHTMLに流し込む
const comment = getUserInput(); // 攻撃者が自由に操作できるとする
const div = document.querySelector("#comment");
div.innerHTML = `<p>${comment}</p>`;

攻撃者が comment に次を入れるとします:

<script>alert("XSS");</script>

→ 結果的に DOM は:

<div id="comment">
  <p><script>alert("XSS");</script></p>
</div>

となり、alert が実行されてしまいます。

ポイント:

  • innerHTML は「文字列をHTMLとして解釈する」
    <script><img onerror=...> などもそのまま有効になる

基本防御:textContent を使う

単に「テキストとして表示したいだけ」の場合は、

div.textContent = comment; // ← こちらを使う

とします。

  • textContent は、文字列をエスケープして表示してくれるイメージ
  • < > などの記号はすべて普通の文字として扱われ、タグにならない

つまり、同じ攻撃文字列:

<script>alert("XSS");</script>

textContent で入れると、画面上には:

<script>alert("XSS");</script>

と「ただの文字列」として表示されるだけで、スクリプトは実行されません。

どうしてもHTMLを差し込みたい場合

たとえば Markdown をHTMLに変換してレンダリングするなど、ある程度制御されたHTMLを描画したいケースもあります。

この場合は:

  1. 信頼できない入力は、
    • サーバ側で検査・サニタイズする
    • あるいはフロントでも、サニタイズ用ライブラリ(DOMPurify など)を必ず通す
  2. innerHTML を直接信用しない
    • 極力 document.createElementappend などでDOMノードを組み立てる
    • 必要ならライブラリ側に任せる

といったアプローチが有効です。

属性値への差し込みも危険

const name = getUserInput();
const a = document.createElement("a");
a.href = `/user?name=${name}`; // ここに "aaa" onclick="..." みたいな文字列を入れられると?

属性値の中に " とか ' が入ってくると、
属性境界を壊して任意の属性を追加される可能性があります。

基本方針:

  • できるだけ element.setAttribute("name", safeValue) を使う
  • URLなどは encodeURIComponent でエンコードしてから埋め込む
  • href="javascript:..." のようなパターンを弾く

DOMベースXSS というパターン

最近多いのが、DOM操作だけで発生する XSS です。

例:

  • JS が location.hash から文字列を取り出し、
  • それを innerHTMLdocument.write などでDOMに書き込む
https://example.com/#<script>...</script>

のように URL に仕込むだけで攻撃可能になってしまうケースもあります。

「どこから来た文字列なのか」を意識して、

  • URL 由来
  • localStorage由来
  • サーバレスポンス由来

などは常に「信頼できない入力」として扱うのが安全です。

eval / Function コンストラクタの危険性

eval は何をする関数か

eval("alert('hi')");
  • 文字列を「そのまま」JavaScriptコードとして評価・実行します。
  • つまり、攻撃者に“好きなコードを実行させる入り口”を渡すことになります。
const codeFromUser = getUserInput(); // 攻撃者からの文字列
eval(codeFromUser); // ← ここで何でも実行できてしまう

XSSと組み合わさると、

  • <script>eval(location.hash.slice(1))</script> みたいなコードがあり、
  • URLの後ろに #alert(document.cookie) と付けられるだけでCookie窃取される、

といった「完全終了パターン」になりがちです。

Function コンストラクタも本質的には eval

const f = new Function("a", "b", "return a + b;");
console.log(f(1, 2)); // 3
  • これも「文字列から関数を生成する」=ほぼeval です。
  • サーバから取ってきた文字列やユーザー入力を渡すと、そのまま任意コード実行の入り口になります。

同様に、

  • setTimeout("doSomething()", 1000);
  • setInterval("doSomething()", 1000);

も、内部的には eval 的に評価されます。
コールバックは必ず関数で渡すのが基本です:

setTimeout(doSomething, 1000); // OK

パフォーマンス的にもデメリット

  • eval / Function
    • JSエンジンの最適化(JITコンパイル)を阻害しやすい
    • 静的解析ツール(ESLintや型チェッカ)が内容を追えない
  • つまり「遅くて危険で、ツールにも優しくない」三重苦。

特別な理由がない限り、使用禁止ルールにしてしまって良いレベルです。

代替手段の考え方

「evalを使いたくなる」例と、その代替:

  1. 文字列で渡された式を計算したい
    ⇒ NG: const expr = "1 + 2 * 3"; const result = eval(expr);
    代替手段:
    • 専用のパーサライブラリを使う(mathjs など)
    • そもそも文字列式を受け取る仕様をやめる
  2. APIレスポンスに「この関数を実行して」と書きたくなる
    ⇒ NG: // サーバから "doFoo" みたいな文字列が返ってきて… const fnName = response.fn; eval(`${fnName}()`);
    代替手段:
    const actions = { doFoo() { /* ... */ }, doBar() { /* ... */ } };
    const fnName = response.fn;
    const action = actions[fnName];
    if (action) action();
     ⇒ディスパッチテーブル(名前→関数のマップ)を使えば安全に切り替えられます。

型チェック・入力バリデーションの重要性

ここは「仕様を満たすための防御」と「セキュリティとしての防御」が両方絡みます。

「値の形」をハッキリ決める

例:フォームで年齢を入力させる場合

  • 期待するもの:
    • 数値
    • 0〜120の整数
  • 実際に来るかもしれないもの:
    • "abc"
    • "-100"
    • "9999999999"
    • "<script>...</script>"

型チェックの例

function parseAge(input) {
  const n = Number(input); // or parseInt
  if (!Number.isInteger(n)) {
    throw new Error("年齢は整数で入力してください");
  }
  if (n < 0 || n > 120) {
    throw new Error("年齢は0〜120の範囲で入力してください");
  }
  return n;
}
  • 数値変換(Number, parseInt
  • Number.isFinite / Number.isInteger / 範囲チェック

などを組み合わせて、「不正な値は弾く」ことが大事です。

Node側(サーバ)では必須。
フロント側ではエラーメッセージをユーザーに返す役割。

ホワイトリスト方式が基本

const color = getUserInput(); // 文字列

// NG: 「なんでも許してあとで頑張ってフィルタ」
element.style.color = color;

// OK: あらかじめ許可する値を限定する(ホワイトリスト)
const allowedColors = ["red", "green", "blue"];
if (!allowedColors.includes(color)) {
  throw new Error("指定できない色です");
}
element.style.color = color;
  • 許可するパターンを限定(ホワイトリスト)する考え方
  • 「禁止するパターン(ブラックリスト)を頑張って列挙する」のはキリがなく、抜け穴が出がち

型システム(TypeScriptなど)の役割

TypeScriptなどの静的型付けは:

  • 開発時 に「ありえない型の組み合わせ」を検出
  • 「ここで null かもしれないよ」「ここで string じゃないよ」と教えてくれる

ただし注意点があります:

  • 型チェックはコンパイル時の話
  • 実行時にはユーザーからどんな値が来るか分からない

「サーバ側では ランタイムでの型チェック/バリデーション が依然として必要」という点を忘れないようにするのがポイントです。


<<前へ(パフォーマンスとメモリ)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次