小さな Web サイト・Web アプリでも入れている「壊さないための仕組み」7 つと、その理由

セキュリティテストCloudflare

はじめに

Web サイトや Web アプリは、公開した日がゴールではありません。文言の修正、項目の追加、ライブラリの更新と、公開後も手を入れ続けます。そのたびに「直したら別のところが壊れた」「更新したら画面が真っ白になった」が起きると、発注した側は安心して改修を頼めなくなります。

そこで私は、数ページの小さな案件でも、最初に「壊さないための仕組み」を入れるようにしています。どれも一度入れてしまえば手間はほとんどかからず、案件が大きくなるほど効いてきます。

この記事では、普段入れている仕組みを 7 つ、「何をしているか」と「発注者にとって何が嬉しいか」に分けて紹介します。例として、Cloudflare(Web サイトを世界中の拠点から配信するサービス)の上で動かす構成のコードを少しだけ載せます。

1. データベースを「本物」でテストする

していること

自動テスト(プログラムが正しく動くかを、プログラムで確かめる仕組み)を書くとき、データベースや画像の保存先を「それらしく振る舞う偽物(モック)」に置き換えるやり方がよくあります。手軽ですが、偽物は本物と細かな振る舞いが違うため、「テストは通るのに本番で動かない」が起きます。

私は、データベース(Cloudflare D1)と画像の保存先(Cloudflare R2)をモックせず、Cloudflare の開発ツール wrangler の getPlatformProxy という機能で、本物と同じ仕組みをパソコンのメモリ上に立ち上げてテストしています。立ち上げたら、本番と同じ手順でテーブルを作る SQL(マイグレーション)を当て、テストごとに中身を空にします。

// テスト用の D1・R2 を立ち上げる。メモリ上に作り、ファイルには保存しない
const proxy = await getPlatformProxy<Cloudflare.Env>({ persist: false });
const { DB: d1, BUCKET: bucket } = proxy.env;
const statements = await readMigrationStatements();
await d1.batch(statements.map((s) => d1.prepare(s)));

テストを書く側は、describe の中で useTestEnv() を 1 行呼ぶだけで、空のデータベースと保存先が用意されます。「記事を消すと、記事とカテゴリの結び付きも消える」のように、データベースの制約まで含めて確かめられます。

さらに、マイグレーションそのものもテストします。古い形のテーブルまで当ててデータを入れ、残りのマイグレーションを当てたあと、既存のデータが新しい形に正しく写っているかを確かめます。

発注者にとって嬉しいこと

テストが本番に近いので、「テストは通ったのに本番で壊れた」が起きにくくなります。とくに、既にお客様のデータが入っている状態での仕様変更でも、データを失わずに移せるかを事前に確かめられます。

2. 確認が通らないと公開されないようにする(CI)

していること

GitHub Actions(GitHub 上でプログラムを自動で動かす仕組み)で、コードを送るたびに次の確認を自動で走らせています。このような自動確認の仕組みを CI と呼びます。

確認 内容
整形 コードの書き方がそろっているか(Prettier)
lint ミスにつながりやすい書き方が無いか(ESLint)
マイグレーション 古いコードを壊す変更が無いか(5 章)
型チェック 値の種類の取り違えが無いか(TypeScript)
テスト 1 章のテストがすべて通るか
ビルド 本番用・ステージング用の両方で組み立てられるか

公開もこの流れにつないでいます。作業用のブランチ(develop)に送るとステージング(お客様に見せる前の確認用の環境)に、本番用のブランチ(main)に送ると本番に、それぞれ自動で出ます。どちらも CI が通ってからでないと進みません。本番には、ステージングで確かめたものだけを出します。

細かいところでは、同じブランチの公開は 1 回ずつ順番に行い、途中で取り消さないようにしています。続けて送ったときに、古い方が後から公開されて新しい方を上書きしてしまうのを防ぐためです。

発注者にとって嬉しいこと

「確認し忘れたまま本番に出る」が仕組みとして起きません。また、本番に出る前に必ずステージングで実際の画面を見られるので、公開前に一緒に確かめていただけます。

3. 部品(ライブラリ)を入れる前に、危ないものを止める

していること

今の Web 開発では、npm という仕組みで他の人が作った部品(パッケージ)を大量に組み合わせます。ここに悪意のあるコードを混ぜる攻撃(サプライチェーン攻撃)が実際に起きています。

対策として、部品を入れるときは必ず Socket Firewall(sfw)を通しています。npm install の前に sfw を付けるだけで、悪意があると分かっている部品を、パソコンに入る前に止めてくれます。

sfw npm install <パッケージ>

CI と公開の流れでも sfw npm ci で入れています。ここで 1 つ気をつけているのが、npm のキャッシュ(一度ダウンロードしたものを手元に取っておく仕組み)を使わないことです。sfw は手元のキャッシュから入るものは調べられないため、キャッシュを使うと確認をすり抜けてしまいます。少し時間はかかりますが、毎回ダウンロードし直して確かめる方を選んでいます。

また、公開に使う Cloudflare の鍵は、部品のインストールやビルドの段階には渡さず、実際に公開する段階にだけ渡しています。万一、部品の中に悪いコードが紛れても、鍵を読まれにくくするためです。

発注者にとって嬉しいこと

作ったサイトに、知らないうちに悪いコードが入り込む危険を下げられます。開発者のパソコンや公開用の鍵が狙われるのも防ぎます。

4. 認証は 1 か所、入力ルールも 1 か所

していること

「この URL はログインした人だけ」「この API(プログラム同士がやり取りする窓口)は鍵を持っている人だけ」という確認を、ページや API ごとに書くと、どこかで書き忘れます。私は、すべてのアクセスが最初に通る middleware(入口の共通処理)1 か所で確認するようにしています。

この入口では、URL の表記ゆれにも気を配ります。たとえば /api/... を //api/... や、文字を記号に置き換えた形(URL エンコード)で書くと、チェックの判定だけをすり抜けられることがあります。そこで、フレームワークがページを決めるときと同じ形に URL をそろえてから判定し、そろえきれない怪しい URL は断っています。合言葉(トークン)を比べるときも、比べる時間の差から中身を推測されないような比べ方にしています。

お問い合わせフォームなどの入力ルール(必須か、何文字までか、メールアドレスの形か)は、Zod というライブラリで 1 か所に定義し、画面側のチェックとサーバー側の検証で同じものを使っています。

// 問い合わせフォームの入力ルール。画面でのチェックとサーバーでの検証の両方で使う
export const contactSchema = z.object({
  name: requiredText('お名前', 100),
  company: requiredText('会社名', 100),
  // email, message も同様
});

発注者にとって嬉しいこと

確認漏れが構造上起きにくくなります。入力ルールを変えたいときも 1 か所直せば画面とサーバーの両方に反映されるので、「画面では通るのに送信するとエラーになる」といった食い違いが起きません。

5. データベースの変更は「追加だけ」を先に出す

していること

新しい版を公開している途中や、公開に失敗したときには、「新しいデータベースの形の上で、古いプログラムが動く」時間がどうしても生まれます。このとき、古いプログラムが使っている項目を消したり名前を変えたりしていると、その間サイトが壊れます。

そこで、1 回の公開で当てるデータベースの変更は、古いプログラムがそのまま動く「追加」だけにしています。

1 回の公開で当ててよい 公開を分ける
テーブルの追加 テーブル・項目の削除
空でもよい項目の追加 名前の変更
初期値付きの項目の追加 型の変更・必須化
インデックスの追加

たとえば項目の名前を変えたいときは、「新しい項目を追加する → 両方に書き込むプログラムを出す → 既存のデータを写す → 古い項目を使うプログラムが無くなった後で消す」と、何回かの公開に分けます。

このルールは人の注意に頼らず、CI で止めています。npm run check:migrations が、変更の SQL に削除や名前の変更が含まれていないかを調べ、見つかれば公開を止めます。本当に消してよい段階になったら、SQL に -- allow-destructive: <理由> と理由を書いたときだけ通します。

また、データベースの変更は失敗しても元に戻さない方針にしています。戻すと、その間に入ったデータを失いやすいためです。上のルールを守っていれば「1 つ前のプログラム + 今のデータベース」は必ず動くので、プログラムだけを 1 つ前に戻せば済みます。

発注者にとって嬉しいこと

仕様変更のたびに「メンテナンス中」で止めたり、公開の瞬間にエラーが出たりすることを避けられます。万一おかしな版を出しても、データを失わずに元に戻せます。

6. ブラウザ側の守りと、確認用環境の鍵

していること

サーバーから返すすべての応答に、ブラウザに守り方を指示する情報(セキュリティヘッダー)を付けています。

  • CSP: ページが読み込んでよいスクリプトや画像の出どころを絞ります。万一、悪いスクリプトを埋め込まれても動きにくくなります
  • 他サイトへの埋め込みの禁止: 見えない枠の中でボタンを押させる詐欺(クリックジャッキング)を防ぎます
  • Permissions-Policy: カメラ・マイク・位置情報など、使わないブラウザの機能を使えなくします。外部の部品を埋め込んだ場合も同じです
  • 常に暗号化された通信(HTTPS)で開かせる指示
// 使わないブラウザの機能は、埋め込んだ外部のページからも使わせない
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',

ステージングには Basic 認証(ブラウザが出すユーザー名・パスワードの入力欄)をかけ、関係者以外が見られないようにしています。鍵が設定されていなければ、開けるのではなく「すべて断る」側に倒しています。

発注者にとって嬉しいこと

サイトを訪れるお客様を、よくある攻撃から守れます。公開前の内容が検索結果や第三者の目に触れることもありません。

7. 「なぜそうしたか」を記録に残す(ADR)

していること

設計で複数の案から 1 つを選んだときは、ADR(Architecture Decision Record。設計の判断の記録)として短い文書に残しています。書くのは「背景と課題」「判断基準」「検討した選択肢」「決定」です。

たとえば、記事の公開状態を「公開・非公開」の 2 択から「下書き・公開中・公開をやめた」の 3 つに変えたときは、ほかに「下書きかどうかの印を足す」案なども並べ、「ありえない状態の組み合わせをデータベースに入れられないこと」を基準に選んだ、と残しています。公開のしかたや、秘密情報の置き場所なども同じように記録しています。

発注者にとって嬉しいこと

数か月後に「なぜこうなっているのか」と聞かれたとき、記憶ではなく記録で答えられます。担当者が替わったり、別の会社に引き継いだりするときも、同じ議論を繰り返さずに済みます。

おわりに

ここで紹介した仕組みは、どれも派手な機能ではありません。ただ、一度入れてしまえば、その後のすべての改修を「壊さずに、早く」進める土台になります。小さな案件ほど後回しにされがちですが、規模が小さいうちに入れる方がずっと安く済みます。

「作って終わり」ではなく、公開後も安心して手を入れ続けられる Web サイト・Web アプリを作りたい方は、お気軽にご相談ください。

← ブログ一覧へ戻る