Blitz.js で気をつけたいこと(脆弱性対策)
まずは下記の html を見てください [1]
<!doctype html>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ログインフォーム</title>
<form method="post">
<input name="name" />
<input name="password" type="password" />
<button type="submit">login</button>
</form>
<script>
document.querySelector('form')?.addEventListener('submit', e => {
e.preventDefault()
const name = document.querySelector('form [name="name"]')?.value
const password = document.querySelector('form [name="password"]')?.value
fetch('/api/query', { method: 'post', body: 'SELECT * FROM users' })
.then(r => r.json())
.then(rows => {
if (rows.find(u => u.name === name && u.password === password)) {
location.href = '/admin-page'
} else {
alert('失敗しました')
}
})
})
</script>
やばいですね。
さて上記のやばさが分かった上で Blitz.js で書いていく上で
このようなコードを書いてしまわないために気をつけたい点を書いていきます。
queries と mutations
詳細は公式の説明やこちらの記事が参考になるのですが
api
, queries
, mutations
という名前のディレクトリの中身にあるファイルはサーバサイド用のコードとして扱われるようです。
データベースに保存するためのデータの組み立てやデータベースからの取り出しはここでやった方が良いですね。
ロールの確認や post したユーザの id を使った create は注意が要ります。
blitz generate all articles title:String content:String belongsTo:User
を実行すると
下記のようなコードが生成されます。 [2] [3]
import { Ctx } from "blitz"
import db, { ArticleCreateArgs } from "db"
type CreateArticleInput = Pick<ArticleCreateArgs, "data">
export default async function createArticle({ data }: CreateArticleInput, ctx: Ctx) {
ctx.session.authorize()
const article = await db.article.create({ data })
return article
}
import React from "react"
import Layout from "app/layouts/Layout"
import { Link, useRouter, useMutation, BlitzPage } from "blitz"
import createArticle from "app/articles/mutations/createArticle"
import ArticleForm from "app/articles/components/ArticleForm"
const NewArticlePage: BlitzPage = () => {
const router = useRouter()
const [createArticleMutation] = useMutation(createArticle)
return (
<div>
<h1>Create New Article</h1>
<ArticleForm
initialValues={{}}
onSubmit={async () => {
try {
const article = await createArticleMutation({ data: { name: "MyName" } })
alert("Success!" + JSON.stringify(article))
router.push("/articles/[articleId]", `/articles/${article.id}`)
} catch (error) {
alert("Error creating article " + JSON.stringify(error, null, 2))
}
}}
/>
<p>
<Link href="/articles">
<a>Articles</a>
</Link>
</p>
</div>
)
}
NewArticlePage.getLayout = (page) => <Layout title={"Create New Article"}>{page}</Layout>
export default NewArticlePage
pages/articles/new.tsx
でエラーが出ているのですが
例えば下記のように修正するとエラーは消えて問題なく動いて見えますが…
- import { Link, useRouter, useMutation, BlitzPage } from "blitz"
+ import { Link, useRouter, useMutation, useSession, BlitzPage } from "blitz"
// 略
+ const session = useSession()
// 略
- const article = await createArticleMutation({ data: { name: "MyName" } })
+ const article = await createArticleMutation({ data: {
+ title: "MyName",
+ content: "",
+ user: {
+ connect: { id: session.userId },
+ }
+ } })
この実装では悪意あるユーザが記事に紐づく著者を自由に設定できてしまいます。 [4]
対策
auth
ディレクトリを参考に正しく実装していきます。
まずは validations.ts
を作成します。
zod については公式を参照してください。 [5]
import * as z from "zod"
export const CreateInput = z.object({
title: z.string(),
content: z.string(),
})
export type CreateInputType = z.infer<typeof CreateInput>
export const UpdateInput = z.object({
id: z.number(),
title: z.string(),
content: z.string(),
})
export type UpdateInputType = z.infer<typeof UpdateInput>
次に createArticle.ts
を編集していきます。
入力をきちんと検証しないと自動生成したい内容を手動で入力されてしまうかもしれません。
一応 parse した返り値から各プロパティを取り出してますが ( zod を使っている限りでは ) そのまま create などに渡しても問題ないと思います。
import { Ctx } from "blitz"
+ import db from "db"
- import db, { ArticleCreateArgs } from "db"
+ import { CreateInput, CreateInputType } from "../validations"
- type CreateArticleInput = Pick<ArticleCreateArgs, "data">
+ type CreateArticleInput = { data: CreateInputType }
export default async function createArticle({ data }: CreateArticleInput, ctx: Ctx) {
ctx.session.authorize()
+ const { title, content } = CreateInput.parse(data)
+
- const article = await db.article.create({ data })
+ const article = await db.article.create({
+ data: {
+ title,
+ content,
+ user: {
+ connect: { id: ctx.session.userId },
+ },
+ },
+ })
return article
}
最後に new.tsx
を修正します。
- const article = await createArticleMutation({ data: { name: "MyName" } })
+ const article = await createArticleMutation({ data: { title: "MyName", content: "" } })
同様に create や delete も修正していきます。
必要であれば getArticle なども修正が要ると思います。 ( 非公開な記事などを実装する場合 )
import { Ctx, AuthorizationError } from "blitz"
import db from "db"
import { UpdateInput, UpdateInputType } from "../validations"
type UpdateArticleInput = UpdateInputType
export default async function updateArticle(input: UpdateArticleInput, ctx: Ctx) {
ctx.session.authorize()
const { id, title, content } = UpdateInput.parse(input)
const where = { id, userId: ctx.session.userId }
const data = { title, content }
const { count } = await db.article.updateMany({ where, data })
if (!count) throw new AuthorizationError()
const article = await db.article.findOne({ where })
return article
}
pages
前述とは逆に、ページの情報は公開されていると思って良さそうです。
極端な例ですが例えば管理者専用ページとして下記のようなフォームを置いてもバレます。
<div>
<label htmlFor={id}>このユーザがガチャを引いたときの SSR 排出率を下げる</label>
<input type="checkbox" checked={checked} id={id} onChange={onChange} />
</div>
あとがき
脆弱性対策以外にも Prisma の experimental な機能についてなど気をつけたい点はあるので
Blitz.js と Prisma と Next.js のドキュメントは読んでおいた方が良さそうです。
Discussion