Skip to main content

Node.jsでSQLインジェクション攻撃を防ぐ

著者
Headshot of Lucien Chemaly

Lucien Chemaly

2024年2月20日

0 分で読めます

ソフトウェアシステムへの依存が高まるにつれて、さまざまなセキュリティ脅威も増加しています。開発者にとって特に注意すべき脅威の1つが、Node.jsを使用する場合に発生するSQLインジェクションです。

SQLインジェクションとは、悪意のあるSQLコードをシステムに注入する攻撃です。機密情報の漏えいやデータの改ざん・削除を引き起こし、攻撃者に不正アクセスを許すこともあります。この脅威への対処は容易ではなく、Node.jsアプリケーションのセキュリティを確保する必要性を示しています。

この記事では、SQLインジェクション攻撃が重大な脅威となる理由と、Node.jsアプリケーションを攻撃から守る方法について解説します。

Node.jsアプリケーションにおけるSQLインジェクションを理解する

SQLインジェクション攻撃は主に、Node.jsアプリケーションがユーザー入力を適切に処理できていない場合に発生します。ユーザー入力を適切に検証・サニタイズせず、クエリを適切に構築しないままSQLクエリに使用すると、攻撃者がクエリを操作できる抜け穴が生じ、不正アクセスやデータ漏えいにつながります。Node.jsに関する過去および最近の脆弱性をすべて確認したい場合は、Snyk Vulnerability Databaseで一覧を確認できます。

SQLインジェクションの脆弱性があるExpress Node.jsアプリ

SQLインジェクションの仕組みを理解するために、Node.js、Express、PostgreSQLデータベースを使って、脆弱なアプリを手早く作成してみましょう。このアプリケーションはフォームからユーザー入力を受け取り、SQLクエリを構築してデータベースに対して実行し、データを取得します。

まず、Node.jsをダウンロードして、マシンにインストールします。Node.jsは、サーバー側でJavaScriptコードを実行するランタイム環境です。

次に、プロジェクト用の新しいディレクトリ(つまり vulnerable-app)を作成し、ターミナルでコマンド cd vulnerable-app を実行して、そのディレクトリに移動します。

プロジェクトディレクトリで、コマンド npm init -y を実行します。このコマンドにより、Node.jsプロジェクトのマニフェストファイルとして使用する、既定値が設定された package.json ファイルが作成されます。

npm install express pg を実行して、Expressとpgライブラリをインストールします。Expressは軽量なWebアプリケーションフレームワークで、pgはPostgreSQL用のNode.jsクライアントです。

続いて、PostgreSQLをダウンロードしてインストールします。インストールが完了したら、vulnerable_dbという名前の新しいデータベースを作成します。このデータベース内に、nameという列を持つusersという名前のテーブルを作成します。実行するSQLコマンドは次のとおりです。

   CREATE TABLE users (
       id SERIAL PRIMARY KEY,
       name VARCHAR(255)
   );

PostgreSQLのインストールと実行方法について詳しくは、W3Schoolsのこちらのドキュメントをご覧ください。

app.jsという名前のファイルを作成し、次のコードを追加します。

const express = require("express");
const { Client } = require("pg");
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json()); // To handle JSON input
const client = new Client({
  host: "your-host",
  database: "your-database",
  username: "your-username",
  password: "your-password",
  port: "your-port",
});
client.connect();
app.post("/search", (req, res) => {
  const userInput = req.body.userInput;
  const query = "SELECT * FROM users WHERE name = '" + userInput + "';";
  client.query(query, (err, result) => {
    if (err) throw err;
    res.send(result.rows);
  });
});
app.post("/create", (req, res) => {
  const { name } = req.body;
  if (!name) {
    res.status(400).send("Name is required");
    return;
  }
  const query = "INSERT INTO users (name) VALUES ($1) RETURNING *;";
  client.query(query, [name], (err, result) => {
    if (err) {
      console.error(err);
      res.status(500).send("Internal Server Error");
      return;
    }
    res.send(result.rows[0]); // Respond with the created user record
  });
});
app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

このコードはExpressサーバーを初期化し、pgライブラリを使ってPostgreSQLデータベースへの接続を設定します。また、URLエンコード形式とJSON形式の入力を処理するようサーバーを構成します。

2つのルートを定義します。POST /searchルートとPOST /createルートです。/searchルートはリクエストボディからユーザー入力を受け取り、SQLクエリを構築してデータベースで実行し、結果を返します。しかし、ユーザー入力をクエリに連結するため、SQLインジェクションの脆弱性があります。/createルートはリクエストボディからnameの値を取り出して、値が存在することを確認したうえで、安全なパラメーター化SQLクエリを構築し、新しいユーザーレコードをデータベースに挿入して、そのレコードを返します。最後に、サーバーはポート3000でリッスンを開始し、コンソールにメッセージを表示して起動したことを知らせます。

注意: PostgreSQLに設定した構成に合わせて、your-username、your-password、your-database、your-host、your-portを置き換えてください。

入力の検証とサニタイズを行っていないため、このコードにはSQLインジェクションの脆弱性があります。攻撃者はフォームに' OR '1'='1'; --のような値を入力できます。この入力によって元のクエリが終了し、SELECT * FROM users WHERE name = '' OR '1'='1'; --';に変更されます。'1'='1'は常に真となるため、usersテーブル内のすべてのレコードが返されます。これは、文字列連結によるSQLインジェクションの典型的な例です。

準備が整ったので、サーバーを起動しましょう。ターミナルでコマンド node app.js を実行します。サーバーが起動し、正常に動作していることを示すServer is running on port 3000というメッセージが表示されます。

node app.jsコマンドでNode.jsアプリを起動し、「Server is running on port 3000.」と表示されたターミナル

アプリケーションをテストするには、ターミナルまたはシェルからユーザーをいくつか作成します。

curl -X POST http://localhost:3000/create -H "Content-Type: application/json" -d '{"name":"User1"}'

curl -X POST http://localhost:3000/create -H "Content-Type: application/json" -d '{"name":"User2"}'

curl -X POST http://localhost:3000/create -H "Content-Type: application/json" -d '{"name":"User3"}'

curl -X POST http://localhost:3000/create -H "Content-Type: application/json" -d '{"name":"User4"}'

curl -X POST http://localhost:3000/create -H "Content-Type: application/json" -d '{"name":"User5"}'

次に、ユーザー確認、データ更新、監査や監視などの目的で特定のユーザーを検索する場合は、ユーザーデータの検索リクエストを送信します。次のコマンドを実行してください。

curl -X POST http://localhost:3000/search -H "Content-Type: application/json" -d '{"userInput":"User1"}'

次のような出力が表示されます。

[{"id":1,"name":"User1"}]

次に、次のコマンドでSQLインジェクションを試し、すべてのユーザーを取得します。

curl -X POST http://localhost:3000/search -H "Content-Type: application/json" -d "{\"userInput\":\"' OR '1'='1'; -- \"}"

次のような出力が表示されます。

[{"id":1,"name":"User1"},{"id":2,"name":"User2"},{"id":3,"name":"User3"},{"id":4,"name":"User4"},{"id":5,"name":"User5"},{"id":6,"name":"User2"},{"id":7,"name":"User2"},{"id":8,"name":"User3"},{"id":9,"name":"User4"},{"id":10,"name":"User5"}]

このコマンドでは、JSONデータ全体を外側の二重引用符(" ")で囲み、完全なJSON文字列にしています。また、JSONデータ内の引用符はバックスラッシュ(\)でエスケープし、JSON文字列の一部として正しく扱われるようにしています。userInputの値は' OR '1'='1'; -- に設定されています。前述のとおり、この値はクエリ条件が常に真となるように変更し、SQLクエリを操作してすべてのユーザーレコードを返させます。

Node.jsでSQLインジェクションの脆弱性を防ぐ方法

SQLインジェクション攻撃がいかに簡単に発生するかがわかったところで、アプリをSQLインジェクションから守る方法をいくつか見ていきましょう。ユーザーにとって安全で堅牢なアプリにするための対策を紹介します。

クエリプレースホルダーを使う

SQLクエリを文字列連結で構築しないことは、SQLインジェクションの防止に欠かせません。攻撃者は入力フィールドを通じて悪意のあるSQLコードを注入し、クエリを操作できます。代わりにクエリプレースホルダーを使い、ユーザー入力が実行可能なコードではなくデータとして扱われるようにします。

app.post('/search', (req, res) => {
    const userInput = req.body.userInput;
    const query = "SELECT * FROM users WHERE name = $1;";
    client.query(query, [userInput], (err, result) => {
        if (err) throw err;
        res.send(result.rows);
    });
});

プリペアドステートメントを使う

SQLインジェクション攻撃を防ぐ最も効果的な方法の1つは、プリペアドステートメントを使うことです。プリペアドステートメントを使えば、ユーザー入力は常に実行可能なコードではなくデータとして扱われます。

pgライブラリを使って、/searchルートをプリペアドステートメントに対応させる方法は次のとおりです。

app.post('/search', (req, res) => {
    const userInput = req.body.userInput;
    const query = "SELECT * FROM users WHERE name = $1;";
    const preparedQuery = {
        text: query,
        values: [userInput]
    };
    client.query(preparedQuery, (err, result) => {
        if (err) throw err;
        res.send(result.rows);
    });
});

このコードでは、プリペアドステートメントをオブジェクトとして定義し、SQLクエリ用のtextプロパティとユーザー入力用のvaluesプロパティを設定しています。このように分離することで、SQLクエリのテンプレートを一度だけ指定し、実行時にプレースホルダーへさまざまな値を渡せます。これにより、SQLクエリでユーザー入力を安全かつ効率的に処理できます。

ユーザー入力を検証・サニタイズする

ユーザー入力が、形式、長さ、値の範囲などの要件を満たしていることを確認するのは、安全性を確保するうえで非常に重要です。joiやexpress-validatorなどの検証ライブラリを活用すれば、このプロセスを簡略化できます。これらのライブラリには、あらかじめ定義された検証ルールや関数が用意されているため、入力要件の適用や検証プロセスの自動化が容易になります。

さらに、アプリケーション環境の安全性と堅牢性を保つには、有害となる可能性のある文字を削除またはエスケープする入力サニタイズも欠かせません。入力サニタイズとは、ユーザー入力がアプリケーションで処理される際に、危害や予期しない動作を引き起こさないように、入力をクリーンアップするプロセスです。

express-validatorライブラリを使って検証とサニタイズを実装する方法は次のとおりです。

 const { body, validationResult } = require('express-validator');

app.post("/search", [
  body('userInput').trim().escape().isAlphanumeric(),
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }

  const userInput = req.body.userInput;
  const query = "SELECT * FROM users WHERE name = $1;";

  client.query(query, [userInput], (err, result) => {
      if (err) throw err;
      res.send(result.rows);
  });
});

ORMとSQLライブラリは最新バージョンを使う

ORMとSQLライブラリは常に最新バージョンを使うようにしてください。ライブラリのメンテナーは、既知のセキュリティ脆弱性を修正するためのアップデートやパッチを頻繁にリリースしています。

npm install pg@latest

定期的にコードをレビューし、セキュリティ監査を実施する

脆弱性を早期に発見するため、コードレビューとセキュリティ監査を定期的に実施しましょう。npm auditやSnykなどのツールを使えば、依存関係にある既知の脆弱性を特定するプロセスを自動化できます。

Visual Studio Code用のSnyk IDE拡張機能で脆弱性を検出する

Snykは開発者向けに設計されたセキュリティツールで、コードやオープンソースの依存関係に潜む脆弱性の特定と修正を支援します。開発ワークフローにシームレスに統合され、SQLインジェクションに関連するものを含め、セキュリティ上の欠陥を継続的に監視・スキャンします。

Snykは脆弱性を特定するだけでなく、自動修正プルリクエストも提供するため、潜在的なセキュリティ問題に簡単に対処し、防止できます。GitHub、GitLab、Bitbucketなど、さまざまな開発プラットフォームと連携できるほか、Visual Studio Code専用の拡張機能も提供しており、コーディング中にリアルタイムでフィードバックを得られます。

Snyk拡張機能をセットアップする

Snyk拡張機能のセットアップは簡単です。まだインストールしていない場合は、Visual Studio Codeをダウンロードしてインストールします。インストール後にVisual Studio Codeを起動し、WindowsではCtrl + Shift + X、macOSではCommand + Shift + Xを押して、拡張機能ビューを開きます。

拡張機能ビューで検索バーに「Snyk」と入力し、Snyk拡張機能のインストールボタンをクリックします。

「snyk」の検索結果と、インストールボタンが表示されたSnyk Security拡張機能が掲載されているVisual Studio Code Marketplace。

インストールすると、Visual Studio Codeの左側のナビゲーションバーにSnyk拡張機能が表示されます。クリックしてからワークスペースを信頼して接続をクリックし、Snykアカウントにサインインして拡張機能を利用します。Snykアカウントをお持ちでない場合は、無料で作成できます。

「ワークスペースを信頼して接続」ボタンが強調表示され、脆弱性スキャナーの詳細が表示された、Snyk拡張機能を含むVisual Studio Codeの画面。

続いて、サインインして認証するよう求められます。完了すると、Authenticatedというメッセージが表示されます。

「認証済み」というメッセージと「アカウントが認証されました。Snykを使用する準備が整いました。」という文が表示された、Snykの認証確認ページ。

Snyk拡張機能をインストールして設定すると、SQLインジェクションの潜在的なリスクを含め、プロジェクト内の既知の脆弱性が自動的にスキャンされます。

Snykプラットフォームでアカウントにアクセスし、手順に従ってSnyk Codeを有効化します。Snyk Codeを有効にすると、SQLインジェクションなどのコードの脆弱性をSnykで検出できるようになります。

SnykアカウントでSnyk Codeが有効になっていない場合、Visual Studio CodeのSnyk拡張機能は、コード内の不適切なプラクティスに起因するSQLインジェクションを検出できません。検出できるのは、ライブラリの古いバージョンなどに関連する脆弱性のみです。

Snykは脆弱性を特定するだけではありません。修正方法について提案や参考情報も提供するため、特に大規模なコードベースや厳しい納期に取り組む際に役立ちます。

このチュートリアルのコードは、こちらのGitHubリポジトリで確認できます。

まとめ

この記事では、Node.jsアプリケーションでSQLインジェクションがどのように発生するかを学び、その防止に役立つ複数の方法を紹介しました。ORMやSQLライブラリの更新、ユーザー入力のサニタイズ、クエリプレースホルダーの使用から、Visual Studio Code用のSnyk IDE拡張機能の活用まで、Node.jsアプリケーションをSQLインジェクション攻撃から守るための対策は数多くあります。

Snyk拡張機能はSQLインジェクションの脆弱性を特定するだけでなく、その解決方法も案内し、仮想のセキュリティメンターとして機能します。Snykを開発ワークフローに組み込めば、セキュリティ脅威に対応するだけでなく、脅威を積極的に防止できます。アプリケーションを本番環境にリリースする前に、堅牢で安全な状態を保てます。

Node.jsの開発を続けるうえで、セキュリティは一度取り組めば終わりではなく、継続的なプロセスであることを忘れないでください。最新のセキュリティのベストプラクティスを把握し、学び続け、アプリケーションを安全に保ちましょう。ユーザーにもきっと感謝されます。自分のデータもユーザーのデータも安全に守られているとわかれば、安心して開発に取り組めます。

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップで、Capture the Flagの課題の解き方を学びましょう。

続きを読む

blog feature ai lilac
Blog

エージェント型修正で脆弱性バックログをプルリクエストに変える

Evo Remediation Agentがオープンプレビューで利用可能になり、Evo Agentic AppSecに依存関係の自動解決機能が加わりました。AppSecチームは継続的な修正キャンペーンを実施できるようになりました

AI Background min crop
Blog

AIが、あなたが受け入れてきたあらゆるリスクを一気に顕在化させる:AI Security Panel: Unfilteredから得た7つの洞察

AIによって攻撃はより安価かつ高速になり、組織がすでに抱えているリスクが露呈しています。4人のセキュリティリーダーが、機械の速度に対応する防御と迅速な修復が今不可欠である理由を語ります。

blog feature ai violet
Blog

VulnBump:AIエージェントはビルドを壊さずに脆弱な依存関係を修正できるか?

VulnBumpでは、実際の依存関係のアップグレード307件をAIエージェントで検証します。脆弱性が解消され、ビルドとテストが正常に通った場合にのみ、修正成功と判定します。