線上編輯 PHP 檔案功能常用於 CMS 後台模板編輯、快速修改設定檔、教學示範環境。但這是極高風險的功能:一旦被攻擊者利用,可直接寫入 Web Shell 控制整個伺服器。本文實作一個簡易版本並詳細說明安全防護措施。
<?php
// 簡單的存取控制:固定密碼參數 (實際應用應用健全的認證授權)
if (($_GET['login'] ?? '') !== 'fuying') {
header('HTTP/1.1 403 Forbidden');
exit('存取被拒絕');
}
header('Content-Type: text/html; charset=utf-8');
// 處理 Magic Quotes (PHP 5.4+ 已移除,保留相容)
if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) {
foreach ($_POST as $k => $v) {
$_POST[$k] = stripslashes($v);
}
}
// 處理儲存
if (isset($_POST['file'], $_POST['text'])) {
$file = $_POST['file'];
$content = $_POST['text'];
// ⚠️ 關鍵安全檢查 (必須實作)
if (!isValidEditableFile($file)) {
die('非法檔案路徑');
}
$fp = @fopen($file, 'wb');
if ($fp) {
$bytes = fwrite($fp, $content);
fclose($fp);
$msg = $bytes !== false ? "✅ 儲存成功 ($bytes bytes)" : "❌ 寫入失敗";
} else {
$msg = "❌ 無法開啟檔案 (權限不足或路徑錯誤)";
}
}
// 讀取檔案內容供編輯
$editFile = $_GET['file'] ?? $_POST['file'] ?? __FILE__;
$content = file_exists($editFile) ? file_get_contents($editFile) : '';
// 防 XSS 輸出
$safeFile = htmlspecialchars($editFile);
$safeContent = htmlspecialchars($content);
$safeMsg = htmlspecialchars($msg ?? '');
?>
<!DOCTYPE html>
<html>
<head>
<title>PHP 線上編輯器</title>
<style>
body { font-family: monospace; padding: 20px; max-width: 1000px; margin: 0 auto; }
textarea { width: 100%; height: 500px; font-family: 'Monaco', 'Consolas', monospace; font-size: 14px; line-height: 1.5; }
.btn { padding: 8px 16px; background: #007bff; color: white; border: none; cursor: pointer; }
.msg { padding: 10px; margin: 10px 0; background: #f8f9fa; border-left: 4px solid #007bff; }
</style>
</head>
<body>
<h1>PHP 線上編輯器</h1>
<p><strong>⚠️ 警告:</strong>此功能極度危險,僅限受信任管理員在受控環境使用!</p>
<?php if ($safeMsg): ?>
<div class="msg"><?= $safeMsg ?></div>
<?php endif; ?>
<form method="post">
<input type="hidden" name="login" value="fuying" />
<div>
<label>檔案路徑: </label>
<input type="text" name="file" size="80" value="<?= $safeFile ?>" />
</div>
<br/>
<textarea name="text"><?= $safeContent ?></textarea>
<br/><br/>
<button type="submit" name="submit" class="btn">儲存</button>
</form>
</body>
</html>
function isValidEditableFile(string $path): bool {
// 1. 正規化路徑
$realPath = realpath($path);
if (!$realPath) return false; // 檔案不存在
// 2. 限制在允許目錄內 (防止目錄遍歷)
$allowedDirs = [
realpath(__DIR__ . '/templates'),
realpath(__DIR__ . '/config'),
realpath(__DIR__ . '/themes'),
];
$allowed = false;
foreach ($allowedDirs as $dir) {
if (strpos($realPath, $dir) === 0) {
$allowed = true;
break;
}
}
if (!$allowed) return false;
// 3. 限制副檔名
$allowedExt = ['php', 'html', 'htm', 'tpl', 'css', 'js', 'json', 'xml', 'txt', 'ini', 'yaml', 'yml'];
$ext = strtolower(pathinfo($realPath, PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExt)) return false;
// 4. 排除敏感檔案
$forbidden = ['config.php', 'database.php', '.env', 'bootstrap.php', 'autoload.php'];
if (in_array(basename($realPath), $forbidden)) return false;
// 5. 檢查寫入權限
if (!is_writable($realPath)) return false;
return true;
}
| 風險 | 對策 | 實作優先級 |
|---|---|---|
| 任意檔案寫入 (Web Shell) | 路徑白名單、副檔名白名單、內容掃描 | 🔴 必須 |
| 目錄遍歷 (../) | realpath() 正規化、strpos 檢查前綴 | 🔴 必須 |
| 權限提升 | 僅限特定管理員角色、IP 白名單、二次驗證 | 🔴 必須 |
| CSRF 攻擊 | 表單 Token、SameSite Cookie | 🟠 高 |
| XSS (編輯器內容) | 輸出時 htmlspecialchars、CSP 標頭 | 🟠 高 |
| 惡意程式碼執行 | 儲存前靜態分析 (PHP-Parser)、禁用危險函數 | 🟡 中 |
| 誤刪/誤改 | 版本控制 (Git 自動提交)、備份機制、操作日誌 | 🟡 中 |
| 編碼問題 | 統一 UTF-8、BOM 處理 | 🟢 低 |
// composer require nikic/php-parser
use PhpParser\ParserFactory;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Node\Expr\FuncCall;
function scanPhpCode(string $code): array {
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
try {
$ast = $parser->parse($code);
} catch (\Throwable $e) {
return ['error' => '語法錯誤: ' . $e->getMessage()];
}
$dangerous = [
'exec', 'shell_exec', 'system', 'passthru', 'proc_open', 'popen',
'eval', 'assert', 'create_function',
'file_put_contents', 'fopen', 'fwrite', 'unlink', 'rmdir',
'chmod', 'chown', 'chgrp',
'mail', 'header', 'setcookie',
'mysqli_query', 'pdo_exec', 'mysql_query',
'base64_decode', 'gzuncompress', 'str_rot13',
];
$found = [];
$traverser = new NodeTraverser();
$traverser->addVisitor(new class($dangerous, &$found) extends \PhpParser\NodeVisitorAbstract {
private $dangerous;
private $found;
public function __construct($dangerous, &$found) { $this->dangerous = $dangerous; $this->found = &$found; }
public function enterNode(\PhpParser\Node $node) {
if ($node instanceof FuncCall && $node->name instanceof \PhpParser\Node\Name) {
$name = strtolower($node->name->getLast());
if (in_array($name, $this->dangerous)) {
$this->found[] = ['function' => $name, 'line' => $node->getStartLine()];
}
}
}
});
$traverser->traverse($ast);
return $found;
}
// 使用
$issues = scanPhpCode($content);
if (!empty($issues)) {
die('檢測到危險函數: ' . json_encode($issues));
}
不要重造輪子,整合成熟的前端編輯器:
<!-- HTML -->
<div id="editor" style="height: 500px;"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs/loader.js"></script>
<script>
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs' } });
require(['vs/editor/editor.main'], function() {
window.editor = monaco.editor.create(document.getElementById('editor'), {
value: <?= json_encode($content) ?>,
language: 'php',
theme: 'vs-dark',
automaticLayout: true,
minimap: { enabled: true },
});
});
</script>
<!-- 儲存時 -->
<script>
document.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault();
const content = window.editor.getValue();
const res = await fetch('save.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file: '= $safeFile ?>', content })
});
alert(await res.text());
});
</script>
function autoGitCommit(string $file, string $message = 'Auto save'): bool {
$repoDir = dirname($file);
$fileName = basename($file);
// 檢查是否為 Git 倉庫
if (!is_dir($repoDir . '/.git')) return false;
$cmd = "cd " . escapeshellarg($repoDir) . " && git add " . escapeshellarg($fileName) . " && git commit -m " . escapeshellarg($message);
exec($cmd, $output, $returnVar);
return $returnVar === 0;
}
// 儲存後呼叫
if ($bytes !== false) {
autoGitCommit($file, "Web editor: update $file");
}
PHP 線上編輯器是把雙面刃:開發便利與安全風險並存。生產環境原則上應禁用,改用 Git 部署流程 (開發機編輯 → Commit → CI/CD 部署)。若業務必須提供,務必實施:嚴格路徑白名單、強認證授權、程式碼靜態掃描、操作審計日誌、自動版本備份。並考慮整合 Monaco Editor 等專業前端元件提升體驗。