はじめに
Prismaを使用してブログのウェブアプリを構築していると想像してください。提供されたメールアドレスとパスワードに基づいてユーザーを認証するためのシンプルなクエリを作成します。
1const user = await prisma.user.findFirst({
2 where: { email, password },
3});無害に見えますよね?しかし、攻撃者が送信した場合、どうなるでしょうか? password = { "not": "" }? メールアドレスとパスワードが一致した場合にのみUserオブジェクトを返す代わりに、提供されたメールアドレスのみが一致した場合でも、クエリは常にUserを返します。
この脆弱性はオペレーターインジェクションとして知られていますが、より一般的にはNoSQLインジェクションと呼ばれています。多くの開発者が認識していないのは、厳格なモデルスキーマにもかかわらず、一部のORMはPostgreSQLのようなリレーショナルデータベースと組み合わせて使用される場合でもオペレーターインジェクションに対して脆弱であるため、予想よりも広範なリスクとなっていることです。
この投稿では、オペレーターインジェクションがどのように機能するかを探り、Prisma ORMにおけるエクスプロイトを実演し、それらを防ぐ方法について議論します。
オペレーターインジェクションの理解
ORMにおけるオペレーターインジェクションを理解するためには、まずNoSQLインジェクションに注目すると興味深いでしょう。MongoDBは、開発者向けに、以下のようなオペレーターを使用してデータをクエリするためのAPIを導入しました。 $eq, $lt そして $ne. ユーザー入力がMongoDBのクエリ関数に盲目的に渡されると、NoSQLインジェクションのリスクが存在します。
JavaScript向けの一般的なORMライブラリは、データクエリ用の同様のAPIを提供し始め、現在ではMongoDBをサポートしていない場合でも、ほとんどすべての主要なORMが何らかのクエリオペレーターのバリエーションをサポートしています。Prisma、Sequelize、TypeORMはすべて、PostgreSQLのようなリレーショナルデータベース向けのクエリオペレーターのサポートを実装しています。
Prismaにおけるオペレーターインジェクションのエクスプロイト
複数のレコードを操作するPrismaのクエリ関数は、通常、クエリオペレーターをサポートしており、インジェクションに対して脆弱です。例として、以下の関数が挙げられます。 findFirst, findMany, updateMany そして deleteMany. Prismaは実行時にクエリで参照されるモデルフィールドを検証しますが、オペレーターはこれらの関数にとって有効な入力であり、そのため検証によって拒否されません。
Prismaでオペレーターインジェクションがエクスプロイトされやすい理由の1つは、Prisma APIが提供する文字列ベースのオペレーターです。 一部のORMライブラリでは、開発者に見落とされやすく、エクスプロイトが容易であるため、文字列ベースのクエリオペレーターのサポートを削除しています。その代わりに、開発者にオペレーター用のカスタムオブジェクトを参照するよう強制しています。これらのオブジェクトはユーザー入力から容易に逆シリアル化できないため、これらのライブラリではオペレーションインジェクションのリスクが大幅に軽減されます。
Prismaの全てのクエリ関数がオペレーターインジェクションに対して脆弱であるわけではありません。単一のデータベースレコードを選択または変更する関数は、通常、オペレーターをサポートせず、オブジェクトが提供された場合にランタイムエラーをスローします。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アプリケーションフレームワーク向けに開発している場合、コントローラーでのユーザー入力検証に特定のメソッドが推奨されている可能性があります。
元の例では、zodによる検証は次のようになります。
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の設計に起因し、使用されているデータベースの種類とは関係ありません。実際、PrismaとPostgreSQLを組み合わせた場合でも、オペレーターインジェクションに対して脆弱である可能性があります。Prismaはオペレーターインジェクションに対する組み込みの保護を提供していますが、開発者はアプリケーションのセキュリティを確保するために、依然として入力検証とサニタイズを実施する必要があります。
付録: Userモデルの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}
