「杏林同學錄」的個人管理模組包含三個頁面:class/address.php(班級通訊錄)、class/modify.php(個人資料修改)、class/uploadphoto.php(照片上傳)。系統採用 Session 驗證登入狀態,直接操作 MySQL user 表。
address.phpSELECT * FROM user 取得所有會員。<?php
$result = mysql_query("SELECT * FROM user", $db);
$row = mysql_num_rows($result);
for ($i = 0; $i <= ($row - 1); $i++) {
// 逐欄位 mysql_result() 取值,效能極差
$name = mysql_result($result, $i, 'name');
$sex = mysql_result($result, $i, 'sex');
// ... 共 14 次 mysql_result() 呼叫/筆
$face = 'image/face/icon' . $face; // 圖片路徑拼接
echo "<table>...<img src='$face.gif'>姓名:$name...</table>"; // 直接輸出未轉義
}
?>
mysql_result() 每次呼叫都定位資料指標,N 筆資料 × M 欄位 = N×M 次函式呼叫。應用 mysql_fetch_assoc() 一次取得整列關聯陣列。echo 輸出,未 htmlspecialchars()。<table>,導致巢狀表格無效。image/face/icon{$face}.gif 假設檔案存在,無預設圖片機制。modify.phpuser 表,寄送確認信,顯示成功訊息。<?php
if ($submit) {
// 1. 必填檢查
if (!$username || !$psw || !$psw2 || !$name || !$ad || !$ph || !$email) {
echo "必填欄位不完整"; exit;
}
// 2. 密碼確認
if ($psw !== $psw2) { echo "密碼不一致"; exit; }
// 3. Email 正則(過度複雜且可能誤判)
if (!ereg('^[-!#$%&\'*+./0-9=?A-Z^_`a-z{|}~]+@[-!#$%&\'*+\/0-9=?A-Z^_`a-z{|}~]+\.[-!#$%&\'*+\./0-9=?A-Z^_`a-z{|}~]+$', $email)) {
echo "Email 不合法"; exit;
}
// 4. 更新資料庫(直接拼接,SQL Injection 高風險)
$sql = "UPDATE user SET user='$user', psw='$psw', name='$name', sex='$sex', birth='$birth',
work='$work', ad='$ad', post='$post', ph='$ph', bp='$bp', email='$email', oicq='$oicq',
account='$account', signature='$signature', face='$face' WHERE user='$userregister'";
mysql_query($sql, $db);
// 5. 寄送確認信(含明文密碼!)
mail($email, "祝賀修改成功", "您的密碼為 $psw...", $headers);
echo "修改成功!";
exit;
}
?>
$user、$psw 等可注入任意 SQL。ereg() 已棄用(PHP 5.3+),正則過度嚴格可能誤攔合法信箱(如含 +、子網域)。value="=$name?>" 未 htmlspecialchars(),屬性注入風險。uploadphoto.php$_FILES['picurl'] 上傳檔案。image/ 目錄存在否,不存在則 mkdir()。copy($picurl, "image/$picurl_name") 將暫存檔移至目標(應用 move_uploaded_file())。photo 欄位,追加新連結 <a href='upload/image/...'>看照片</a>| 寫回資料庫。unlink($picurl)。<?php
if ($upload) {
if ($picurl == 'none') { echo "未上傳檔案"; exit; }
$v = opendir('image');
if (!$v) { mkdir('image'); $v = opendir('image'); }
// 使用 copy() 而非 move_uploaded_file(),無法確保檔案真為上傳檔
$up = copy($picurl, "image/$picurl_name");
if ($up) {
$result = mysql_query("SELECT photo FROM user WHERE user='$userregister'", $db);
$photo = mysql_result($result, 0, 'photo');
$photo = str_replace("'", "''", $photo); // 手動跳脫單引號,不完全
$photo .= "<a href='upload/image/$picurl_name'>看照片</a>|";
mysql_query("UPDATE user SET photo='$photo' WHERE user='$userregister'", $db);
echo "上傳成功!檔名:$picurl_name 大小:$picurl_size 類型:$picurl_type";
} else { echo "上傳失敗"; }
unlink($picurl);
closedir($v);
exit;
}
?>
accept="image/*" 前端限制,伺服端未檢查 MIME、副檔名、魔術位元組,可上傳 .php 執行檔導致 RCE。$picurl_name(用戶端檔名),可含 ../ 路徑遍歷、特殊字元。copy() 非 move_uploaded_file():無法保證來源為合法上傳檔案。photo 欄位存累積的 <a> 標籤字串,查詢、刪除、維護極度困難。mkdir('image') 預設 0777,且上傳目錄可直接 HTTP 存取,若上傳 PHP 檔可直接執行。-- 用戶主表
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
nickname VARCHAR(50) NOT NULL,
sex ENUM('M','F') NULL,
birth DATE NULL,
phone VARCHAR(30) NULL,
pager VARCHAR(30) NULL,
email VARCHAR(100) NULL,
oicq VARCHAR(20) NULL,
workplace VARCHAR(100) NULL,
zipcode VARCHAR(10) NULL,
address VARCHAR(200) NULL,
bio TEXT NULL,
signature TEXT NULL,
face_id TINYINT UNSIGNED DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 用戶相簿(獨立表,支援多張照片、標題、排序)
CREATE TABLE user_photos (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
filename VARCHAR(255) NOT NULL, -- 系統產生唯一檔名
original_name VARCHAR(255) NOT NULL, -- 用戶原始檔名
mime_type VARCHAR(50) NOT NULL,
size_bytes BIGINT UNSIGNED NOT NULL,
sort_order INT UNSIGNED DEFAULT 0,
is_avatar BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_created (user_id, created_at)
);
<?php
// upload_photo.php
session_start();
require 'db.php';
require 'csrf.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') exit;
if (!validate_csrf($_POST['csrf'] ?? '')) exit('CSRF failed');
if (empty($_SESSION['user_id'])) exit('Unauthorized');
$file = $_FILES['photo'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
echo json_encode(['success' => false, 'error' => '上傳失敗或無檔案']);
exit;
}
// 1. 檔案大小限制(2MB)
if ($file['size'] > 2 * 1024 * 1024) {
echo json_encode(['success' => false, 'error' => '檔案不得超過 2MB']);
exit;
}
// 2. MIME 型別驗證(使用 finfo)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mime, $allowedMimes, true)) {
echo json_encode(['success' => false, 'error' => '僅允許 JPG/PNG/GIF/WebP 格式']);
exit;
}
// 3. 檯面名稱安全化
$ext = match ($mime) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
};
$safeName = bin2hex(random_bytes(16)) . '.' . $ext; // 隨機檔名
$saveDir = __DIR__ . '/../uploads/photos/' . date('Y/m/');
if (!is_dir($saveDir)) mkdir($saveDir, 0755, true);
$savePath = $saveDir . $safeName;
// 4. 移動檔案
if (!move_uploaded_file($file['tmp_name'], $savePath)) {
echo json_encode(['success' => false, 'error' => '檔案儲存失敗']);
exit;
}
// 5. 寫入資料庫
$relativePath = 'uploads/photos/' . date('Y/m/') . $safeName;
$stmt = $pdo->prepare("
INSERT INTO user_photos (user_id, filename, original_name, mime_type, size_bytes)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([$_SESSION['user_id'], $relativePath, $file['name'], $mime, $file['size']]);
echo json_encode([
'success' => true,
'photo' => [
'id' => $pdo->lastInsertId(),
'url' => '/'.$relativePath,
'name' => $file['name']
]
]);
?>
<?php
// api/address_book.php
session_start();
require 'db.php';
$page = max((int)($_GET['page'] ?? 1), 1);
$perPage = min(max((int)($_GET['per_page'] ?? 20), 10), 100);
$offset = ($page - 1) * $perPage;
$stmt = $pdo->prepare("
SELECT id, nickname, sex, birth, phone, email, oicq, workplace, zipcode, address, bio, face_id
FROM users ORDER BY nickname LIMIT :offset, :perPage
");
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->bindValue(':perPage', $perPage, PDO::PARAM_INT);
$stmt->execute();
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 總數
$total = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
header('Content-Type: application/json');
echo json_encode([
'data' => $users,
'pagination' => [
'page' => $page,
'per_page' => $perPage,
'total' => $total,
'total_pages' => (int)ceil($total / $perPage)
]
]);
?>
「杏林同學錄」的個人管理模組雖實作了通訊錄、資料修改、照片上傳完整流程,但存在SQL Injection、XSS、明文密碼郵件、檔案上傳無驗證、資料結構反正規化、效能災難(mysql_result)、無分頁、無 CSRF 保護等嚴重缺陷。重構應採用PDO 預備語句、輸出轉義、密碼雜湊、安全檔案上傳流程、資料表正規化、分頁 API、CSRF Token等現代最佳實務,並引入圖片處理庫(縮圖、格式轉換)、雲端儲存(S3)等擴充基礎設施。