feat: add RichTextEditor component for rich text editing in articles
feat: create SanitizeBlogBodiesCommand to clean existing blog bodies according to current HTML sanitization policies test: add AppointmentTreatmentSessionLinkTest to ensure appointment booking functionality works correctly with treatment session links
This commit is contained in:
@@ -21,24 +21,27 @@ authz/headers/cors/inject) + پروبهای دستی با JWT واقعی هر
|
||||
| 1 | `TreatmentProtocolController` هیچ گِیت مجوزی نداشت — منشیِ `services:false` میتوانست پروتکل درمان را بخواند، بازنویسی و حذف کند | 🟧 High | ✅ رفع شد |
|
||||
| 2 | `react-router` — ۵ advisory از جمله XSS و open redirect | 🟧 High | ✅ رفع شد (مهاجرت به v8) |
|
||||
| 3 | `lodash-es` — code injection در `_.template` + دو prototype pollution | 🟧 High | ✅ رفع شد (override به 4.18.1) |
|
||||
| 4 | ۶۲ moderate در `@ckeditor/ckeditor5-build-classic` (deprecated) | 🟨 Medium | ⚠️ risk پذیرفتهشده — تصمیم ۲۰۲۶-۰۸-۰۷ |
|
||||
| 4 | ۶۲ moderate در `@ckeditor/ckeditor5-build-classic` (deprecated) | 🟨 Medium | ✅ رفع شد — ۲۰۲۶-۰۸-۰۸ (مهاجرت به `ckeditor5@48`) |
|
||||
| 5 | `dangerouslySetInnerHTML` روی بدنهٔ بلاگ در `BlogReviewPage` | 🟨 Medium | ✅ رفع شد (sanitize هنگام ذخیره) |
|
||||
| 6 | `APP_SECRET` واقعی در `.env.test` تحت git | 🟦 Low | ✅ رفع شد |
|
||||
| 7 | پسورد sandbox درگاه ملت هاردکد | ⬜ Info | ✅ رفع شد (به env منتقل شد) |
|
||||
| 8 | ۱۰ روت `GET` که مجوزِ رجیستریشان را enforce نمیکنند | 🟨 Medium | ✅ رفع شد — ۲۰۲۶-۰۸-۰۸ |
|
||||
| 9 | `AppointmentPlanController` هیچ گِیت مجوزی نداشت — دوقلوی یافتهٔ ۱ | 🟧 High | ✅ رفع شد — ۲۰۲۶-۰۸-۰۸ |
|
||||
| 10 | ۳۴ روت نوشتنی که گِیتشان **بعد از** واکشی رکورد است | 🟦 Low | ⚠️ باز — فهرست کامل زیر |
|
||||
| 10 | ۳۴ روت نوشتنی که گِیتشان **بعد از** واکشی رکورد است | 🟦 Low | ◐ نیمهرفع — ۲۲ روت بسته شد، ۱۲ روت باز |
|
||||
| 11 | `MyAppointmentsController::$branches` تزریق نشده بود — اتصال نوبت به جلسهٔ درمان همیشه ۵۰۰ میداد | 🟧 High | ✅ رفع شد — ۲۰۲۶-۰۸-۰۸ |
|
||||
| 12 | سیاست پاکسازی، `style` جدول را میبرد — رگرسیونِ ظاهریِ ناشی از رفعِ یافتهٔ ۵ | 🟦 Low | ✅ رفع شد — ۲۰۲۶-۰۸-۰۸ |
|
||||
|
||||
سیاست اولیه «فقط Critical/High رفع شود» بود؛ کاربر بعداً رفعِ همهٔ یافتههای باز را خواست، پس
|
||||
یافتههای ۲، ۳، ۵، ۶ و ۷ هم بسته شدند. یافتهٔ ۴ طبق تصمیم صریح خارج از محدوده ماند و یافتهٔ ۸
|
||||
حین همین کار کشف شد.
|
||||
یافتههای ۲، ۳، ۵، ۶ و ۷ هم بسته شدند. یافتهٔ ۸ حین همین کار کشف شد.
|
||||
|
||||
یافتههای ۹ و ۱۰ در جلسهٔ ۲۰۲۶-۰۸-۰۸ کشف شدند، حین بستنِ یافتهٔ ۸. شرحشان در بخش
|
||||
یافتههای ۹ تا ۱۲ در جلسههای ۲۰۲۶-۰۸-۰۸ کشف شدند، حین بستنِ یافتههای ۸ و ۴. شرحشان در بخش
|
||||
«پیگیری ۲۰۲۶-۰۸-۰۸» انتهای همین سند است.
|
||||
|
||||
```bash
|
||||
npm audit --omit=dev # قبل: high=2 moderate=62 بعد: high=0 moderate=3
|
||||
ddev exec php bin/phpunit # ۱۵۵۵ تست، ۴۸۳۲ assertion، سبز
|
||||
npm audit --omit=dev # ۲۰۲۶-۰۸-۰۷ قبل: high=2 moderate=62 · بعد: high=0 moderate=3
|
||||
# ۲۰۲۶-۰۸-۰۸ پس از مهاجرت CKEditor: ۰ آسیبپذیری
|
||||
ddev exec php bin/phpunit # ۱۵۶۸ تست سبز (بود ۱۵۵۵)
|
||||
ddev exec php vendor/bin/phpstan # No errors (بود ۱۷ خطا بیرون از baseline)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -681,18 +684,177 @@ fail-closed مستندِ `SecretaryPermissionChecker`. «خالیِ خاموش»
|
||||
برخلاف پیشنهاد ۳ گزارش، پرسنل در DB زنده ساخته **نشد**: دادهٔ دستی با اولین re-seed میرود و
|
||||
هیچوقت خودکار اجرا نمیشود.
|
||||
|
||||
## یافتهٔ ۱۱ 🟧 HIGH — `$branches` تزریقنشده: اتصال نوبت به جلسهٔ درمان همیشه ۵۰۰
|
||||
|
||||
**۱. فایل:** `src/Appointment/Controller/MyAppointmentsController.php:384`
|
||||
|
||||
**۲. شرح:** کنترلر `$this->branches->pair($user)` را صدا میزد ولی `AddressResolver` هرگز در
|
||||
constructor نبود. هر `POST /api/v1/my/appointment` که `treatment_session_uuid` داشت روی
|
||||
«Undefined property» میافتاد. یعنی اتصال نوبت به جلسهٔ درمان از پنل **هیچوقت کار نکرده**.
|
||||
|
||||
**۳. چرا کسی ندید:** این شاخه هیچ تستی نداشت. `phpstan` دقیقاً همین را گزارش میکرد، ولی بین
|
||||
۱۶ خطای بیاثرِ دیگر گم شده بود — «۱۷ خطا» عددی ثابت شده بود که همه ازش رد میشدند. درسش این
|
||||
است که baselineِ نخوانده، باگ زنده را پنهان میکند.
|
||||
|
||||
**۴. رفع:** تزریق `AddressResolver $branches` — همان سرویسی که `AppointmentPlanController` و
|
||||
`TreatmentProtocolController` با همین نام استفاده میکنند.
|
||||
|
||||
**۵. تست:** `tests/Appointment/AppointmentTreatmentSessionLinkTest.php` — سه سناریو: بدون
|
||||
اتصال (۲۰۱)، uuidِ ناموجود (۴۰۴ با `treatment_session_uuid` در فیلد خطا، نه ۵۰۰)، و رشتهٔ
|
||||
خالی (۲۰۱).
|
||||
|
||||
## یافتهٔ ۱۲ 🟦 LOW — رگرسیونِ ظاهریِ ناشی از رفعِ یافتهٔ ۵
|
||||
|
||||
**۱. شرح:** سیاست `html_sanitizer.yaml` برای `<table>` هیچ attributeی مجاز نکرده بود. از آنجا
|
||||
که پاکسازی در **لحظهٔ ذخیره** است، هر مقالهای که از پنل ویرایش میشد حاشیه و فاصلهٔ جدولش را
|
||||
از دست میداد. رگرسیون از ۲۰۲۶-۰۸-۰۷ فعال بود و کسی ندیده بود، چون هنوز کسی مقالهٔ جدولدار را
|
||||
ویرایش نکرده بود.
|
||||
|
||||
**۲. رفع:** `border`, `cellpadding`, `cellspacing` مجاز شدند — هر سه عددی و غیرقابلاجرا.
|
||||
`style` عمداً ممنوع ماند و ظاهرِ از دست رفته (`border-collapse`, `width`) در `.blog-body`
|
||||
داخل `assets/admin/styles.css` بازسازی شد؛ یعنی presentation به لایهٔ درستش رفت.
|
||||
|
||||
**۳. تست:** `BlogBodySanitizerTest::testTableKeepsInertLayoutAttributesButLosesStyle`.
|
||||
|
||||
**۴. کارِ باقیمانده در repo دیگر:** `nobat724_front` همان بدنه را رندر میکند و قاعدهٔ CSS
|
||||
معادل را ندارد. جدولِ مقالهها آنجا بدون `border-collapse` نمایش داده میشود.
|
||||
|
||||
## یافتهٔ ۴ — بسته شد: مهاجرت CKEditor
|
||||
|
||||
`@ckeditor/ckeditor5-build-classic@44.3.0` (deprecated، ۶۲ advisory) با پکیج umbrella
|
||||
`ckeditor5@48.4.0` جایگزین شد. `@ckeditor/ckeditor5-react@11.2.0` از قبل نصب بود و
|
||||
peer dependency اش `ckeditor5 >= 46` است، پس bump دیگری لازم نشد.
|
||||
|
||||
مهاجرت کمریسک بود چون سطح مصرف کوچک است: دو صفحه، یک `ClassicEditor`، ده دکمهٔ نوار ابزار.
|
||||
پیکربندی در کامپوننت مشترک `assets/admin/components/RichTextEditor.tsx` متمرکز شد تا مهاجرت
|
||||
بعدی یک فایل باشد و نوار ابزارِ دو صفحه از هم واگرا نشود.
|
||||
|
||||
در پکیج umbrella، برخلاف buildِ آماده، فهرست پلاگینها باید صریح باشد. عمداً دقیقاً همان
|
||||
پلاگینهای دکمههای قبلی آورده شد و نه بیشتر: هر پلاگین اضافه یعنی markup تازهای که
|
||||
`html_sanitizer.yaml` مجازش نکرده و هنگام ذخیره حذف میشود.
|
||||
|
||||
```
|
||||
npm audit --omit=dev # قبل: 61 (moderate=3, low=58) → بعد: 0
|
||||
npx tsc --noEmit # بدون خطا
|
||||
ddev exec yarn dev # webpack compiled successfully — 54 فایل
|
||||
npx vitest --run # ۸۰۱ تست فرانتاند سبز
|
||||
```
|
||||
|
||||
## یافتهٔ ۱۰ — نیمهرفع: ۲۲ روت از ۳۴ بسته شد
|
||||
|
||||
چکِ اصلیِ این روتها شیءمحور است و بالا نمیرود — `ClinicController::update` به `$clinic`ِ همان
|
||||
رکورد نیاز دارد. ولی **سهمِ منشی** از آن چک همیشه همان توگلِ رجیستری است، پس یک پیشچکِ
|
||||
فقط-منشی اکیداً ضعیفتر است: هر کسی را که رد کند، چکِ پایینتر هم رد میکرد. یعنی هیچ مسیرِ
|
||||
مجازی بسته نمیشود و فقط ۴۰۴ به ۴۰۳ تبدیل میشود.
|
||||
|
||||
`ClinicDoctorAccessChecker` عمداً در پیشچک نیست: پزشکِ عضو ممکن است روی رکوردِ کلینیکِ دیگری
|
||||
که مالکش است اقدام کند، و آنجا محیطِ فعال با محیطِ رکورد یکی نیست. آن حالت را فقط چکِ
|
||||
شیءمحورِ پایین میتواند درست بسنجد.
|
||||
|
||||
بسته شد (۲۲): `AppointmentController` × ۴ · `AppointmentSettingsController` × ۹ ·
|
||||
`ClinicController::update`, `detachDoctor` · `ClinicDoctorPermissionController` ×۱ ·
|
||||
`ClinicInvitationController` × ۴ · `ResourceBlockController` × ۲.
|
||||
|
||||
باز ماند (۱۲) — هیچکدام منبعی در `PermissionCatalog` ندارند و چکشان مالکیتِ خودِ رکورد است:
|
||||
|
||||
- `ClinicController` — `createAddress`, `updateAddress`, `deleteAddress`. `addresses` فقط
|
||||
`view` دارد؛ نوشتنِ آدرس صریحاً owner-or-admin است و اصلاً قابل واگذاری نیست.
|
||||
- `DoctorController` — `update`, `delete`, `updateAddress`, `deleteAddress`.
|
||||
- `RepresentationController::update`.
|
||||
- `SecretaryController` — `create`, `update`, `deactivate`, `syncClinicDoctors`.
|
||||
|
||||
پوششِ نشتِ باقیمانده کمارزش است: uuidِ پزشک از فهرست عمومی پزشکان در دسترس است. برای منشی و
|
||||
نماینده ارزشش بیشتر است ولی همچنان Low. بستنشان یا کلیدِ تازه در رجیستری میخواهد — که UI
|
||||
مجوزها و دو Entity و تایپهای فرانت را درگیر میکند — یا یک مکانیزم موازیِ نقشمحور، که یافتهٔ ۱
|
||||
توصیه کرد نسازیم.
|
||||
|
||||
`ApiLeastPrivilegeTest::testHoistedGatesAnswer403BeforeTheLookup` این ۲۲ تا را قفل میکند: اگر
|
||||
کسی خطِ پیشچک را بردارد، پاسخ به ۴۰۴ برمیگردد و تست قرمز میشود.
|
||||
|
||||
## پاکسازی `phpstan` — و آنچه زیرش پنهان بود
|
||||
|
||||
هر ۱۷ خطای بیرون از baseline بسته شد و `phpstan` حالا `No errors` میدهد. baseline از ۴۴ به
|
||||
۴۳ ردیف رسید. جنس خطاها:
|
||||
|
||||
- **۱ باگ زنده** — یافتهٔ ۱۱ بالا.
|
||||
- **۳ ناهمخوانی نوع** — `BillingController` فیلترهای `from`/`to` را string میفرستاد و
|
||||
`InvoiceService` عددِ صحیح میخواست. تبدیل در مرزِ ورودی نشست، نه در repository.
|
||||
- **۶ property تزریقشده و بلااستفاده** در پنج سرویس — حذف شدند.
|
||||
- **۲ فراخوانیِ `getEntityManager()` از بیرون** در `SecretaryService` — با `EntityManagerInterface`
|
||||
تزریقشده جایگزین شد.
|
||||
- **۲ ignore pattern کهنه** که دیگر با هیچ خطایی مطابقت نداشتند.
|
||||
- **۳ مقایسهٔ همیشه-درست** — ساده شدند.
|
||||
|
||||
دو مورد عمداً با `@phpstan-ignore-next-line` ماندند: `ServiceItem::getConsumables()` و
|
||||
`getStaffMembers()`. `phpstan` فقط constructor را میبیند و میگوید property همیشه مقدار دارد؛
|
||||
Doctrine اما بدون constructor هیدریت میکند. گاردِ `??=` عمدی است.
|
||||
|
||||
## پاکسازی مقالههای قدیمی
|
||||
|
||||
`app:blog:sanitize-bodies` ساخته شد و روی هر ۴۲۷ مقاله اجرا شد. دستور است نه migration، چون
|
||||
سیاست ممکن است دوباره سفت شود و آنوقت باید همین گذر تکرار شود.
|
||||
|
||||
عددِ خام گمراهکننده بود: «۴۲۶ مقاله تغییر میکند» در نگاه اول یعنی ۴۲۶ مقالهٔ آلوده. تفکیکِ
|
||||
جنسِ تغییر نشان داد:
|
||||
|
||||
| جنس تغییر | تعداد | یعنی چه |
|
||||
|---|---|---|
|
||||
| سختسازی | ۳۹۸ | افزودن `rel="noopener noreferrer"` یا decode شدن ` ` |
|
||||
| حذف attribute | ۲۸ | فقط `style` روی ۲۲ جدول و ۱ `div`؛ بقیه ترمیمِ HTML شکسته |
|
||||
| بدون تغییر | ۱ | — |
|
||||
|
||||
**هیچ مقالهای markup اجرایی نداشت** — نه `<script>`، نه `onerror`، نه `javascript:`. یعنی
|
||||
یافتهٔ ۵ یک ریسک بالقوه را بست، نه یک نشتِ فعال را.
|
||||
|
||||
پس از اجرا، ۴۲۵ مقاله `rel="noopener noreferrer"` دارند و هیچکدام `style=` ندارند. اجرای دوم
|
||||
صفر تغییر گزارش میدهد.
|
||||
|
||||
## ناپایداریِ سوییت — ریشهاش پیدا و بسته شد
|
||||
|
||||
گزارش ۲۰۲۶-۰۸-۰۷ نوشته بود یک تست، ۲۰۰ تست بعدتر تستی بیربط را میشکند، و آن را با
|
||||
`resetManager()` در `tearDown` همان تست مهار کرده بود. مهار بود نه رفع: با اضافهشدنِ
|
||||
تستهای این جلسه، خطا سه بار روی سه تستِ متفاوت ظاهر شد — `ClinicRecordAccessTest`،
|
||||
`ServiceRescheduleTest`، `CommentListNPlusOneTest` — و هر سه در اجرای تکی سبز بودند.
|
||||
|
||||
ریشه، بستهشدنِ manager نبود. تستی که موجودیتی را `persist()` میکند و بی`flush()` تمام
|
||||
میشود، همان unit of work را برای تست بعدی به ارث میگذارد؛ آنجا اولین `flush()` با
|
||||
«A new entity was found through the relationship …» میشکند. `setUp` فقط وقتی ریست میکرد
|
||||
که manager **بسته** باشد، و این حالت manager را باز ولی آلوده میگذارد.
|
||||
|
||||
رفع: یک `$this->em->clear()` در `ApiTestCase::setUp`. عمداً `clear()` نه `resetManager()` —
|
||||
همان نمونه میماند، پس هیچ ارجاعی به managerِ مرده نمیرسد و فقط identity map خالی میشود.
|
||||
دو اجرای کاملِ پیاپی سبز شد.
|
||||
|
||||
## تغییرات این جلسه
|
||||
|
||||
```
|
||||
src/Shared/Controller/PermissionGateTrait.php گِیت مشترک (جدید)
|
||||
src/Shared/Controller/PermissionGateTrait.php گِیت مشترک + پیشچکِ منشی (جدید)
|
||||
src/Resource/Controller/ResourcePermissionTrait.php روی گِیت مشترک سوار شد
|
||||
src/Billing/Controller/BillingController.php ۱۲ روت → payments.view/create/update
|
||||
src/Appointment/Controller/MyAppointmentsController.php ۵ روت → appointments.view/create
|
||||
src/Billing/Controller/BillingController.php ۱۲ روت → payments.* + تبدیل نوعِ فیلترها
|
||||
src/Appointment/Controller/MyAppointmentsController.php ۵ روت → appointments.* + تزریق AddressResolver
|
||||
src/Appointment/Plan/Controller/AppointmentPlanController.php ۳ روت → services.* (یافتهٔ ۹)
|
||||
tests/Shared/ApiLeastPrivilegeTest.php پویش نوشتنی + ALLOWED_WRITE؛ KNOWN_GAPS خالی شد
|
||||
src/Appointment/Controller/AppointmentController.php ۴ پیشچک (یافتهٔ ۱۰)
|
||||
src/Appointment/Controller/AppointmentSettingsController.php ۹ پیشچک
|
||||
src/Clinic/Controller/ClinicController.php ۲ پیشچک
|
||||
src/Clinic/Controller/ClinicDoctorPermissionController.php ۱ پیشچک
|
||||
src/ClinicInvitation/Controller/ClinicInvitationController.php ۴ پیشچک
|
||||
src/Appointment/Availability/Controller/ResourceBlockController.php ۲ پیشچک
|
||||
src/Blog/Command/SanitizeBlogBodiesCommand.php پاکسازی مقالههای قدیمی (جدید)
|
||||
config/packages/html_sanitizer.yaml سه attributeِ ظاهریِ جدول مجاز شد
|
||||
assets/admin/components/RichTextEditor.tsx ادیتور مشترک روی ckeditor5@48 (جدید)
|
||||
assets/admin/pages/BlogFormPage.tsx استفاده از ادیتور مشترک
|
||||
assets/admin/pages/RepresentationBlogFormPage.tsx استفاده از ادیتور مشترک
|
||||
assets/admin/styles.css .blog-body — ظاهر جدولِ مقاله
|
||||
package.json ckeditor5@48؛ build-classic حذف شد
|
||||
phpstan-baseline.neon دو ردیفِ کهنه رفت، ۴۴ → ۴۳
|
||||
پنج سرویس (Patient/Inventory/Secretary/ClinicService/…) propertyهای بلااستفاده حذف شد
|
||||
tests/Shared/ApiLeastPrivilegeTest.php پویش نوشتنی + ALLOWED_WRITE + قفلِ GATE_BEFORE_LOOKUP
|
||||
tests/Staff/StaffCrossTenantTest.php IDOR بینمحیطیِ پرسنل (جدید)
|
||||
tests/Appointment/AppointmentTreatmentSessionLinkTest.php رگرسیونِ یافتهٔ ۱۱ (جدید)
|
||||
tests/Blog/BlogBodySanitizerTest.php attributeهای جدول
|
||||
tests/Secretary/SecretaryAppointmentScopeTest.php منشیِ بیرابطه: خالی → ۴۰۳
|
||||
docs/api/billing.md · appointment.md · appointment-plan.md مجوز هر روت
|
||||
tests/ApiTestCase.php clear() در setUp — رفعِ ناپایداریِ سوییت
|
||||
docs/api/billing.md · appointment.md · appointment-plan.md · blog.md · insurance.md · doctor-service.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user