はじめに
Prismaを使用してブログ用Webアプリを構築しているとします。提供された電子メールとパスワードに基づいてユーザーを認証する簡単なクエリを記述します:
1const user = await prisma.user.findFirst({
2 where: { email, password },
3});
無害に見えるだろう?しかし、攻撃者が password = { "not": "" }
?emailとpasswordが一致した場合のみUserオブジェクトを返すのではなく、emailのみが一致した場合は常にUserを返します。
この脆弱性は演算子インジェクションとして知られていますが、より一般的にはNoSQLインジェクションと呼ばれています。多くの開発者が気づいていないのは、厳密なモデルスキーマにもかかわらず、いくつかのORMは PostgreSQLのようなリレーショナルデータベースで使われているときでさえ演算子インジェクションに対して 脆弱であるということです。
この投稿では、演算子インジェクションがどのように機能するかを調べ、Prisma ORMでの悪用を実演し、それを防ぐ方法について説明します。
オペレーター・インジェクションを理解する
ORMの演算子インジェクションを理解するには、まずNoSQLの演算子インジェクションを見てみるのが面白い。MongoDBは、次のような演算子を使ってデータを問い合わせるAPIを開発者に紹介した。 eqドル
, ドル
そして ドル
.ユーザー入力がMongoDBのクエリー関数にやみくもに渡されると、NoSQLインジェクションのリスクが存在する。
JavaScript用の人気のあるORMライブラリは、データをクエリするための同様のAPIを提供し始め、今ではほとんどすべての主要なORMが、MongoDBをサポートしていなくても、クエリ演算子のいくつかのバリエーションをサポートしている。Prisma、Sequelize、TypeORMはすべて、PostgreSQLのようなリレーショナルデータベース用のクエリ演算子を実装しています。
Prismaにおける演算子インジェクションの悪用
複数のレコードを操作するPrismaクエリ関数は、一般的にクエリ演算子をサポートしており、インジェクションの脆弱性があります。関数の例 最初を見つける
, findMany
, updateMany
そして 削除多数
.Prismaは実行時にクエリで参照されるモデル・フィールドを検証しますが、演算子はこれらの関数の有効な入力であるため、検証によって拒否されることはありません。
Prismaで演算子インジェクションが悪用されやすい理由の1つは、Prisma APIで提供されている文字列ベースの演算子です。 いくつかのORMライブラリは、文字列ベースのクエリ演算子のサポートを削除しています。これは、開発者が見落としやすく、悪用されやすいためです。その代わりに、開発者は演算子用のカスタムオブジェクトを参照することを余儀なくされます。これらのオブジェクトはユーザー入力から容易にデシリアライズできないため、これらのライブラリでは操作インジェクションのリスクが大幅に減少します。
Prismaのすべてのクエリ関数が演算子インジェクションの脆弱性を持つわけではありません。1つのデータベースレコードを選択または変更する関数は通常、演算子をサポートしておらず、Objectが指定されると実行時エラーがスローされます。findUnique 以外に、Prisma の update、delete、upsert 関数も where フィルタで演算子を受け付けません。
1 // This query throws a runtime error:
2 // Argument `email`: Invalid value provided. Expected String, provided Object.
3 const user = await prisma.user.findUnique({
4 where: { email: { not: "" } },
5 });
オペレーター・インジェクションを防ぐためのベスト・プラクティス
1.ユーザー入力をプリミティブデータ型にキャストする
通常、文字列や数値のようなプリミティブなデータ型に入力をキャストすれば、攻撃者がオブジェクトを注入するのを防ぐのに十分である。元の例では、キャストは次のようになる:
1 const user = await prisma.user.findFirst({
2 where: { email: email.toString(), password: password.toString() },
3 });
2.ユーザー入力の検証
キャストは効果的ですが、入力がビジネスロジックの要件を満たしていることを確認するために、ユーザー入力を検証したい場合があります。
class-validator、zod、joiなど、サーバーサイドでユーザー入力を検証するライブラリはたくさんあります。NestJSやNextJSのようなWebアプリケーションフレームワークで開発している場合、コントローラでユーザー入力を検証する特定の方法を推奨していることが多いでしょう。
元の例では、ゾッドの検証は次のようになる:
1import { z } from "zod";
2
3const authInputSchema = z.object({
4 email: z.string().email(),
5 password: z.string().min(8)
6});
7
8const { email, password } = authInputSchema.parse({email: req.params.email, password: req.params.password});
9
10const user = await prisma.user.findFirst({
11 where: { email, password },
12});
3.ORMを常にアップデートする
アップデートを継続することで、セキュリティの改善や修正を受けることができます。たとえば、Sequelize はバージョン 4.12 からクエリー演算子の文字列エイリアスを無効にし、演算子インジェクションの影響を大幅に軽減しました。
結論
演算子インジェクションは、最新のORMを使用するアプリケーションにとって現実的な脅威である。この脆弱性はORMのAPI設計に起因するもので、使用するデータベースの種類とは関係ありません。実際、PostgreSQLと組み合わせたPrismaでさえ、演算子インジェクションに対して脆弱である可能性があります。Prismaは演算子インジェクションに対する組み込みの保護を提供していますが、開発者はアプリケーションのセキュリティを確保するために入力検証とサニタイズを実践しなければなりません。
付録ユーザーモデルのPrismaスキーマ
1// This is your Prisma schema file,
2// learn more about it in the docs: https://pris.ly/d/prisma-schema
3
4generator client {
5 provider = "prisma-client-js"
6}
7
8datasource db {
9 provider = "postgresql"
10 url = env("DATABASE_URL")
11}
12
13// ...
14
15model User {
16 id Int @id @default(autoincrement())
17 email String @unique
18 password String
19 name String?
20 posts Post[]
21 profile Profile?
22}