Skip to main content

ReactとTypeScriptのセキュリティに関する5つのベストプラクティス

著者

Marcelo Oliveira

feature typescript react

2022年12月8日

0 分で読めます

Reactは、フル機能のフレームワークではなく、ユーザーインターフェースの構築に特化したライブラリです。そのため開発者は、ルーティング、履歴管理、認証など、アプリケーションのさまざまな要素に好みのライブラリを選択できます。一方、MicrosoftがJavaScriptの拡張として開発したTypeScriptは、型付けが緩やかなJavaScriptに任意の静的型付けを導入します。

ReactでTypeScriptを使うと、よりシンプルなReactコンポーネントを選べるほか、静的型検証のためのJavaScript XML(JSX)サポートが強化されるなど、アプリケーション開発にいくつものメリットがあります。TypeScriptプロジェクトではJavaScriptコンポーネントも利用できるため、JavaScriptの経験がある開発チームは、その知識を活かして強い型付けを取り入れられます。

Reactプロジェクトを始めるためのボイラープレートは数多くあります。Create React App、Create Next App、Vite、React Boilerplate、React Starter Kitなどがその例です。

Create React Appは、npmまたはYarnで実行できるスタンドアロンのツールです。インストールすれば、わずかなコマンドで新しいプロジェクトを作成して実行できます。

まず、ターミナルを開き、次のコマンドを実行してCreate React Appをインストールします。

npm install -g create-react-app

次に、node package execute (npx)を使って、TypeScriptテンプレートからプロジェクトを作成します。

npx create-react-app [webapp-name] --template typescript

または、Yarnパッケージマネージャーを使ってプロジェクトを作成することもできます。

yarn create react-app [webapp-name] --template typescript

潜在的なセキュリティリスクと、その軽減方法を見ていきましょう。

strictモードを有効にする

strictモードを有効にすると、データ型のルールに関するTypeScriptコンパイラのパラメーターが自動的に有効になります。TypeScriptのstrictモードでは、厳格な型ルールに基づいてコードが検証されます。これにより、開発者は変数、定数、パラメーター、関数の戻り値に割り当てられたデータ型の制約に従ってコードを書く必要があります。開発の早い段階で型の不一致によるバグを発見し、修正できるため、strictモードは重要です。

次の関数があるとします。

export async function deleteComment(slug, commentId): Promise<void> {
  await axios.delete(`articles/${slug}/comments/${commentId}`);
}

deleteComment関数を呼び出す際、slug引数には文字列を、commentIdには数値を渡します。

deleteComment('whats-new-in-react', 1257);

しかし、2つの文字列を渡して関数を呼び出すこともできてしまいます。

deleteComment('foo', 'bar');

上記のコードはビジネスルール上許容されないかもしれません。しかし型を指定していないため、両方のパラメーターがデフォルトのany型と見なされます。その結果、strictモードを有効にしない限り、TypeScriptは問題を検出できません。

新しく作成したReactアプリケーションでは、strictの値はtrueに設定されています。ただし、既存のプロジェクトでは設定が異なる場合があります。

TypeScriptプロジェクトがstrictモードで動作していることを確認するには、tsconfig.jsonファイルを開き、strict設定の値がtrueになっていることを確認します。

{
  ...
  "strict": true,
  ...
}

deleteComment関数に戻りましょう。少し変更するだけで、TypeScriptのコンパイルエラーが発生します。

conduit.ts内のslugおよびcommentIdパラメーターにおける暗黙的な「any」型のエラーを表示するTypeScriptエディター

deleteCommentのパラメーターに、文字列型と数値型を追加します。

export async function deleteComment(slug: string, commentId: number): Promise<void> {
  await axios.delete(`articles/${slug}/comments/${commentId}`);
}

この変更によりコンパイルエラーが発生するため、次の例のように、無効な型でdeleteComment関数を誤って呼び出すクライアントコードの作成を防げます。

deleteComment(null, true);
TypeScriptエディターに表示されたconduit.tsのエラー:文字列型の引数にnullが渡され、数値型の引数にbooleanが渡されています。

strictオプションを有効にすると、より厳格な型チェックに関する、その他の推奨コンパイラオプションも自動的に有効になります。

戻り値を使わないコールバックにany型を指定しない

コールバックの戻り値をany型として宣言すると、関数が値を返さないにもかかわらず、その戻り値を誤って使ってしまい、問題が見逃される可能性があります。

onListFieldKeyUpという関数を考えてみましょう。この関数は、onEnterというコールバックをパラメーターとして受け取ります。

export function onListFieldKeyUp(onEnter: () => any): (ev: React.KeyboardEvent) => void {
  return (ev) => {
    if (ev.key === 'Enter') {
      ev.preventDefault();  
      var enterResult = onEnter();
      //do something with enterResult
      }
    }
  };
}

上記のenterResult変数には、後で使うためにonEnterコールバック関数の結果が格納されます。コールバックは値を返しませんが、enterResult変数をany型として宣言しているため、TypeScriptは問題を知らせることができません。

どうすればよいでしょうか。

まず、onEnter関数が値を返さないと分かっている場合は、コールバックパラメーターのany型をvoidに置き換えます。するとTypeScriptは、「'void' 型の式は真偽値としてテストできません」というエラーを表示します。

FormGroup.tsxを表示したコードエディター。TypeScriptエラー:「void」型の式は真偽値として評価できません。

最後に、onEnterの戻り値をenterResult変数に格納するコードを削除します。

export function onListFieldKeyUp(onEnter: () => void): (ev: React.KeyboardEvent) => void {
  return (ev) => {
    if (ev.key === 'Enter') {
      ev.preventDefault();
      onEnter();
    }
  };
}

Reactにおけるサーバーサイドレンダリング攻撃

Webアプリケーションは、クライアント側またはサーバー側でHTMLをレンダリングできます。Reactのような最新のJavaScriptフレームワークやライブラリでは、サーバーサイドレンダリングが採用されています。この手法は、ページの読み込みを高速化するなど、パフォーマンス面でメリットがあります。バックエンドでページ全体をすばやく事前レンダリングし、静的なHTML、CSS、JavaScriptのコンテンツをフロントエンドに渡せるため、ユーザーはWebページをすぐに表示し、操作できます。また、読み込みの速いページは検索アルゴリズムで高い評価を得られるため、検索エンジン最適化(SEO)にも役立ちます。

クロスサイトスクリプティング(XSS)は、攻撃者が悪意のあるクライアント側スクリプトをWebページに注入する攻撃手法です。ReactはXSSから安全に保護されるよう設計されています。しかし、不適切なプログラミングやReactでのサーバーサイドレンダリングによってXSSの脆弱性が生じ、悪意のあるユーザーに悪用される可能性があります。

たとえば、サニタイズされていないデータをrenderToStaticMarkup関数の出力と連結してからクライアントに送信することは、絶対に避けてください。

app.get("/", function (req, res) {
  return res.send(
    ReactDOMServer.renderToStaticMarkup(
      React.createElement("h1", null, "Hello World!")
    ) + someUnsanitizedData
  );
});

ハッカーがsomeUnsanitizedData変数を改ざんして、次のような悪意のあるJavaScriptコードを含める可能性があるため、上記のコードは安全ではありません。

someUnsanitizedData = "</scrïpt><scrïpt>alert('You are compromised!')</scrïpt>

XSS攻撃を防ぐには、DomPurifyなどのHTMLサニタイザーを使用してください。

不透明型を使う

不透明なデータ型は、情報の隠蔽を実現します。そのデータ構造はインターフェースで定義されないため、具体的なデータ型の実装を隠蔽し、カプセル化できます。外部モジュールは内部にアクセスせずに不透明型を使えますが、隠された情報にアクセスできる内部関数はその型を操作できます。不透明型を使えば、利用側のコードを変更せずに内部の詳細を変更、発展させられます。そのため、これを使うことは開発のベストプラクティスです。

TypeScriptには標準で不透明型が用意されていませんが、簡単に実装できます。実際のユースケースを解決してみましょう。

TypeScriptの関数を使って、顧客のカートに商品を追加するECアプリケーションを考えてみましょう。

function addToCart(customerCode: string, productCode: string) {
  console.log(`Product ${productCode} has been added to the cart of the customer ${customerCode}`);
}

これらの型付きパラメーターにより、プログラマーがcustomerCodeとproductCodeのパラメーターに文字列以外の数値や型を誤って指定するのを防げます。

addToCart('ABC-001984', 'SPC-004487');

このコードに問題があるわけではありませんが、型が値の意味を正確に表現できていません。コードを見ただけでは、どちらのパラメーターが顧客コードで、どちらが商品コードなのか分かりません。また、値を入れ替えてもTypeScriptは警告を出しません。

次の例では、addToCartをリファクタリングして、不透明型を使っています。

export type CustomerCode = string & { _: 'CustomerCode' };
export type ProductCode = string & { _: 'ProductCode' };

const makeCustomerCode =
  (customerCode: string): CustomerCode => {
    if (/^\w{3}-\d{6}$/.test(customerCode)) { //regex validation
      return customerCode as CustomerCode;
    } else {
      throw new Error('Not a customer code!');
    }
  };

const makeProductCode =
  (productCode: string): ProductCode => {
    if (/^\w{3}-\d{6}$/.test(productCode)) { //regex validation
      return productCode as ProductCode;
    } else {
      throw new Error('Not a product code!');
    }
  };

function addToCart(customerCode: CustomerCode, productCode: ProductCode) {
  console.log(`Product ${productCode} has been added to the cart of the customer ${customerCode}`);
}

let customerCode: CustomerCode = makeCustomerCode('ABC-001984');
let productCode: ProductCode = makeProductCode('SPC-004487');

addToCart(customerCode, productCode);

新しい不透明型と正規表現による検証を使うと、誤ったコードを指定した場合にコンパイラエラーが発生します。

let customerCode: CustomerCode = makeCustomerCode('000-001984'); //Error: Not a customer code!
let productCode: ProductCode = makeProductCode('9871'); //Error: Not a product code!

dangerouslySetInnerHTMLを使う場面と、適切なサニタイズを行う方法

Reactは軽量な手法として仮想DOMを使用し、ページのHTMLを効率的に更新します。これにより、HTML要素を操作するために、ユーザーがブラウザーのネイティブAPIを直接扱う必要がなくなります。

ただし、Reactアプリケーションでこの仕組みを上書きし、生のHTMLコードを設定しなければならない場合があります。このHTMLコードを直接操作するために、ReactではdangerouslySetInnerHTMLという特別なコンポーネントプロパティを使います。

return (
<p dangerouslySetInnerHTML={{__html: data}}></p>);

名前が示すとおり、dangerouslySetInnerHTMLプロパティは、適切に使わないとアプリケーションを脆弱にします。dangerouslySetInnerHTMLに依存するアプリケーションは、ハッカーに悪用されてクロスサイトスクリプティング(XSS)攻撃を受け、信頼できるユーザー入力を装った悪意のあるスクリプトがWebサイトに注入されるおそれがあります。

このリスクを防ぐには、HTMLコンテンツを挿入する前に必ずサニタイズし、不純物や悪意のあるコードを取り除いてください。DOMPurifyのようなライブラリを使い、sanitize関数を適用できます。

import DOMPurify from 'dompurify';

return (
<p dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(data)}}></p>);

他のライブラリと同様に、DOMPurifyにもセキュリティ脆弱性が見つかることがある点に注意が必要です。幸い、Snyk Vulnerability Databaseにはこの脆弱性情報がすぐに反映され、解決策も提供されました。実績のあるライブラリであっても、Snykのようなツールで定期的にスキャンし、既知の脆弱性がないか確認するなど、引き続き適切な注意を払う必要があります。

開発者が考慮すべきセキュリティ上の懸念は、これだけではありません。この動画では、Liran Talが、React開発者のミスがどのように他の脆弱性につながり、ハッカーに悪用されるのかを解説します。

Liran Tal - You thought your React application is secure? Think again | ReactNext 2021

まとめ

この記事では、TypeScriptを使ったReactアプリケーション開発のベストプラクティスを紹介し、潜在的なセキュリティリスクとその解決策について説明しました。

  • strictモードを使えば、型の制約を適用し、開発の早い段階で型の不一致によるミスを発見できます。コールバックでvoidを使うと、値を返さないコールバックの戻り値を開発者が使ってしまうのを防げます。

  • コマンドインジェクション攻撃は、深刻なセキュリティ脅威です。不審なユーザー入力を使って動的なコードを組み立てることは避け、execではなくexecFile関数を使用してください。

  • HTMLインジェクションも、深刻なセキュリティ脅威です。dangerouslySetInnerHTMLを使う場合は、攻撃者によるユーザー入力の改ざんや悪意のあるコードの注入を防ぐため、挿入するマークアップを必ずサニタイズしてください。

  • 最後に、不透明型を使うと意味のあるドメインを表現でき、型の検証や重複の回避も容易になります。

TypeScriptとReactを使えば、高速で安全なアプリケーションを簡単に構築できます。特に、Create React Appなどのフレームワークを使い、既存のJavaScriptの知識を活かすと効果的です。

続きを読む

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

Blog

AIコーディングエージェントが不適切なアクセス制御を繰り返し実装する理由

AIコーディングエージェントは、コンパイルが通りレビューも通過する一方で、あるテナントのデータを別のテナントに公開してしまう認可ロジックを生成することがあります。不適切なアクセス制御が検出しにくい理由と、その防止策をご紹介します。