204 lines
8.6 KiB
PHP
204 lines
8.6 KiB
PHP
<?php
|
|
|
|
namespace Tests\Browser;
|
|
|
|
use App\Models\Role;
|
|
use App\Models\User;
|
|
use App\Models\UserRole;
|
|
use Laravel\Dusk\Browser;
|
|
use Tests\DuskTestCase;
|
|
|
|
/**
|
|
* SecurityTest — Kiểm thử bảo mật phân quyền và chống tấn công XSS.
|
|
*
|
|
* [TC-53] test_regular_user_cannot_access_admin_area
|
|
* [TC-54] test_xss_injection_on_registration_form
|
|
*/
|
|
class SecurityTest extends DuskTestCase
|
|
{
|
|
private User $regularUser;
|
|
private User $adminUser;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// User thường — KHÔNG có role admin (không có bản ghi UserRole)
|
|
$phone = '09' . rand(10000000, 99999999);
|
|
$this->regularUser = User::create([
|
|
'name' => 'DUSK REGULAR USER',
|
|
'phone' => $phone,
|
|
'email' => $phone . '@phone.local',
|
|
'password' => bcrypt('test123'),
|
|
'address' => 'TP.HCM',
|
|
'known_sis' => 0,
|
|
'job' => 'Người dùng thường',
|
|
'status' => 1,
|
|
'spined' => 0,
|
|
]);
|
|
|
|
// Admin user — có đầy đủ roles
|
|
$stale = User::where('email', 'dusk_secure_admin@test.local')->first();
|
|
if ($stale) {
|
|
UserRole::where('user_id', $stale->id)->delete();
|
|
$stale->delete();
|
|
}
|
|
$this->adminUser = User::create([
|
|
'name' => 'DUSK SECURE ADMIN',
|
|
'email' => 'dusk_secure_admin@test.local',
|
|
'phone' => '09' . rand(10000000, 99999999),
|
|
'password' => bcrypt('dusk_admin_pwd_2026'),
|
|
'status' => 1,
|
|
'address' => 'Test',
|
|
'known_sis' => 0,
|
|
'job' => 'Admin',
|
|
'spined' => 0,
|
|
]);
|
|
$roles = Role::where('role_status', 1)->get();
|
|
foreach ($roles as $role) {
|
|
UserRole::create(['user_id' => $this->adminUser->id, 'role_id' => $role->id]);
|
|
}
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
UserRole::where('user_id', $this->adminUser->id)->delete();
|
|
$this->adminUser->delete();
|
|
$this->regularUser->delete();
|
|
parent::tearDown();
|
|
}
|
|
|
|
// =========================================================================
|
|
// [TC-53] Người dùng thường không thể truy cập khu vực admin
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Người dùng đã đăng nhập nhưng KHÔNG có role admin bị redirect về /admin
|
|
* khi cố truy cập bất kỳ route nào trong middleware UserRole.
|
|
*
|
|
* Middleware UserRole (app/Http/Middleware/UserRole.php):
|
|
* - Kiểm tra constructMenuLeft() → lấy danh sách route được phép từ roles
|
|
* - Nếu route hiện tại không trong danh sách → redirect('admin')
|
|
* - User thường không có UserRole record → menu rỗng → TẤT CẢ admin route bị chặn
|
|
*/
|
|
public function test_regular_user_cannot_access_admin_area(): void
|
|
{
|
|
$this->browse(function (Browser $browser) {
|
|
// Đăng nhập là user thường (không có role nào)
|
|
$browser->loginAs($this->regularUser);
|
|
|
|
// Thử truy cập /admin/account → phải bị redirect về /admin (trang login admin)
|
|
$browser->visit('/admin/account')
|
|
->assertPathIs('/admin');
|
|
|
|
// Thử truy cập /admin/users → tương tự bị redirect
|
|
$browser->visit('/admin/users')
|
|
->assertPathIs('/admin');
|
|
|
|
// Thử truy cập /admin/prizes → bị redirect
|
|
$browser->visit('/admin/prizes')
|
|
->assertPathIs('/admin');
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// [TC-54] XSS Injection vào form đăng ký — output phải được escape đúng cách
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Nhập XSS payload vào trường `name` khi đăng ký. Ứng dụng phải:
|
|
* 1. Lưu dữ liệu an toàn (không thực thi script)
|
|
* 2. Render escaped output trong admin — Blade {{ }} tự escape HTML entities
|
|
*
|
|
* Luồng kiểm tra:
|
|
* - Frontend: đăng ký với name = <script>alert('XSS')</script>
|
|
* - Controller: uppercase → <SCRIPT>ALERT('XSS')</SCRIPT> rồi lưu DB
|
|
* - Admin view (table.blade.php): {{ $item->name ?? '' }} → render escaped
|
|
* - Kết quả: source HTML chứa <SCRIPT> thay vì tag <script> thực
|
|
*/
|
|
public function test_xss_injection_on_registration_form(): void
|
|
{
|
|
$xssPhone = '09' . rand(10000000, 99999999);
|
|
$xssPayload = "<script>alert('XSS')</script>";
|
|
|
|
// NOTE: mb_strtoupper converts the name to <SCRIPT>ALERT('XSS')</SCRIPT>.
|
|
// 'ALERT' is NOT a valid JS function (case-sensitive) → no JS dialog fires from the payload.
|
|
// The only alerts that CAN appear are from other sources (e.g. DataTables warnings).
|
|
// This test checks XSS protection via HTML source inspection, not via alert detection.
|
|
|
|
try {
|
|
$this->browse(function (Browser $browser) use ($xssPhone, $xssPayload) {
|
|
|
|
// ----------------------------------------------------------------
|
|
// BƯỚC 1: Đăng ký tài khoản với XSS payload trong trường name
|
|
// ----------------------------------------------------------------
|
|
$browser->visit('/logout')
|
|
->visit('/login')
|
|
->type('name', $xssPayload)
|
|
->type('phone', $xssPhone)
|
|
->type('address', 'Test City')
|
|
->radio('known_sis', '0')
|
|
->type('job', 'Tester')
|
|
->press('Gửi thông tin')
|
|
->waitForText('thành công', 10)
|
|
->assertPathIs('/game');
|
|
|
|
$browser->visit('/logout');
|
|
|
|
// ----------------------------------------------------------------
|
|
// BƯỚC 2: Đăng nhập admin và mở trang danh sách user
|
|
// ----------------------------------------------------------------
|
|
$browser->visit('/admin')
|
|
->type('email', 'dusk_secure_admin@test.local')
|
|
->type('password', 'dusk_admin_pwd_2026')
|
|
->press('Login')
|
|
->waitForLocation('/admin/account', 10);
|
|
|
|
// Dismiss any alert that appears on the account page (e.g. DataTables warnings).
|
|
// CRITICAL: Always dismiss before proceeding — leaving an alert open crashes ChromeDriver.
|
|
$this->dismissAnyAlert($browser);
|
|
|
|
$browser->visit('/admin/users')
|
|
->waitFor('#table', 5);
|
|
|
|
// Dismiss any alert on the users page (DataTables may fire errMode alerts
|
|
// if HTML content in cells triggers a parsing/column-count error).
|
|
$this->dismissAnyAlert($browser);
|
|
|
|
// ----------------------------------------------------------------
|
|
// ASSERTION: HTML source không chứa tag <script> thô (unescaped)
|
|
// Blade {{ }} chuyển <SCRIPT>ALERT(...)</SCRIPT> thành:
|
|
// <SCRIPT>ALERT('XSS')</SCRIPT>
|
|
// Nên source page KHÔNG chứa literal <script> hay <SCRIPT> tags.
|
|
// ----------------------------------------------------------------
|
|
$browser->assertSourceMissing("<script>alert('XSS')</script>")
|
|
->assertSourceMissing("<SCRIPT>ALERT('XSS')</SCRIPT>");
|
|
|
|
// Kiểm tra dương: source phải chứa phiên bản đã được HTML-escape
|
|
$browser->assertSourceHas('<SCRIPT>');
|
|
});
|
|
|
|
} finally {
|
|
User::where('phone', $xssPhone)->delete();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper: Dismiss any open alert dialog silently.
|
|
* Prevents Chrome lockup when an unexpected alert (e.g. DataTables warning) appears.
|
|
*
|
|
* @param Browser $browser
|
|
* @param string|null &$alertText Will be populated with the alert text if one was found.
|
|
*/
|
|
private function dismissAnyAlert(Browser $browser, ?string &$alertText = null): void
|
|
{
|
|
try {
|
|
$alert = $browser->driver->switchTo()->alert();
|
|
$alertText = $alert->getText();
|
|
$alert->accept();
|
|
} catch (\Facebook\WebDriver\Exception\NoSuchAlertException $e) {
|
|
$alertText = null; // No alert present — expected
|
|
}
|
|
}
|
|
}
|