アプリを root でデータベースにつないでいると、SQL インジェクションの穴が1つあっただけで、テーブルを丸ごと消されるおそれがあります。MySQL では CREATE USER でユーザーを作り、GRANT で必要な操作だけを許しておけば、もしものときの被害は許した範囲で止まります。この記事では、アプリ用と集計用のユーザーを作り、テーブルごと・列ごとの権限、ロール(権限のまとまり)、REVOKE での取り消しまでを順に試します。MySQL 8.0 で GRANT からユーザーを作れなくなった点にも触れます。
以下の実行結果は MySQL 8.0 で実際に実行したものです。
このシナリオで考える
通販サイトの shop データベースを例にします。顧客の customers と注文の orders の2つのテーブルがあり、customers の email 列は個人情報です。このデータベースを使うのは、Web アプリと、売上を集計する社員の2者です。
| アカウント | 使う人 | 許す操作 |
|---|---|---|
| app_user@10.0.% | Web アプリ(10.0.x.x のサーバー) | shop の全テーブルの読み書き |
| report_user@% | 売上を集計する社員 | orders の読み取りと、customers の id・name・prefecture の読み取り |
CREATE DATABASE shop;
USE shop;
CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
prefecture VARCHAR(10) NOT NULL,
email VARCHAR(100) NOT NULL
);
CREATE TABLE orders (
id INT PRIMARY KEY,
customer_id INT NOT NULL,
amount INT NOT NULL,
ordered_at DATETIME NOT NULL
);
INSERT INTO customers VALUES
(1, '佐藤', '東京都', 'sato@example.com'),
(2, '鈴木', '大阪府', 'suzuki@example.com');
INSERT INTO orders VALUES
(1, 1, 3200, '2026-11-01 10:00:00'),
(2, 2, 1800, '2026-11-02 15:30:00');
CREATE USERでアカウントを作る
MySQL のアカウントは、ユーザー名と接続元ホストの組で1つと数えます。同じ app_user でも、どこからつなぐかを host の部分で決めます。
CREATE USER 'app_user'@'10.0.%' IDENTIFIED BY 'App#Pass_2026';
CREATE USER 'report_user'@'%' IDENTIFIED BY 'Report#Pass_2026';
SELECT user, host FROM mysql.user
WHERE user IN ('app_user', 'report_user')
ORDER BY user;
+-------------+--------+
| user | host |
+-------------+--------+
| app_user | 10.0.% |
| report_user | % |
+-------------+--------+
host の書き方は次のとおりです。
| host の書き方 | つなげる接続元 |
|---|---|
| ‘localhost’ | MySQL と同じサーバーから |
| ‘10.0.%’ | 10.0. で始まる IP アドレスから |
| ‘10.0.0.0/255.255.0.0’ | ネットマスクで指定した範囲から(上と同じ範囲) |
| ‘10.0.0.0/16’ | CIDR 表記で指定した範囲から(MySQL 8.0.23 以降) |
| ‘%’ | どこからでも |
‘%’ は手軽ですが、パスワードが漏れると外からも入れてしまいます。アプリのサーバーの IP が決まっているなら、その範囲に絞っておくと、ほかの場所からはログインできません。なお、ここでのパスワードは説明用です。実際には長いランダムな文字列にしてください。
GRANTで必要な権限だけを渡す
作ったばかりのアカウントは、ログインはできても何も読めません。GRANT で「どの操作を」「どこに対して」許すかを指定します。
| 範囲 | ON の書き方 | 例 |
|---|---|---|
| サーバー全体 | *.* | GRANT PROCESS ON *.* TO … |
| データベース | shop.* | GRANT SELECT ON shop.* TO … |
| テーブル | shop.orders | GRANT SELECT ON shop.orders TO … |
| 列 | 権限のあとに列名 | GRANT SELECT (id, name) ON shop.customers TO … |
アプリ用には、shop の中のデータの読み書きだけを許します。テーブルを作る・消す(CREATE・DROP)は渡しません。
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'app_user'@'10.0.%';
SHOW GRANTS FOR 'app_user'@'10.0.%';
+-------------------------------------------------------------------------+
| Grants for app_user@10.0.% |
+-------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `app_user`@`10.0.%` |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `shop`.* TO `app_user`@`10.0.%` |
+-------------------------------------------------------------------------+
1行目の USAGE は「権限なし」を表す行で、どのアカウントにも出ます。この app_user が DROP TABLE shop.orders を実行しても、次のエラーで止まります(@ のあとには、実際につないだ接続元が入ります)。
ERROR 1142 (42000): DROP command denied to user 'app_user'@'10.0.%' for table 'orders'
GRANT と REVOKE は、実行した時点でサーバーに反映されます。FLUSH PRIVILEGES を続けて打つ必要はありません。アプリ側の対策と組み合わせると、インジェクションの被害をさらに小さくできます。プレースホルダの使い方は「SQLインジェクションの仕組みと対策:プレースホルダで防ぐ方法」で説明しています。
列ごとの権限で個人情報を見せない
集計用の report_user には、orders は全部、customers は email を除いた3列だけを読ませます。列の権限は、権限名のあとにカッコで列名を並べます。
GRANT SELECT ON shop.orders TO 'report_user'@'%';
GRANT SELECT (id, name, prefecture) ON shop.customers TO 'report_user'@'%';
SHOW GRANTS FOR 'report_user'@'%';
+--------------------------------------------------------------------------------------+
| Grants for report_user@% |
+--------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `report_user`@`%` |
| GRANT SELECT (`id`, `name`, `prefecture`) ON `shop`.`customers` TO `report_user`@`%` |
| GRANT SELECT ON `shop`.`orders` TO `report_user`@`%` |
+--------------------------------------------------------------------------------------+
report_user で email を含めて SELECT すると、列の名前を挙げたエラーになります。SELECT * も、許していない列が含まれるので通りません。
-- SELECT name, email FROM shop.customers;
ERROR 1143 (42000): SELECT command denied to user 'report_user'@'%' for column 'email' in table 'customers'
-- SELECT * FROM shop.customers;
ERROR 1142 (42000): SELECT command denied to user 'report_user'@'%' for table 'customers'
SELECT id, name, prefecture のように3列だけを書けば、ふつうに読めます。集計担当の人に列名を毎回気にさせたくないときは、3列だけのビューを作り、そのビューにだけ SELECT を許すやり方もあります。ビューの作り方は「MySQLのVIEW入門:基本構文・更新可能ビュー・性能注意点」で説明しています。
ロールで権限をまとめて渡す
MySQL 8.0 からは、権限のまとまりをロールとして作り、それを人に渡せます。集計担当が5人いても、ロールに1回 GRANT するだけで済みます。
CREATE ROLE 'shop_read', 'shop_write';
GRANT SELECT ON shop.* TO 'shop_read';
GRANT INSERT, UPDATE, DELETE ON shop.* TO 'shop_write';
CREATE USER 'kato'@'%' IDENTIFIED BY 'Kato#Pass_2026';
GRANT 'shop_read' TO 'kato'@'%';
SET DEFAULT ROLE 'shop_read' TO 'kato'@'%';
SHOW GRANTS FOR 'kato'@'%';
SHOW GRANTS FOR 'kato'@'%' USING 'shop_read';
+-------------------------------------+
| Grants for kato@% |
+-------------------------------------+
| GRANT USAGE ON *.* TO `kato`@`%` |
| GRANT `shop_read`@`%` TO `kato`@`%` |
+-------------------------------------+
+----------------------------------------+
| Grants for kato@% |
+----------------------------------------+
| GRANT USAGE ON *.* TO `kato`@`%` |
| GRANT SELECT ON `shop`.* TO `kato`@`%` |
| GRANT `shop_read`@`%` TO `kato`@`%` |
+----------------------------------------+
ふつうの SHOW GRANTS では、ロールの名前しか出ません。USING でロールを指定すると、中身の権限まで展開して見られます。
SET DEFAULT ROLE の行は省けません。ロールは渡しただけでは有効にならず、ログインした直後は何も読めない状態です。SET DEFAULT ROLE で指定したロールが、ログインのたびに有効になります。サーバーの設定 activate_all_roles_on_login を ON にすると、渡したロールがすべてログイン時に有効になります。
REVOKEで取り消し、DROP USERで消す
渡した権限は REVOKE で取り消せます。書き方は GRANT とほぼ同じで、TO が FROM に変わります。
REVOKE DELETE ON shop.* FROM 'app_user'@'10.0.%';
SHOW GRANTS FOR 'app_user'@'10.0.%';
DROP USER 'kato'@'%';
+-----------------------------------------------------------------+
| Grants for app_user@10.0.% |
+-----------------------------------------------------------------+
| GRANT USAGE ON *.* TO `app_user`@`10.0.%` |
| GRANT SELECT, INSERT, UPDATE ON `shop`.* TO `app_user`@`10.0.%` |
+-----------------------------------------------------------------+
すでにつながっている接続にいつ効くかは、権限の範囲で変わります。
| 変えた権限の範囲 | いまの接続に効くとき |
|---|---|
| テーブル・列 | 次に SQL を送ったとき |
| データベース | 次に USE を実行したとき |
| サーバー全体(PROCESS などの静的な権限) | つなぎ直したとき |
アプリはコネクションプールで接続を使い回すことが多いので、データベース単位の権限を外したあとは、アプリを再起動して接続を作り直すと確実です。
よくある落とし穴
古い手順書の「GRANT … TO 新しいユーザー」は、MySQL 8.0 ではユーザーを作ってくれません。まだないアカウントに GRANT すると、次のエラーになります。CREATE USER を先に実行してください。
GRANT SELECT ON shop.* TO 'batch_user'@'localhost';
ERROR 1410 (42000): You are not allowed to create a user with GRANT
名前が同じでも、host が違えば別のアカウントです。’report_user’@’localhost’ を作ると、’report_user’@’%’ とは別にパスワードと権限を持ちます。
CREATE USER 'report_user'@'localhost' IDENTIFIED BY 'Other#Pass_2026';
SELECT user, host FROM mysql.user WHERE user = 'report_user' ORDER BY host;
SHOW GRANTS FOR 'report_user'@'localhost';
+-------------+-----------+
| user | host |
+-------------+-----------+
| report_user | % |
| report_user | localhost |
+-------------+-----------+
+-------------------------------------------------+
| Grants for report_user@localhost |
+-------------------------------------------------+
| GRANT USAGE ON *.* TO `report_user`@`localhost` |
+-------------------------------------------------+
ログインするときは、接続元に合うアカウントのうち host がより細かいほうが選ばれます。「権限を付けたのに読めない」ときは、SELECT CURRENT_USER(); でどのアカウントとして入っているかを確かめてください。host を省いて CREATE USER ‘tmp_user’ と書くと、host は ‘%’ になります。
ほかにも次の点に気をつけてください。
- アプリに GRANT ALL PRIVILEGES ON *.* を渡さない。ほかのデータベースやユーザー管理まで触れてしまいます
- mysql.user などの表を UPDATE で直接書き換えない。FLUSH PRIVILEGES を打つまで反映されず、書き間違いにも気づきにくくなります
- 使わなくなったアカウントは DROP USER で消す。退職者や古いバッチのアカウントが残ると、パスワードが漏れたときの入口になります
まとめ
MySQL のアカウントはユーザー名と接続元の組で、CREATE USER で作ってから GRANT で権限を渡します。アプリには対象のデータベースの読み書きだけ、集計担当には必要なテーブルと列の読み取りだけ、と役割ごとに分けておけば、パスワードが漏れても被害は許した範囲で止まります。人が増えるならロールにまとめ、SET DEFAULT ROLE まで忘れずに実行してください。権限を変えたら SHOW GRANTS で中身を確かめる習慣をつけておくと、付け忘れや付けすぎにすぐ気づけます。
もっと学ぶなら【PR】
- MySQL徹底入門 第5版:MySQL の機能を広く解説した本。日付関数などの細かい動きを調べたいときに
※Amazonのアソシエイトとして、当サイトは適格販売により収入を得ています。
参考リンク
- MySQL 8.0 Reference Manual: GRANT Statement
- MySQL 8.0 Reference Manual: Using Roles
- MySQL 8.0 Reference Manual: Specifying Account Names
- MySQL 8.0 Reference Manual: When Privilege Changes Take Effect
アイキャッチ画像: Photo by AngTheDesigner on Unsplash
