Skip to main content

Expressを使ったNode.jsアプリケーションでアクセス制御の不備を防ぐ

著者
Headshot of Ben Smitthimedhin

Ben Smitthimedhin

blog feature pypi spoof

2024年5月22日

0 分で読めます

Expressフレームワークで構築されたWebアプリケーションでは、バックエンドのNode.jsアプリケーションにおけるアクセス制御が不可欠です。アクセス制御により、ユーザーは利用を許可されたデータや機能にのみアクセスできます。しかし、アクセス制御に不備があると、ユーザーが本来アクセスできないデータにアクセスできてしまいます。攻撃者が機密データの改ざんや窃取を試みる場合、特に深刻な問題となります。

個人データの漏えいは多くの企業に甚大な被害をもたらすため、アクセス制御の脆弱性を理解し、発生を防ぐことが重要です。この記事では、Node.jsアプリケーションにおけるアクセス制御の不備と、Expressフレームワークを基盤とするWebアプリケーションの構築時に、こうした脆弱性を防ぐ方法について解説します。

アクセス制御の不備とは?

アクセス制御とは、アプリケーション内でユーザーが実行・閲覧できる内容を制限することです。アクセス制御の不備は、アプリケーションにおけるアクセス制御ルールの実装に問題があり、権限のないユーザーが機密情報にアクセスしたり、制限された操作を実行したりできる状態を指します。

たとえば、アクセス制御の不備によって一般ユーザーが他のユーザーの個人データを閲覧したり、そのユーザーのパスワードを変更したり、さらには管理者レベルまで権限を昇格したりできる場合、データ漏えいから不正なシステム変更まで、壊滅的な結果を招くおそれがあります。

次のセクションでは、さまざまな種類のアクセス制御の脆弱性を示すコード例を見ていきます。

Expressのミドルウェアにおける垂直アクセス制御の不備

アクセス制御の脆弱性の一種に、垂直アクセス制御の不具合や弱点があります。これは、本来アクセス権限がない一般ユーザーが、管理者レベルの機能やその他の権限を取得できてしまう状態です。その結果、一般ユーザーが権限の階層を上がり、本来できないはずの変更や削除など、管理者のような操作を実行できてしまいます。

垂直アクセス制御の不備につながる具体的なコードの脆弱性をいくつか紹介します。

Expressのルートにある保護されていない管理者パネル

Webページやサーバー上の管理者向け機能につながるルートが、何らかの認可によって保護されていない場合、管理者パネルは保護されていない状態になります。

// The express server right below and the `app.listen()` will not appear in other code snippets but // is assumed to be present
const express = require('express');
const app = express();

app.get('/admin', (req, res) => {
  res.send('Welcome to the Admin Panel!')
});

app.post('login', (req, res) => {
// code relating to logging in
}

app.listen(3000, () => {
  console.log(`Server is running on port 3000`);
});

この例では、ルート/adminにGETリクエストを送ったユーザーは、ログイン済みかどうか、必要な権限を持っているかどうかにかかわらず、自動的にWelcome to the Admin Panel!というメッセージにアクセスできます。本来連携させるべき/adminパネルと/loginルートが切り離されているため、アクセス制御の不備にあたります。

クエリパラメーターを使ったExpressの管理者パネル

URLパラメーターを使った管理者パネルは簡単に実装できますが、検証を行わないと簡単に操作されてしまいます。

app.get('/admin', (req, res) => {
  if (req.query.isAdmin) {
    res.send('Admin panel');
  } else {
    res.send('Access denied');
  }
});

この例では、/adminルートに直接GETリクエストを送ると、ユーザーにはAccess deniedという応答が返されます。この拒否を回避するには、URLに?isAdmin=trueを追加するだけです。管理者パネルにアクセスするユーザーに関係なく、これらのパラメーターを追加できるため、最初の例と同様に、一般ユーザーが管理者権限を得られる脆弱性が生じます。

難読化によるExpressルートのアクセス制御

理論上は、次のExpressルートミドルウェアのように、推測しにくいルートを設定して管理者パネルのルートを隠すことができます。

app.get('/897928394834023-12', (req, res) => {
  res.send('Admin Panel')
})

一見すると管理者パネルの安全性が高まるように思えますが、これはアプリケーションを保護する安全な方法ではなく、誤った方法です。攻撃者はサイトマップに侵入し、Webサイトに存在するすべてのエンドポイントを明らかにできます。また、ファジング手法を使ってあらゆるエンドポイントを試し、返される応答を確認することも、ログファイルから管理者パネルにアクセスできるURLを探すこともできます。巧妙に見えるこの方法では、実質的なセキュリティ対策にはならず、アクセス制御の不備を招きます。

Expressアプリケーションにおけるログへの平文記録

開発者は、アプリケーションのユーザーに重要な情報が意図せず漏えいしないよう、ログの仕組みにも注意する必要があります。たとえば、ログデータからパスワードを除外せず、機密情報を含むログイン操作の記録をアプリケーションが作成する場合があります。

// Dummy user data
const users = [
  {email: 'logan@test.com', password: '1234', role: 'user', id: '1'},
  {email: 'megan@test.com', password: '1234', role: 'admin', id: '2' },
];

app.post('/login', (req, res) => {
  const user = users.find((u) => u.email === req.body.email);
  if (user.role === 'admin') {
    console.log(user.email + ' logged in as Admin with password:' + user.password)
  }
})

末尾にあるconsole.logの記述に注目してください。ログファイルにアクセスできる人は、ユーザーデータにもアクセスできるため、管理者権限を持つユーザーになりすましてログインできてしまいます。このため、状況に応じて必要な情報だけを記録することが重要です。また、侵害が発生した際にハッカーがデータを悪用できるような重要情報を、ログに記録しないようにすることも大切です。

Expressのミドルウェアにおける水平アクセス制御の不備

垂直アクセス制御の問題では、ユーザーが権限を昇格できる状況が生じます。一方、水平アクセス制御の問題は、同じ権限レベルにある他のユーザーのリソースやデータにアクセスできてしまう状態です。

Node.jsのExpressフレームワークで発生する水平アクセス制御の脆弱性について、具体的なコード例を見ていきましょう。

リクエストパラメーターのユーザーIDをExpressルートに渡す

リクエストパラメーターにユーザーIDを表示し、その同じIDを使ってユーザーデータにアクセスできるアプリケーションには問題が生じるおそれがあります。つまり、他人のIDを知っているだけで、その人のデータにアクセスできてしまいます。

たとえば、自分のIDが123の場合、Webサイトでアカウント情報にアクセスするには、www.[website name].com/account?userId=123に移動します。

// Dummy user data
const users = [
  {email: 'logan@test.com', role: 'user', id: 1, accountData: 'secret!'},
  {email: 'megan@test.com', role: user, id: 2, accountData: 'secret two!' },
];

app.get('/account', (req, res) => {
  const userId = req.query.userId; // Get the user ID from the request parameter
  if (userId) {
    const user = users.find((u) => u.id === Number(userId));
    if (user) {
      res.send(`Account information for ${user.username}: ${user.accountData}`);
    } else {
      res.send('User not found');
    }
  } else {
    res.send('User ID not provided');
  }
});

この例では、クエリuserId=1を指定して/accountにGETリクエストを送ると、別のユーザーとしてログインしていても、logan@test.comのaccountDataオブジェクトを簡単に取得できます。同様に、クエリuserId=2を指定してGETリクエストを送れば、パラメーターにユーザーIDを指定するだけでmegan@test.comのaccountDataオブジェクトを取得できます。このように、クエリの結果としてaccountDataオブジェクトが返されます。

このセキュリティ脆弱性は、一般に安全でない直接オブジェクト参照(IDOR)と呼ばれ、JavaやRubyなどのプログラミング言語でも見られます。

予測困難なユーザーIDをリクエストパラメーターからExpressルートミドルウェアに渡す

前の例を踏まえると、適切なアクセス制御とはユーザーIDを難読化することだと考えるかもしれません。たとえば、IDが1のユーザーは、ハッシュ化やエンコードなどの加工によって、クエリ内では8a76dh3tとなり、他人のユーザーデータへのアクセスが難しくなります。

// Dummy user data
const users = [
  {email: 'logan@test.com', role: 'user', id: 1, accountData: 'secret!'},
  {email: 'megan@test.com', role: user, id: 2, accountData: 'secret two!' },
];

app.get('/account', (req, res) => {
  const obfuscatedId = req.query.id; // Get the obfuscated user ID from the request parameter
  if (obfuscatedId) {
    const user = users.find((u) => obfuscateUserId(u.id) === obfuscatedId);
    if (user) {
      res.send(`Account information for ${user.email}: ${user.accountData}`);
    } else {
      res.send('User not found');
    }
  } else {
    res.send('User ID not provided');
  }
});

// Obfuscation function
const obfuscateUserId = (userId) => {
  // custom obfuscation logic here
  return obfuscatedId;
}

この例では、obfuscateUserId関数が既存ユーザーの一覧からユーザーIDを取得し、難読化します。そのため、/accountルートにGETリクエストを送ってアカウント情報を取得するには、リクエストパラメーターに難読化されたID(例:userId=8a76dh3t)を指定する必要があります。

この方法は実効性のあるセキュリティ対策ではなく、アクセス制御の不備を温存するだけです。時間をかけて粘り強く試せば、ハッカーはファジングによって適切な難読化IDを推測し、侵入できます。結果として、またしてもアクセス制御の不備が残ります。

ExpressアプリケーションのCSRF対策の不備を攻撃に利用する

最後に、悪意のある攻撃者がExpressアプリケーションのアクセス制御の不備を悪用する一般的な方法として、クロスサイトリクエストフォージェリ(CSRF)攻撃があります。簡単に言うと、偽のWebページやリンクを作成し、ユーザーをだまして情報を送信させる攻撃です。その情報は、実際のWebサイトに正規のリクエストを送信する機能に組み込まれ、攻撃者がデータにアクセスできるようになります。

CSRF対策を実装しなければ、確認したサーバーアプリケーションの例はいずれも、ユーザーが情報を入力して送信する偽のフォームに組み込まれ、悪意のある第三者に悪用されるおそれがあります。

Node.jsでアクセス制御の脆弱性を防ぐ方法

垂直・水平アクセス制御の脆弱性の例を確認したところで、次は、それらを防ぐための一般的な対策を見ていきましょう。

ライブラリを最新バージョンに保つ

アクセス制御を高い基準で実装する方法の一つは、広く信頼され、適切に保守・保護されているnpmパッケージを使うことです。ただし、Node.jsとそのパッケージエコシステムでは、セキュリティ脆弱性に対処する更新が頻繁にリリースされるため、常に最新の状態に保つ必要もあります。

既知のセキュリティ問題のリスクを低減するには、Node.jsランタイムとパッケージを最新の状態に保つことが不可欠です。その方法の一つとして、Snyk Advisorを使ってpackage.jsonファイルの状態を確認できます。脆弱性への対処が必要なライブラリや、非推奨または保守されなくなったnpmパッケージがわかります。

サンプルのpackage.jsonファイルをSnyk Advisorにドラッグ&ドロップすると、このプロジェクトには問題がある可能性のあるnpmパッケージが複数含まれていることがわかります。

csrf、passport-local-mongoose、loaderの3つのパッケージについて、健全性スコアとレビューの詳細を示すSnyk Advisorの依存関係ヘルスレポート。

詳しく見ると、Snyk Advisorではloaderが8年間新しいリリースを出しておらず、他のパッケージと比べてnpmユーザーの間であまり利用されていないことがわかります。アクセス制御の安全性を確保するための第一歩として、定期的に更新を確認してすぐに適用すること、または古くなったライブラリをプロジェクトから置き換えることが有効です。

難読化だけに頼らない

いくつかのアプリケーション例で見たように、セキュリティ・バイ・オブスキュリティは信頼できる対策ではありません。攻撃者が最終的にアクセス方法を推測できたり、ファジングで突破できたりするなら、アプリケーションは安全ではありません。適切なアクセス制御の仕組みと認証チェックを実装し、許可されたユーザーだけが機密機能やデータにアクセスできるようにする必要があります。

前の例では、エンドポイントに推測しにくい番号を使ったルートがありました。

app.get('/897928394834023-12', (req, res) => {
  res.send('Admin Panel')
})

実際には、アクセスを許可する前に適切な認証方式でデータを保護する、実績のある方法を選ぶほうが安全です。Passport.jsやAWS Cognitoなどの、より安全なログインライブラリの使用が推奨されますが、ここでは学習目的で、外部ライブラリを使わないログインページの大まかな例を紹介します。

// Dummy user data
const users = [
  {email: 'logan@test.com', password: '1234', role: 'user', id: '1'},
  {email: 'megan@test.com', password: '1234', role: 'admin', id: '2' }
];

app.post('/login', (req, res) => {
  // Generally, we should ensure HTTPS is used to encrypt the request body
  const user = users.find((u) => u.email === req.body.email && u.password === req.body.password);
  if (!user) {
    res.send('Error page')
  }
  if (user.role === 'admin') {
    res.send('Admin panel');
  }
  res.send('User panel')
})

管理者パネルのルートを難読化する代わりに、ユーザーは/loginに直接アクセスできます。ただし、管理者パネルへのアクセスが許可される前に、リクエスト本文で正しいメールアドレスとパスワードを入力する必要があります。これにより、リスクのある難読化手法に頼らず、各ユーザーにどの情報や応答を提供するかをより適切に管理できます。

デフォルトでアクセスを拒否し、きめ細かな権限を付与する

アプリケーション内のさまざまなデータへのアクセスを許可するユーザーを決める際は、まず最小限の権限から始めるのが賢明です。最小権限の原則を採用し、ユーザーには最初はすべてのアクセスを拒否して、必要な場合に限り特定のリソースや操作へのアクセスを許可します。

さらに、権限を段階的かつ細かく設定し、ユーザーが明示的に許可された操作のみを実行できるようにします。前の例のように、役割を追加することで、よりきめ細かな制御を実現できます。

app.post('/login', (req, res) => {
  // Generally, we should ensure HTTPS is used to encrypt the request body
  const user = users.find((u) => u.email === req.body.email && u.password === req.body.password);
  if (!user) {
    res.send('Error page')
  }
  switch (user.role) {
    case 'admin':
      res.send('Level 3 panel');
      break;
    case 'manager':
      res.send('Level 2 panel');
      break;
    case 'user+':
      res.send('Level 1 panel');
      break;
    case 'user':
      res.send('User panel');
      break;
    default:
      res.send('Unauthorized access');
  }
})

ここでは、最も高い権限が“Level 3 panel”に割り当てられ、adminロールを持つユーザーだけがアクセスできます。managerロールでログインした他のユーザーは“Level 2 panel”にアクセスできます。このパネルには、たとえば各ユーザーの情報が表示されますが、削除ボタンはありません。

明示的にロールを割り当てられていないユーザーには、自動的に“Unauthorized access”という応答が返されます。ユーザーにきめ細かな権限を付与し、必要に応じて段階的に引き上げることで、本来アクセス権がないユーザーに余分な権限を与えずに済みます。

徹底した監査とテストを実施する

コードベースを定期的に監査し、静的コード解析、依存関係スキャン、安全なコードレビューなどのセキュリティテストを実施して、アクセス制御の脆弱性やその他のセキュリティ上の問題を特定しましょう。アクセス制御の脆弱性として考えられる項目のリストをブックマークしておき、それを参考にアプリケーションに該当する問題がないか確認するのも有効です。また、アプリケーション全体にユニットテストを追加することは、コードの各行が期待どおりに動作することを確かめる標準的な方法です。

静的アプリケーション・セキュリティ・テスト(SAST)などの自動スキャンツールを活用すれば、一般的なセキュリティ上の問題を特定しやすくなります。SnykのVS Code拡張機能を使えば、JavaScriptファイルの保存時にセキュリティスキャンの結果がすぐにフィードバックされ、自動修正の提案も受けられます。

Visual Studio Code向けSnyk IDE拡張機能を使う

ぜひ活用したい強力なツールの1つがSnykです。Snykにはアプリケーションのセキュリティを確保するさまざまなツールがありますが、とりわけVisual Studio(VS)Code拡張機能を使うと、Node.jsコードの作成中にアクセス制御の不備を検出し、修正できます。

Snyk拡張機能の設定方法と効果的な使い方をご紹介します。

VS Codeの拡張機能タブを開き、「Snyk」と検索して、Snyk Security - Code, Open Source Dependencies, IaC Configurations拡張機能を見つけます。インストールをクリックします。

Snyk Security拡張機能の掲載と[インストール]ボタンが表示されたVisual Studio Codeの拡張機能マーケットプレイス

インストール後、SnykからIDEに接続してアカウントを認証するよう求められます。Connect VS Code with Snykをクリックすると、接続の許可を求めるSnykの外部ウェブページが開きます。そこでAuthenticateをクリックします。

認証が完了したら、スキャンをクリックしてコードの脆弱性を調べます。必要に応じて拡張機能を設定することもできます。

先ほどの/admin、/login、/accountルートの例では、Snykによって12件のセキュリティ上の脆弱性が検出されました。さらにSnykは、アプリケーションやその一部を構成するライブラリに潜む、コード品質、設定、オープンソースのセキュリティに関する問題もスキャンします。

JavaScriptファイル全体で12件の脆弱性と71件のコード品質の問題を示すSnyk Codeのセキュリティダッシュボード
Expressアプリでのクロスサイトリクエストフォージェリ(CSRF)の問題と、それに関連するJavaScriptコード例を示すSnykの脆弱性レポート。

Snyk拡張機能は、アクセス制御に関するものを含め、既知のセキュリティ上の脆弱性をコードからスキャンします。リアルタイムでフィードバックを提供し、検出した問題の修正方法を提案します。この拡張機能を使えば、アクセス制御の不備が本番環境に持ち込まれる前に検出し、対処できます。

さらにSnykには、さまざまなコードスキャンツールもあります。VS Code拡張機能と同様に、コードの作成中にセキュリティ上の問題を見つけられるよう設計されており、アクセス制御の問題をリアルタイムで特定して対処するために欠かせないツールです。

まとめ

この記事では、Node.jsにおけるアクセス制御の不備について解説しました。これは、Expressウェブフレームワークで構築されたアプリケーションにおいて、データや権限への不正アクセスにつながる重大なセキュリティ上の懸念事項です。保護されていないExpressの管理パネル用ルートから、予測可能なリクエストパラメーターのユーザーIDまで、この記事の例を通じて、アクセス制御の不備がもたらす潜在的なリスクを紹介しました。

Node.jsアプリケーションのアクセス制御の脆弱性を防ぐには、ライブラリを最新バージョンに保つ、隠蔽によるセキュリティに頼らない、最小権限の原則を適用する、徹底的な監査とテストを実施する、Snyk Visual Studio Code拡張機能などのツールを活用するといった対策が有効です。これらを組み合わせることで、アプリケーションのセキュリティを大幅に強化できます。安全なアプリケーションの開発と維持は継続的な取り組みです。アプリケーションとデータの完全性を保つには、常に警戒を怠らないことが重要です。

Snykを使って、今すぐセキュリティ修正を始めましょう!

続きを読む

feature insights context
Blog

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

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

illustration hero ai
Blog

AIハリケーンが到来

AIはソフトウェア開発とサイバー攻撃の双方を加速させています。リーダーは、エージェントとコードを開発の初期段階から保護し、実行時に制御を徹底するとともに、防御策を独立して検証しなければなりません。

feature insights context
Blog

予防は、本質的に解決済みの問題なのでしょうか?

エージェントが生成するコードの予防策はアーキテクチャ上解決されていますが、開発を遅らせることなくセキュリティを守る制御を選ぶことが、依然として課題です。