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:
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user