feat: add BlogBodySanitizer for HTML sanitization on article save

- Implemented BlogBodySanitizer to clean HTML content before saving articles, ensuring security against XSS attacks.
- Added tests for BlogBodySanitizer to verify that unsafe tags and attributes are stripped from the content.
- Introduced ApiLeastPrivilegeTest to ensure that unauthorized users cannot access sensitive API routes, maintaining strict access control.
This commit is contained in:
hamed
2026-08-07 21:13:38 +03:30
parent a4a24c51af
commit 6876135a53
114 changed files with 2067 additions and 269 deletions
+111
View File
@@ -0,0 +1,111 @@
<?php
namespace App\Tests\Blog;
use App\Blog\Repository\BlogRepository;
use App\Tests\ApiTestCase;
/**
* آدیت ۲۰۲۶-۰۸-۰۷: بدنهٔ مقاله بدون پاک‌سازی ذخیره می‌شد و پنل با
* `dangerouslySetInnerHTML` رندرش می‌کرد. پاک‌سازی حالا در لحظهٔ ذخیره است، پس
* این تست‌ها به‌جای خروجی، **آنچه در DB نشسته** را می‌سنجند.
*/
class BlogBodySanitizerTest extends ApiTestCase
{
private function storedBody(string $uuid): string
{
$this->em->clear();
return self::getContainer()->get(BlogRepository::class)->findByUuid($uuid)->getBody();
}
public function testScriptTagIsStrippedOnCreate(): void
{
$admin = $this->createUser(['ROLE_ADMIN']);
$res = $this->authJson('POST', '/api/v1/blog', $admin, [
'title' => 'مقالهٔ تست',
'body' => '<p>سلام</p><script>alert(1)</script>',
]);
$this->assertSame(201, $this->responseCode());
$body = $this->storedBody($res['data']['data']['uuid'] ?? $res['data']['uuid']);
$this->assertStringNotContainsString('<script', $body);
$this->assertStringNotContainsString('alert(1)', $body);
$this->assertStringContainsString('سلام', $body, 'متن سالم نباید حذف شود');
}
public function testInlineHandlerAndJavascriptUrlAreStripped(): void
{
$admin = $this->createUser(['ROLE_ADMIN']);
$res = $this->authJson('POST', '/api/v1/blog', $admin, [
'title' => 'مقالهٔ تست',
'body' => '<p onclick="steal()">متن</p><a href="javascript:alert(1)">لینک</a>'
. '<img src="x" onerror="alert(2)">',
]);
$this->assertSame(201, $this->responseCode());
$body = $this->storedBody($res['data']['data']['uuid'] ?? $res['data']['uuid']);
$this->assertStringNotContainsString('onclick', $body);
$this->assertStringNotContainsString('onerror', $body);
$this->assertStringNotContainsString('javascript:', $body);
$this->assertStringContainsString('متن', $body);
}
public function testSafeRichTextSurvives(): void
{
$admin = $this->createUser(['ROLE_ADMIN']);
$html = '<h2>عنوان</h2><p><strong>پررنگ</strong> و <em>کج</em></p>'
. '<ul><li>یک</li><li>دو</li></ul>'
. '<a href="https://example.com">پیوند</a>'
. '<table><tbody><tr><td>خانه</td></tr></tbody></table>';
$res = $this->authJson('POST', '/api/v1/blog', $admin, [
'title' => 'مقالهٔ تست',
'body' => $html,
]);
$this->assertSame(201, $this->responseCode());
$body = $this->storedBody($res['data']['data']['uuid'] ?? $res['data']['uuid']);
foreach (['<h2', '<strong', '<em', '<ul', '<li', '<a', '<table', '<td'] as $tag) {
$this->assertStringContainsString($tag, $body, "عنصر مجاز {$tag} نباید حذف شود");
}
$this->assertStringContainsString('https://example.com', $body);
// لینک باید rel امن بگیرد، وگرنه tabnabbing باز می‌ماند.
$this->assertStringContainsString('noopener', $body);
}
public function testUpdatePathIsSanitizedToo(): void
{
$admin = $this->createUser(['ROLE_ADMIN']);
$res = $this->authJson('POST', '/api/v1/blog', $admin, [
'title' => 'مقالهٔ تست',
'body' => '<p>اولیه</p>',
]);
$uuid = $res['data']['data']['uuid'] ?? $res['data']['uuid'];
$this->authJson('PATCH', "/api/v1/blog/{$uuid}", $admin, [
'body' => '<p>ویرایش</p><script>alert(3)</script>',
]);
$this->assertSame(200, $this->responseCode());
$body = $this->storedBody($uuid);
$this->assertStringNotContainsString('<script', $body, 'مسیر ویرایش هم باید پاک‌سازی شود');
$this->assertStringContainsString('ویرایش', $body);
}
/** بدنه‌ای که چیزی جز markup ناامن ندارد، بعد از پاک‌سازی خالی است → ۴۲۲، نه ذخیره. */
public function testBodyThatIsOnlyUnsafeMarkupIsRejected(): void
{
$admin = $this->createUser(['ROLE_ADMIN']);
$this->authJson('POST', '/api/v1/blog', $admin, [
'title' => 'مقالهٔ تست',
'body' => '<script>alert(1)</script>',
]);
$this->assertSame(422, $this->responseCode());
}
}