- Kiến thức nền tảng
- Cấu trúc dự án mẫu
- Http Request/Response siêu gọn
- Helper CSRF: sinh, lấy, verify token
- Helper HTML: auto-escape và in input ẩn CSRF
- Middleware CSRF: chỉ kiểm với phương thức thay đổi trạng thái
- Middleware Security Headers + CSP nonce
- Bootstrap mini-router và gắn middleware
- View demo: form có CSRF và anti-XSS
- Hướng dẫn gắn token vào AJAX và form
- Chiến lược giảm XSS
- Kiểm thử bảo mật
- Thực hành tốt nhất
- Kết luận
Kiến thức nền tảng
- CSRF: kẻ tấn công lợi dụng cookie đăng nhập của nạn nhân để gửi yêu cầu trái phép. Giải pháp chuẩn là synchronizer token: server phát sinh token ngẫu nhiên, gắn vào form hoặc header; server chỉ chấp nhận request có token hợp lệ.
- XSS: chèn script độc hại vào DOM hoặc HTML trả về. Cốt lõi là escape output theo ngữ cảnh và áp Content-Security-Policy hạn chế nguồn script.
Cấu trúc dự án mẫu
.
├─ public/
│ ├─ index.php // bootstrap + router + middleware
│ └─ form.php // view demo (GET)
└─ src/
├─ Middleware/
│ ├─ CsrfMiddleware.php
│ └─ SecurityHeadersMiddleware.php
├─ Support/
│ ├─ Csrf.php
│ └─ Html.php
└─ Http/
├─ Request.php
└─ Response.php
Http Request/Response siêu gọn
<?php
// src/Http/Request.php
namespace App\Http;
class Request {
public string $method;
public string $uri;
public array $get;
public array $post;
public array $server;
public array $cookies;
public array $headers;
public function __construct() {
$this->method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$this->uri = strtok($_SERVER['REQUEST_URI'] ?? '/', '?');
$this->get = $_GET;
$this->post = $_POST;
$this->server = $_SERVER;
$this->cookies = $_COOKIE;
$this->headers = function_exists('getallheaders') ? (getallheaders() ?: []) : [];
}
public function input(string $key, $default = null) {
return $this->post[$key] ?? $this->get[$key] ?? $default;
}
public function header(string $key, $default = null) {
foreach ($this->headers as $k => $v) {
if (strcasecmp($k, $key) === 0) return $v;
}
return $default;
}
public function isSafeMethod(): bool {
return in_array($this->method, ['GET','HEAD','OPTIONS'], true);
}
}
<?php
// src/Http/Response.php
namespace App\Http;
class Response {
private int $status = 200;
private array $headers = [];
private string $body = '';
public function status(int $code): self { $this->status = $code; return $this; }
public function header(string $name, string $value): self { $this->headers[$name] = $value; return $this; }
public function write(string $content): self { $this->body .= $content; return $this; }
public function send(): void {
http_response_code($this->status);
foreach ($this->headers as $k => $v) header("$k: $v");
echo $this->body;
}
}
Helper CSRF: sinh, lấy, verify token
<?php
// src/Support/Csrf.php
namespace App\Support;
final class Csrf {
public const SESSION_KEY = '_csrf_token';
public const FORM_KEY = '_csrf';
public static function ensureSessionStarted(): void {
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start([
'cookie_httponly' => true,
'cookie_samesite' => 'Strict',
'cookie_secure' => isset($_SERVER['HTTPS']),
]);
}
}
public static function token(): string {
self::ensureSessionStarted();
if (empty($_SESSION[self::SESSION_KEY])) {
$_SESSION[self::SESSION_KEY] = self::generate();
}
return $_SESSION[self::SESSION_KEY];
}
private static function generate(): string {
return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
}
public static function validate(?string $provided): bool {
self::ensureSessionStarted();
$stored = $_SESSION[self::SESSION_KEY] ?? '';
return is_string($provided) && is_string($stored) && hash_equals($stored, $provided);
}
public static function rotate(): void {
// tùy chọn: xoay token sau khi verify thành công để giảm rủi ro replay
$_SESSION[self::SESSION_KEY] = self::generate();
}
}
Helper HTML: auto-escape và in input ẩn CSRF
<?php
// src/Support/Html.php
namespace App\Support;
final class Html {
public static function e($value): string {
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', false);
}
public static function csrfInput(): string {
$t = Csrf::token();
return '<input type="hidden" name="'.self::e(Csrf::FORM_KEY).'" value="'.self::e($t).'">';
}
}
Middleware CSRF: chỉ kiểm với phương thức thay đổi trạng thái
<?php
// src/Middleware/CsrfMiddleware.php
namespace App\Middleware;
use App\Http\Request;
use App\Http\Response;
use App\Support\Csrf;
final class CsrfMiddleware {
public function __invoke(Request $req, Response $res, callable $next): Response {
if ($req->isSafeMethod()) {
// Tạo sẵn token cho view
Csrf::token();
return $next($req, $res);
}
// Lấy token từ form hoặc header
$token = $req->input(\App\Support\Csrf::FORM_KEY)
?: $req->header('X-CSRF-Token');
if (!Csrf::validate($token)) {
return $res->status(403)
->header('Content-Type', 'text/plain; charset=utf-8')
->write('Forbidden: CSRF verification failed');
}
// tùy chọn: xoay token sau khi verify
Csrf::rotate();
return $next($req, $res);
}
}
Middleware Security Headers + CSP nonce
<?php
// src/Middleware/SecurityHeadersMiddleware.php
namespace App\Middleware;
use App\Http\Request;
use App\Http\Response;
final class SecurityHeadersMiddleware {
public static string $nonce;
public function __invoke(Request $req, Response $res, callable $next): Response {
// Nonce cho script an toàn
self::$nonce = rtrim(strtr(base64_encode(random_bytes(16)), '+/', '-_'), '=');
$res = $next($req, $res);
$csp = [
"default-src 'self'",
"script-src 'self' 'nonce-".self::$nonce."'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"base-uri 'self'",
"object-src 'none'",
"frame-ancestors 'self'",
"upgrade-insecure-requests"
];
return $res
->header('Content-Security-Policy', implode('; ', $csp))
->header('X-Content-Type-Options', 'nosniff')
->header('X-Frame-Options', 'SAMEORIGIN')
->header('Referrer-Policy', 'strict-origin-when-cross-origin')
->header('X-XSS-Protection', '0');
}
public static function nonceAttr(): string {
return " nonce=\"".self::$nonce."\"";
}
}
Bootstrap mini-router và gắn middleware
<?php
// public/index.php
require __DIR__.'/../src/Http/Request.php';
require __DIR__.'/../src/Http/Response.php';
require __DIR__.'/../src/Support/Csrf.php';
require __DIR__.'/../src/Support/Html.php';
require __DIR__.'/../src/Middleware/CsrfMiddleware.php';
require __DIR__.'/../src/Middleware/SecurityHeadersMiddleware.php';
use App\Http\Request;
use App\Http\Response;
use App\Support\Html;
use App\Middleware\CsrfMiddleware;
use App\Middleware\SecurityHeadersMiddleware;
$req = new Request();
$res = new Response();
$pipeline = [
new SecurityHeadersMiddleware(),
new CsrfMiddleware(),
];
$next = array_reduce(
array_reverse($pipeline),
fn($next, $mw) => fn($req, $res) => $mw($req, $res, $next),
function(Request $req, Response $res): Response {
// Router tối giản
if ($req->method === 'GET' && $req->uri === '/') {
ob_start();
include __DIR__.'/form.php';
$html = ob_get_clean();
return $res->header('Content-Type', 'text/html; charset=utf-8')->write($html);
}
if ($req->method === 'POST' && $req->uri === '/submit') {
// Xử lý dữ liệu đã qua CSRF middleware
$username = $req->input('username', '');
// Escape khi render lại HTML
return $res->header('Content-Type', 'text/html; charset=utf-8')
->write('<p>Gửi thành công, user: '.Html::e($username).'</p>');
}
return $res->status(404)->header('Content-Type','text/plain; charset=utf-8')->write('Not Found');
}
);
$next($req, $res)->send();
View demo: form có CSRF và anti-XSS
<?php
// public/form.php
use App\Support\Html;
use App\Middleware\SecurityHeadersMiddleware;
$nonceAttr = SecurityHeadersMiddleware::nonceAttr();
?>
<!— Không dùng <head> ở bài blog, nhưng file chạy thật vẫn là HTML hoàn chỉnh —>
<h2>Demo form có CSRF token + auto-escape</h2>
<form method="post" action="/submit">
<?= Html::csrfInput() ?>
<label>Username</label>
<input type="text" name="username" required>
<button type="submit">Gửi</button>
</form>
<script<?= $nonceAttr ?>>
// Ví dụ thêm CSRF vào Fetch API (AJAX)
(function(){
const tokenInput = document.querySelector('input[name="_csrf"]');
window.apiPost = function(url, data) {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': tokenInput ? tokenInput.value : ''
},
credentials: 'same-origin',
body: JSON.stringify(data)
});
};
})();
</script>
Hướng dẫn gắn token vào AJAX và form
- Với form HTML, chỉ cần in
Html::csrfInput()để có input ẩn_csrf. - Với Fetch/AJAX, gửi header
X-CSRF-Tokenbằng cách đọc từ input ẩn đã render. - Middleware sẽ chặn mọi phương thức POST/PUT/PATCH/DELETE không có token hợp lệ.
Chiến lược giảm XSS
- Escape output đúng ngữ cảnh: dùng
Html::e()khi in dữ liệu người dùng vào HTML. - Dùng CSP với nonce: chỉ cho phép script nội bộ có thuộc tính
nonce. Middleware đã đặt header và cung cấpnonceAttr(). - Không chèn HTML thô từ input. Nếu cần rich text, dùng sanitizer chuyên dụng ở server.
- Tránh eval, new Function, inline event handler vì bị CSP chặn hoặc dễ gây lỗ hổng.
Kiểm thử bảo mật
- Mở trang demo, gửi form bình thường: phải thành công.
- Mở tab mới, POST tới
/submitthiếu_csrfhoặc sai header: phải bị 403. - Thử nhập username chứa ký tự đặc biệt như
<script>alert(1)</script>: trang phản hồi phải in ra dạng đã escape, không chạy script. - Kiểm tra response header có CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy.
Thực hành tốt nhất
- SameSite cookie ở session đã bật Strict trong ví dụ, nhưng đừng chỉ dựa vào nó; CSRF token vẫn bắt buộc.
- Rotate token sau khi xác thực để giảm nguy cơ token reuse.
- Tách view/template và luôn auto-escape mặc định, chỉ cho phép “raw” ở các vùng đã được sanitize.
- Audit định kỳ các chỗ in dữ liệu người dùng và endpoint state-changing.
Kết luận
Chỉ với vài file nhỏ gọn, ta đã có middleware chống CSRF và XSS chạy tốt cho PHP thuần: token đồng bộ cho form và AJAX, CSP header kèm nonce, và auto-escape output. Bạn có thể nhúng trực tiếp vào dự án hiện có hoặc đóng gói thành thư viện nội bộ để tái sử dụng.
Bình luận