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:
@@ -25,6 +25,7 @@ class BlogController extends BaseController
|
||||
private readonly FileValidatorService $fileValidator,
|
||||
private readonly \App\Blog\Service\BlogWriter $blogWriter,
|
||||
private readonly \App\Blog\Service\BlogCacheInvalidator $cacheInvalidator,
|
||||
private readonly \App\Blog\Service\BlogBodySanitizer $bodySanitizer,
|
||||
private readonly string $projectDir,
|
||||
) {}
|
||||
|
||||
@@ -369,7 +370,9 @@ class BlogController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'title و body باید رشته باشند', 422);
|
||||
}
|
||||
$title = trim((string) ($data['title'] ?? ''));
|
||||
$body = trim((string) ($data['body'] ?? ''));
|
||||
// پاکسازی پیش از سنجشِ خالیبودن: بدنهای که فقط `<script>` است، بعد از
|
||||
// پاکسازی خالی میشود و باید همان ۴۲۲ «الزامی است» را بگیرد، نه ذخیره شود.
|
||||
$body = $this->bodySanitizer->clean(trim((string) ($data['body'] ?? '')));
|
||||
|
||||
if (empty($title) || empty($body)) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'title و body الزامی است', 422);
|
||||
@@ -475,7 +478,7 @@ class BlogController extends BaseController
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
if (array_key_exists('title', $data)) $blog->setTitle($data['title']);
|
||||
if (array_key_exists('body', $data)) $blog->setBody($data['body']);
|
||||
if (array_key_exists('body', $data)) $blog->setBody($this->bodySanitizer->clean((string) $data['body']));
|
||||
if (array_key_exists('summary', $data)) $blog->setSummary($data['summary']);
|
||||
if (array_key_exists('tags', $data)) $blog->setTags((array)$data['tags']);
|
||||
if (array_key_exists('status', $data)) $blog->setStatus($data['status']);
|
||||
|
||||
@@ -35,6 +35,7 @@ class RepresentationBlogController extends BaseController
|
||||
private readonly CityRepository $cityRepo,
|
||||
private readonly BlogWriter $writer,
|
||||
private readonly BlogCacheInvalidator $cacheInvalidator,
|
||||
private readonly \App\Blog\Service\BlogBodySanitizer $bodySanitizer,
|
||||
) {}
|
||||
|
||||
private function currentRepresentation(User $user): Representation
|
||||
@@ -106,7 +107,9 @@ class RepresentationBlogController extends BaseController
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
$title = trim((string) ($data['title'] ?? ''));
|
||||
$body = trim((string) ($data['body'] ?? ''));
|
||||
// پاکسازی پیش از سنجشِ خالیبودن — بدنهای که چیزی جز markup ناامن ندارد
|
||||
// باید ۴۲۲ بگیرد نه اینکه خالی ذخیره شود.
|
||||
$body = $this->bodySanitizer->clean(trim((string) ($data['body'] ?? '')));
|
||||
if ($title === '' || $body === '') {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'title و body الزامی است', 422);
|
||||
}
|
||||
@@ -140,7 +143,7 @@ class RepresentationBlogController extends BaseController
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
if (array_key_exists('title', $data)) $blog->setTitle((string) $data['title']);
|
||||
if (array_key_exists('body', $data)) $blog->setBody((string) $data['body']);
|
||||
if (array_key_exists('body', $data)) $blog->setBody($this->bodySanitizer->clean((string) $data['body']));
|
||||
if (array_key_exists('summary', $data)) $blog->setSummary($data['summary']);
|
||||
if (array_key_exists('tags', $data)) $blog->setTags((array) $data['tags']);
|
||||
if (array_key_exists('image_url', $data)) $blog->setImageUrl($data['image_url'] ?: null);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Blog\Service;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;
|
||||
|
||||
/**
|
||||
* پاکسازی HTML بدنهٔ مقاله در **لحظهٔ ذخیره**.
|
||||
*
|
||||
* بدنهٔ مقاله را ادمین با CKEditor مینویسد و پنل بعداً با
|
||||
* `dangerouslySetInnerHTML` رندرش میکند (BlogReviewPage). تا پیش از این، هرچه
|
||||
* میآمد همان ذخیره و همان رندر میشد.
|
||||
*
|
||||
* چرا هنگام ذخیره و نه هنگام نمایش: بدنه چند مصرفکننده دارد — پنل ادمین، سایت
|
||||
* عمومی `nobat724_front` و فید. اگر پاکسازی در لایهٔ نمایش بود، هر مصرفکنندهٔ
|
||||
* تازه دوباره آسیبپذیر شروع میکرد. یک نقطهٔ ورود، یک تضمین.
|
||||
*
|
||||
* چرا با وجود CSP: هدر فعلی `script-src 'self'` است و `<script>` تزریقی را اجرا
|
||||
* نمیکند، ولی CSP لایهٔ دوم است نه اولین دفاع، و هندلرهای inline و لینک
|
||||
* `javascript:` را کامل نمیبندد.
|
||||
*
|
||||
* سیاست عناصر در `config/packages/html_sanitizer.yaml` است، نه اینجا: همانجا
|
||||
* جای پیکربندی است و تغییرش نباید کد لازم داشته باشد.
|
||||
*/
|
||||
class BlogBodySanitizer
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'html_sanitizer.sanitizer.blog.body')]
|
||||
private readonly HtmlSanitizerInterface $sanitizer,
|
||||
) {}
|
||||
|
||||
public function clean(string $html): string
|
||||
{
|
||||
return $this->sanitizer->sanitize($html);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user