【TypeORM】Query Builder の select() でパラメーターを使用する方法【SQL インジェクション】

2022.08.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

where() にはあるけど select() にはない引数

TypeORM のクエリビルダーの where() , andWhere() , orWhere() にはパラメーターが引数で parameters?: ObjectLiteral と定義されています。

https://github.com/typeorm/typeorm/blob/master/src/query-builder/UpdateQueryBuilder.ts#L208-L274

そのためユーザーからの入力を用いて WHERE を組み立てる際にはパラーメーターを使用し SQL インジェクションを防ぐことができます。

const inputName = "なまえ' OR true -- "
const firstUser = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .where("user.name = :name", { name: inputName })
    .getOne()

しかし select() , addSelect() には引数としてパラメーターを受け取るようになっていません。

https://github.com/typeorm/typeorm/blob/master/src/query-builder/SelectQueryBuilder.ts#L117-L226

ですのでユーザーからの入力を用いて SELECT を組み立てる際に変数をそのまま組み込んでしまうと SQL インジェクションの原因になってしまいます。

実際に、 typeorm init --docker コマンドで TypeORM の環境を構築し、user テーブルにデータが入っていることを確認後、index.ts に以下のコードを貼り付け実行すると user テーブルが空になることが確認できます。

index.ts

import { AppDataSource } from './data-source'
import { User } from './entity/User'

/**
 * 指定した名前と同じ名前のユーザーはfirstNameを、それ以外はlastNameを取得する
 */
AppDataSource.initialize()
  .then(async () => {
    const input = `John' THEN null END FROM "user"; DELETE FROM "user"; -- `
    //const input = 'John'
    const users = await AppDataSource.manager
      .createQueryBuilder(User, 'user')
      .select(
        `CASE "firstName" WHEN '${input}' THEN "firstName" ELSE "lastName" END`
      )
      .getRawMany()

    console.log(users)
  })
  .catch((error) => console.log(error))

またログを有効にしていると以下のログが出ており、実行時エラーも起きておらず、入力された文字列がそのまま SQL として実行されることも確認できます。

query: SELECT CASE "firstName" WHEN 'John' THEN null END FROM "user"; DELETE FROM "user"; -- ' THEN "firstName" ELSE "lastName" END FROM "user" "user"

このように select() にユーザーからの入力をそのまま入れてしまう(特に CASE 文)と SQL インジェクションができてしまうので、もしそのような使い方をしている人は今回紹介する select() でパラメーターを使う方法に切り替えることを検討しましょう。

select() でパラメーターを使う方法

引数ではパラメーターを受け取るようになっていませんが、別で setParameter()setParameters() というメソッドが用意されています。

https://github.com/typeorm/typeorm/blob/7198a7a732dd513b6324539f215d819032aecb3a/src/query-builder/QueryBuilder.ts#L389-L425

ドキュメントだと where() の引数を使わずに setParameters() を使ってパラメーターを使うサンプルコードが記述されていますが、同じようにこれらのメソッドを用いて select() でもパラメーターを使うことができます。

先ほどの SQL インジェクションができてしまったコードを setParameter() を用いて書き直すと以下の通りになります。

index.ts

import { AppDataSource } from './data-source'
import { User } from './entity/User'

/**
 * 指定した名前と同じ名前のユーザーはfirstNameを、それ以外はlastNameを取得する
 */
AppDataSource.initialize()
  .then(async () => {
    const input = `John' THEN null END FROM "user"; DELETE FROM "user"; -- `
    //const input = 'John'
    const users = await AppDataSource.manager
      .createQueryBuilder(User, 'user')
      .select(
        `CASE "firstName" WHEN :param THEN "firstName" ELSE "lastName" END`
      )
      .setParameter('param', input)
      .getRawMany()

    console.log(users)
  })
  .catch((error) => console.log(error))

ログでは以下のようにパラメーターを用いているのが確認でき、入力した文字列がそのまま SQL として実行されていないことが確認できます。

query: SELECT CASE "firstName" WHEN $1 THEN "firstName" ELSE "lastName" END FROM "user" "user" 
-- PARAMETERS: ["John' THEN null END FROM \"user\"; DELETE FROM \"user\"; -- "]

注意点

TypeORM のパラメーターの使い方における注意点ですが、1つの SQL 文の中でパラメーター名が重複してしまうと最後に指定した値で上書きされてしまいます。

const users = await AppDataSource.manager
  .createQueryBuilder(User, 'user')
  .where('age > :age', { age: 10 })
  .andWhere('age < :age')
  .setParameter('age', 20)
  .getRawMany()
query: SELECT "user"."id" AS "user_id", "user"."firstName" AS "user_firstName", "user"."lastName" AS "user_lastName", "user"."age" AS "user_age" FROM "user" "user" WHERE age > $1 AND age < $2 
-- PARAMETERS: [20,20]

上記では :age という名前で 10 と 20 をそれぞれ別の場所で設定していますが、実際に出力される SQL を見ると両方とも 20 になっていることが確認できます。

まとめ

select() , addSelect() には引数としてパラメータを受け取るようになっていませんが諦めないでください。

setParameter()setParameters() を使って select でもパラメータを使うことができます。