この記事はソニックガーデン プログラマ アドベントカレンダーの18日目の記事です。
Webアプリケーション開発では、「クライアント(ブラウザ)から送られるデータは常に改ざん可能である」という前提を念頭に置くことが重要です。入力フォームやAPIリクエスト、JavaScript経由で送信されるデータは、ユーザーによって簡単に書き換えられる可能性があることを常に意識しましょう。
1. 権限チェック(Authorization)
クライアントからのデータ改ざんとは?
ユーザーがWebブラウザを介して送信するデータは、素のHTMLフォームやJSのコードを少しいじったり、ブラウザの開発者ツールや外部ツール(Postman, curlなど)を用いることで、いとも簡単に不正な値へと書き換えることが可能です。
これによって次のような事態が発生します。
- 他人のデータを改ざん可能: 他のユーザーが投稿した記事の内容を不正に書き換えられる
- 不正操作の実行: 権限がないはずのユーザーが管理者専用の操作(例: ユーザー削除)を行える
- ビジネスへの影響: ECサイトで他のユーザーの注文情報を改ざんし、商品が別の住所に発送されるなどのトラブルが発生する
例1: フォームの値を改ざん
<form method="POST" action="/articles/1">
<input type="hidden" name="article[title]" value="正しいタイトル">
<button type="submit">送信</button>
</form>
このフォームの title
の値を開発者ツールで "悪意のあるタイトル"
に変更することで、不正なデータを送信できます。
例2: APIリクエストによる改ざん
ブラウザのネットワークタブや専用ツール(例: Postmanやcurl)を使用して、以下のようにあらゆるリクエストを直接送信できます。
PATCH /orders/1 HTTP/1.1
Host: example.com
Content-Type: application/json
{
"order": {
"address": "別の住所"
}
}
画面側での制御
UI上では、権限がない操作を行うボタンやリンクは極力表示しないようにします。これにより、権限のない操作を「うっかり押してしまう」ケースを防げます。
例: RailsのViewでのボタン制御
<% if current_user.can_edit? %>
<%= link_to '編集', edit_article_path(article), class: 'btn btn-primary' %>
<% end %>
ただし、これは「見た目を隠している」だけであり、リクエスト自体は裏から直接送信できるため、これだけで防御は不十分です。
サーバーサイドでの権限チェック
不正なリクエストによるデータ改ざんや操作を防ぐためには、サーバー側での厳密な権限制御が必須です。
- コントローラー内で
current_user
と操作対象のリソースを照合し、適切な権限があるか確認します -
Pundit
やCanCanCan
などの権限管理gemを利用するのも有効です
例: Rails コントローラでの権限チェック(Pundit利用)
class ArticlePolicy < ApplicationPolicy
def update?
user.manager? || record.author == user
end
end
class ArticlesController < ApplicationController
def update
article = current_team.articles.published.find(params[:id])
authorize article
if article.update(article_params)
redirect_to article
else
render :edit
end
end
private
def article_params
params.require(:article).permit(:title, :content)
end
end
authorize article
で権限判定を行い、許可されていない操作であればエラーとなります。
2. 入力値チェック(Validation)
入力値チェックが不十分だと起きる問題
-
不正データの保存:
- 空欄や無効なデータが保存されると、後続の処理でエラーが発生する可能性があります。例えば、名前の空欄を許すとメール送信時に「宛先名が空欄」のエラーとなることがあります
-
エラーの発生:
- データが想定外の形式で保存されると、例えば、ECサイトで商品価格フィールドに「無料」といった文字列が保存されると、数値を期待する計算処理に文字列が渡されて例外が発生するなど、アプリケーションが正常に動作しなくなる問題が発生します
-
データの不整合:
- 無効な日付や存在しないユーザーIDが保存されると、日付フィールドに「32/13/2024」のような無効な日付が保存され、日付計算が正しく行えない
各層でのバリデーション
バリデーションは複数の層で実施し、ユーザーが入力ミスに気づきやすい仕組みを作るとともに、システムとしても堅牢性を高めます。
Viewでの入力チェック
- 目的: ユーザーがすぐに入力ミスに気づけるようにする
- 実装例: 入力必須チェック、フォーマット確認、カスタムエラーメッセージの表示
例: 必須フィールドのバリデーション
<%= form_with model: @user, local: true do |form| %>
<div class="form-group">
<%= form.label :email, "メールアドレス" %>
<%= form.email_field :email, class: 'form-control', required: true %>
</div>
<div class="form-group">
<%= form.label :age, "年齢" %>
<%= form.number_field :age, class: 'form-control', required: true, min: 18, max: 100 %>
</div>
<div class="form-group">
<%= form.submit "登録", class: "btn btn-primary" %>
</div>
<% end %>
サーバー側(Rails Model)
- 目的: 入力値をシステム全体で正確に取り扱うため。
-
主なバリデーション:
presence
,uniqueness
,format
,numericality
,length
など。
例: Rails モデルのバリデーション
class User < ApplicationRecord
belongs_to :company
validates :email, presence: true,
format: { with: URI::MailTo::EMAIL_REGEXP },
uniqueness: true
validates :password, length: { minimum: 6 }
validates :age, presence: true,
numericality: { only_integer: true,
greater_than_or_equal_to: 18,
less_than_or_equal_to: 100 }
end
データベース(DB 制約)
- 目的: 最終防衛ラインとしてデータの完全性を保証。
-
主な制約:
NOT NULL
,UNIQUE
, 外部キー制約など。
例: マイグレーションでの制約追加
def change
add_reference :users, :company, foreign_key: true, null: false
add_column :users, :age, :integer, null: false
add_column :users, :email, :string, null: false
add_index :users, :email, unique: true
end
DB制約により、Railsやアプリ側で想定外の不正データが流入した場合でも、データベースレベルで弾くことができます。
まとめ
- 権限制御: クライアント側でボタンを隠すなどの対策に加え、必ずサーバー側でも権限チェックを行うことで、想定外のデータ操作を防ぐ
- バリデーション: フロントエンド、サーバーサイド、データベースという複数段階でデータの妥当性を検証し、不正データ流入を最小化する
これらを徹底することで、不正利用やデータ不整合による大きなトラブルを未然に防止できます。
しかし、これで「絶対安全」ではありません。攻撃者は日々新たな手口を考え出します。定期的な見直しやコードレビュー、セキュリティ情報へのキャッチアップを心がけましょう。