回答編集履歴

1

追記

2016/06/20 02:20

投稿

mpyw
mpyw

スコア5223

test CHANGED
@@ -2,332 +2,280 @@
2
2
 
3
3
 
4
4
 
5
+ 原因が判明しましたので,内容を変更します.ttyp03さんのご指摘通りです.
6
+
7
+
8
+
9
+ ただ,依然としてこの部分はPHPの文字列として直接変数展開せずに,プリペアドステートメントのプレースホルダ「?」に当てはめるように書くべきなので,セキュリティ上まずい書き方であることには変わりません.
10
+
11
+
12
+
13
+ ## 修正すべき脆弱性
14
+
15
+
16
+
17
+ - ユーザから入力された変数をそのままSQL文の中に展開しているので,**SQLインジェクション攻撃**に対する致命的な脆弱性がある.この脆弱性は1つもあってはいけないレベルで危険.そもそもPDOの使い方を間違っているので,正しい用法を覚える.
18
+
19
+ - `htmlspecialchars`を通さずにHTML内にテキストデータを表示しようとしている場所がある.**XSS攻撃**に対する脆弱性になり得るので,常に通すように心がけるべき.
20
+
21
+
22
+
23
+ ## 誤作動やエラーの防止
24
+
25
+
26
+
27
+ - 変数未定義のエラーを防ぐために,`$_POST`の存在確認を`isset`で行うべき.欲を言えば更に`is_string`で文字列かどうかの確認を行うべき.これらをまとめて行ってくれる,`filter_input`の利用が推奨される.
28
+
29
+ - `SET NAMES utf8`はデータベース側の文字セットしか変更できないため,基本的には,PDOドライバ側も同時に変更できる,DSNでの`charset=utf8`指定を利用すべき.
30
+
31
+ - エラーモードがデフォルトの`PDO::ERRMODE_SILENT`のままなので,`->prepare()`のコールに失敗した場合にPDOExceptionが発生せず,単にfalseが返されるだけになってしまっている.エラーモードを`PDO::ERRMODE_EXCEPTION`に変更しない限りは,いちいち返り値をチェックする必要がある.
32
+
5
- 日付にどんな値が格納されているか,ま検索時にどんな値ったのか不具合再現できる具体値ださい.
33
+ - LIKE検索のためのエスケープていない`%` `_` `\` といった特殊文字含むキーワードしく取り扱えない.
34
+
6
-
35
+ - SELECTの結果に対して`->rowCount()`は使えず,常にゼロになってしまうため,結果を配列として取得したあと配列に対して`count()`を実行する.
7
-
8
-
36
+
37
+
38
+
9
- また,日付する条件分岐も違和感を覚えました.
39
+ ## 読みやすいコードにするためは必要
40
+
41
+
42
+
10
-
43
+ - HTMLの表示内容はほとんど重複するにも関わらず,絞り込み検索を行うためだけにPHPファイルを複製している.このような場合は「絞り込み条件が送信されてきたか」で判定し,HTML表示部分は共通化するだけで十分.
44
+
11
-
45
+ - PHPのテンプレートエンジンとしての特性を無視して,`print`でHTMLタグ全体を出力している部分がある.基本的には変数に依存する部分のみを `<?= ~ ?>` 記法で出力するだけで十分.
46
+
12
-
47
+ - ロジックとHTMLを可能な限り分離すべき.**「あらかじめ必要な変数を作っておき,すべての変数が用意できたらHTMLを書き始める」**というやり方を徹底する.`<body>`まで書いてからデータベースに接続して…,というのは典型的にダメな書き方.また,HTMLを途中まで書いておいて最後まで書き終わる前に`die`で強制終了するのNG.
48
+
49
+
50
+
51
+ ## 改善済みのコード
52
+
53
+
54
+
13
- ```php
55
+ ```html
14
-
56
+
15
- $stmt =$dbh->prepare($sql);
57
+ <?php
16
-
17
-
18
-
19
- if ($vid != "") {
58
+
20
-
59
+
60
+
21
- $sql .= " and id = $vid ";
61
+ // HTML中にテキストを埋め込むときに利用する関数
62
+
63
+ function h($str)
64
+
65
+ {
66
+
67
+ return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
22
68
 
23
69
  }
24
70
 
25
71
 
26
72
 
73
+ // $_POST['vid'], $_POST['nm'], $_POST['ndt'] を安全に文字列として受け取る
74
+
75
+ // もし存在していない場合,空文字列になる
76
+
77
+ $vid = (string)filter_input(INPUT_POST, 'vid');
78
+
79
+ $nm = (string)filter_input(INPUT_POST, 'nm');
80
+
81
+ $ndt = (string)filter_input(INPUT_POST, 'ndt');
82
+
83
+
84
+
85
+ try {
86
+
87
+
88
+
89
+ // localhostのkadaiデータベースに文字コードutf8で接続
90
+
91
+ // SQL実行時のエラーもPDOExceptionに変換してスローさせる
92
+
93
+ $pdo = new PDO('mysql:dbname=kadai;host=localhost;charset=utf8', 'root', '', [
94
+
95
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
96
+
97
+ ]);
98
+
99
+
100
+
101
+ // 共通のSQL文と空のパラメータ用配列を準備する
102
+
103
+ $sql = '
104
+
105
+ SELECT id, name, address, tel1, tel2, email, ndate, remarks
106
+
107
+ FROM mst_member
108
+
109
+ WHERE 1 = 1
110
+
111
+ ';
112
+
113
+ $params = [];
114
+
115
+
116
+
117
+ // $vid が入力されていたら,AND条件としてプレースホルダ「?」を追加し.
118
+
119
+ // そこに文字列として当てはめるための値を $params に追加する
120
+
121
+ if ($vid !== '') {
122
+
123
+ $sql .= ' AND id = ? ';
124
+
125
+ $params[] = $vid;
126
+
127
+ }
128
+
129
+
130
+
131
+ // $nm が入力されていたら,AND条件としてプレースホルダ「?」を追加し.
132
+
133
+ // そこに文字列として当てはめるための値を $params に追加する
134
+
135
+ // 名前に関してはLIKE検索を行うので,LIKE検索で使われる特殊文字を
136
+
137
+ // addcslashes関数を使ってエスケープしたあと,両端を%で囲んだ値を使用する
138
+
27
- if ($nm != "") {
139
+ if ($nm !== '') {
28
-
140
+
29
- $sql .= " and name like '%$nm%' ";
141
+ $sql .= ' AND name like ? ';
142
+
143
+ $params[] = '%' . addcslashes($nm, '\_%') . '%';
144
+
145
+ }
146
+
147
+
148
+
149
+ // $hdt が入力されていたら,AND条件としてプレースホルダ「?」を追加し.
150
+
151
+ // そこに文字列として当てはめるための値を $params に追加する
152
+
153
+ if ($ndt !== '') {
154
+
155
+ $sql .= ' AND ndate >= ? ';
156
+
157
+ $params[] = $ndt;
158
+
159
+ }
160
+
161
+
162
+
163
+ // SQLを実行し,すべての結果を2次元の配列として一気に取得
164
+
165
+ $stmt = $pdo->prepare($sql);
166
+
167
+ $stmt->execute($params);
168
+
169
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
170
+
171
+
172
+
173
+ } catch (PDOException $e) {
174
+
175
+
176
+
177
+ // もしどこかでエラーが発生した場合,「500 Internal Server Error」として
178
+
179
+ // テキスト形式でエラーメッセージを表示して終了する
180
+
181
+ header('Content-Type: text/plain; charset=UTF-8', true, 500);
182
+
183
+ echo "ただいま障害により大変ご迷惑をお掛けしております。\n";
184
+
185
+ echo "Error: {$e->getMessage()}\n";
186
+
187
+ exit;
188
+
189
+
30
190
 
31
191
  }
32
192
 
33
193
 
34
194
 
195
+ // エラーが起こらなければHTMLとして表示を開始する
196
+
197
+ header('Content-Type: text/html; charset=UTF-8');
198
+
199
+
200
+
201
+ ?>
202
+
203
+ <!DOCTYPE html>
204
+
205
+ <title>名簿一覧</title>
206
+
207
+ <h1>名簿一覧</h1>
208
+
209
+ <form method="post" action="">
210
+
211
+ ID(半角) <input type="text" name="vid" value="<?=h($vid)?>"><br>
212
+
213
+ 氏名 <input type="text" name="nm" value="<?=h($nm)?>"><br>
214
+
215
+ 入会日 <input type="date" name="ndt" value="<?=($ndt)?>"><br>
216
+
217
+ <input type="submit" value="検索">
218
+
219
+ </form>
220
+
221
+ <?php if ($_SERVER['REQUEST_METHOD'] === 'POST'): /* POSTされたときだけ結果件数を示す */ ?>
222
+
223
+ <p>検索結果は<?=h(count($rows))?>件です</p>
224
+
225
+ <?php endif; ?>
226
+
35
- if ($ndt != "") {
227
+ <table border="1">
36
-
37
- $sql .= " and ndate >= $ndt ";
228
+
38
-
39
- } else {
40
-
41
- $post[]="";
229
+ <tr>
42
-
43
- }
230
+
44
-
45
-
46
-
47
- $stmt = $dbh->prepare($sql);
231
+ <th>ID</th>
48
-
49
- $data[] = $vid;
232
+
50
-
51
- $data[] = $nm;
52
-
53
- $data[] = $ndt;
233
+ <th>名前</th>
234
+
54
-
235
+ <th>住所</th>
236
+
237
+ <th>固定電話番号</th>
238
+
239
+ <th>携帯電話番号</th>
240
+
241
+ <th>Eメールアドレス</th>
242
+
55
- $stmt->execute($data);
243
+ <th>入会日</th>
244
+
245
+ <th>備考</th>
246
+
247
+ </tr>
248
+
249
+ <?php foreach ($rows as $row): ?>
250
+
251
+ <tr>
252
+
253
+ <td><?=h($row['id'])?></td>
254
+
255
+ <td><?=h($row['name'])?></td>
256
+
257
+ <td><?=h($row['address'])?></td>
258
+
259
+ <td><?=h($row['tel1'])?></td>
260
+
261
+ <td><?=h($row['tel2'])?></td>
262
+
263
+ <td><?=h($row['email'])?></td>
264
+
265
+ <td><?=h($row['ndate'])?></td>
266
+
267
+ <td><?=h($row['remarks'])?></td>
268
+
269
+ </tr>
270
+
271
+ <?php endforeach; ?>
272
+
273
+ </table>
56
274
 
57
275
  ```
58
276
 
59
277
 
60
278
 
61
- この`$post`ってなんですかね?使ってない変数に見えるんですが.
62
-
63
-
64
-
65
- ## 修正すべき脆弱性
66
-
67
-
68
-
69
- - ユーザから入力された変数をそのままSQL文の中に展開しているので,**SQLインジェクション攻撃**に対する致命的な脆弱性がある.この脆弱性は1つもあってはいけないレベルで危険.そもそもPDOの使い方を間違っているので,正しい用法を覚える.
70
-
71
- - `htmlspecialchars`を通さずにHTML内にテキストデータを表示しようとしている場所がある.**XSS攻撃**に対する脆弱性になり得るので,常に通すように心がけるべき.
72
-
73
-
74
-
75
- ## 誤作動やエラーの防止
76
-
77
-
78
-
79
- - 変数未定義のエラーを防ぐために,`$_POST`の存在確認を`isset`で行うべき.欲を言えば更に`is_string`で文字列かどうかの確認を行うべき.これらをまとめて行ってくれる,`filter_input`の利用が推奨される.
80
-
81
- - `SET NAMES utf8`はデータベース側の文字セットしか変更できないため,基本的には,PDOドライバ側も同時に変更できる,DSNでの`charset=utf8`指定を利用すべき.
82
-
83
- - エラーモードがデフォルトの`PDO::ERRMODE_SILENT`のままなので,`->prepare()`のコールに失敗した場合にPDOExceptionが発生せず,単にfalseが返されるだけになってしまっている.エラーモードを`PDO::ERRMODE_EXCEPTION`に変更しない限りは,いちいち返り値をチェックする必要がある.
84
-
85
- - LIKE検索のためのエスケープを行っていないため,`%` `_` `\` といった特殊文字を含むキーワードを正しく取り扱えない.
86
-
87
- - SELECTの結果に対して`->rowCount()`は使えず,常にゼロになってしまうため,結果を配列として取得したあと配列に対して`count()`を実行する.
88
-
89
-
90
-
91
- ## 読みやすいコードにするためには必要
92
-
93
-
94
-
95
- - HTMLの表示内容はほとんど重複するにも関わらず,絞り込み検索を行うためだけにPHPファイルを複製している.このような場合は「絞り込み条件が送信されてきたか」で判定し,HTML表示部分は共通化するだけで十分.
96
-
97
- - PHPのテンプレートエンジンとしての特性を無視して,`print`でHTMLタグ全体を出力している部分がある.基本的には変数に依存する部分のみを `<?= ~ ?>` 記法で出力するだけで十分.
98
-
99
- - ロジックとHTMLを可能な限り分離すべき.**「あらかじめ必要な変数を作っておき,すべての変数が用意できたらHTMLを書き始める」**というやり方を徹底する.`<body>`まで書いてからデータベースに接続して…,というのは典型的にダメな書き方.また,HTMLを途中まで書いておいて最後まで書き終わる前に`die`で強制終了するのNG.
100
-
101
-
102
-
103
- ## 改善済みのコード
104
-
105
-
106
-
107
- ```html
108
-
109
- <?php
110
-
111
-
112
-
113
- // HTML中にテキストを埋め込むときに利用する関数
114
-
115
- function h($str)
116
-
117
- {
118
-
119
- return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
120
-
121
- }
122
-
123
-
124
-
125
- // $_POST['vid'], $_POST['nm'], $_POST['ndt'] を安全に文字列として受け取る
126
-
127
- // もし存在していない場合,空文字列になる
128
-
129
- $vid = (string)filter_input(INPUT_POST, 'vid');
130
-
131
- $nm = (string)filter_input(INPUT_POST, 'nm');
132
-
133
- $ndt = (string)filter_input(INPUT_POST, 'ndt');
134
-
135
-
136
-
137
- try {
138
-
139
-
140
-
141
- // localhostのkadaiデータベースに文字コードutf8で接続
142
-
143
- // SQL実行時のエラーもPDOExceptionに変換してスローさせる
144
-
145
- $pdo = new PDO('mysql:dbname=kadai;host=localhost;charset=utf8', 'root', '', [
146
-
147
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
148
-
149
- ]);
150
-
151
-
152
-
153
- // 共通のSQL文と空のパラメータ用配列を準備する
154
-
155
- $sql = '
156
-
157
- SELECT id, name, address, tel1, tel2, email, ndate, remarks
158
-
159
- FROM mst_member
160
-
161
- WHERE 1 = 1
162
-
163
- ';
164
-
165
- $params = [];
166
-
167
-
168
-
169
- // $vid が入力されていたら,AND条件としてプレースホルダ「?」を追加し.
170
-
171
- // そこに文字列として当てはめるための値を $params に追加する
172
-
173
- if ($vid !== '') {
174
-
175
- $sql .= ' AND id = ? ';
176
-
177
- $params[] = $vid;
178
-
179
- }
180
-
181
-
182
-
183
- // $nm が入力されていたら,AND条件としてプレースホルダ「?」を追加し.
184
-
185
- // そこに文字列として当てはめるための値を $params に追加する
186
-
187
- // 名前に関してはLIKE検索を行うので,LIKE検索で使われる特殊文字を
188
-
189
- // addcslashes関数を使ってエスケープしたあと,両端を%で囲んだ値を使用する
190
-
191
- if ($nm !== '') {
192
-
193
- $sql .= ' AND name like ? ';
194
-
195
- $params[] = '%' . addcslashes($nm, '\_%') . '%';
196
-
197
- }
198
-
199
-
200
-
201
- // $hdt が入力されていたら,AND条件としてプレースホルダ「?」を追加し.
202
-
203
- // そこに文字列として当てはめるための値を $params に追加する
204
-
205
- if ($ndt !== '') {
206
-
207
- $sql .= ' AND ndate >= ? ';
208
-
209
- $params[] = $ndt;
210
-
211
- }
212
-
213
-
214
-
215
- // SQLを実行し,すべての結果を2次元の配列として一気に取得
216
-
217
- $stmt = $pdo->prepare($sql);
218
-
219
- $stmt->execute($params);
220
-
221
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
222
-
223
-
224
-
225
- } catch (PDOException $e) {
226
-
227
-
228
-
229
- // もしどこかでエラーが発生した場合,「500 Internal Server Error」として
230
-
231
- // テキスト形式でエラーメッセージを表示して終了する
232
-
233
- header('Content-Type: text/plain; charset=UTF-8', true, 500);
234
-
235
- echo "ただいま障害により大変ご迷惑をお掛けしております。\n";
236
-
237
- echo "Error: {$e->getMessage()}\n";
238
-
239
- exit;
240
-
241
-
242
-
243
- }
244
-
245
-
246
-
247
- // エラーが起こらなければHTMLとして表示を開始する
248
-
249
- header('Content-Type: text/html; charset=UTF-8');
250
-
251
-
252
-
253
- ?>
254
-
255
- <!DOCTYPE html>
256
-
257
- <title>名簿一覧</title>
258
-
259
- <h1>名簿一覧</h1>
260
-
261
- <form method="post" action="">
262
-
263
- ID(半角) <input type="text" name="vid" value="<?=h($vid)?>"><br>
264
-
265
- 氏名 <input type="text" name="nm" value="<?=h($nm)?>"><br>
266
-
267
- 入会日 <input type="date" name="ndt" value="<?=($ndt)?>"><br>
268
-
269
- <input type="submit" value="検索">
270
-
271
- </form>
272
-
273
- <?php if ($_SERVER['REQUEST_METHOD'] === 'POST'): /* POSTされたときだけ結果件数を示す */ ?>
274
-
275
- <p>検索結果は<?=h(count($rows))?>件です</p>
276
-
277
- <?php endif; ?>
278
-
279
- <table border="1">
280
-
281
- <tr>
282
-
283
- <th>ID</th>
284
-
285
- <th>名前</th>
286
-
287
- <th>住所</th>
288
-
289
- <th>固定電話番号</th>
290
-
291
- <th>携帯電話番号</th>
292
-
293
- <th>Eメールアドレス</th>
294
-
295
- <th>入会日</th>
296
-
297
- <th>備考</th>
298
-
299
- </tr>
300
-
301
- <?php foreach ($rows as $row): ?>
302
-
303
- <tr>
304
-
305
- <td><?=h($row['id'])?></td>
306
-
307
- <td><?=h($row['name'])?></td>
308
-
309
- <td><?=h($row['address'])?></td>
310
-
311
- <td><?=h($row['tel1'])?></td>
312
-
313
- <td><?=h($row['tel2'])?></td>
314
-
315
- <td><?=h($row['email'])?></td>
316
-
317
- <td><?=h($row['ndate'])?></td>
318
-
319
- <td><?=h($row['remarks'])?></td>
320
-
321
- </tr>
322
-
323
- <?php endforeach; ?>
324
-
325
- </table>
326
-
327
- ```
328
-
329
-
330
-
331
279
  ## 参考リンク
332
280
 
333
281