- 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.
26 KiB
گزارش آدیت امنیتی ClinicPro — ۲۰۲۶-۰۸-۰۷
نوع: آدیت دلتایی روی سطح حملهٔ ساختهشده بعد از AUDIT-2026-07-19.md — ۳۵۴ کامیت،
عمدتاً رجیستری مجوزها، دامنهٔ جدید Treatment، نقش/پنل staff و جداسازی محیط.
روش: درایور symfony-security-audit (white-box: deps/sinks/guards/secrets/config — black-box:
authz/headers/cors/inject) + پروبهای دستی با JWT واقعی هر نقش روی اپ در حال اجرا + بازرسی کد.
تارگت: clinicpro/ روی ddev — Symfony 7.4، PHP 8.3.31، Doctrine ORM 3.6، LexikJWT، MariaDB
11.8، React 19 admin.
قانون گزارش: هیچ یافتهای بدون بازتولیدِ اجراشده ثبت نشده. خروجی درایور lead است نه یافته؛ leadهایی که پس از خواندن کد «مثبت کاذب» بودند، در بخش «رد شد» با دلیل آمدهاند.
خلاصهٔ وضعیت
| # | یافته | شدت | وضعیت |
|---|---|---|---|
| 1 | TreatmentProtocolController هیچ گِیت مجوزی نداشت — منشیِ services:false میتوانست پروتکل درمان را بخواند، بازنویسی و حذف کند |
🟧 High | ✅ رفع شد |
| 2 | react-router — ۵ advisory از جمله XSS و open redirect |
🟧 High | ⚠️ باز — نیازمند تصمیم |
| 3 | lodash-es — code injection در _.template + دو prototype pollution |
🟧 High | ⚠️ باز (از آدیت قبلی) |
| 4 | ۶۲ moderate در @ckeditor/ckeditor5-build-classic (deprecated) |
🟨 Medium | ⚠️ risk پذیرفتهشده — تصمیم ۲۰۲۶-۰۸-۰۷ |
| 5 | dangerouslySetInnerHTML روی بدنهٔ بلاگ در BlogReviewPage |
🟨 Medium | ⚠️ باز |
| 6 | APP_SECRET واقعی در .env.test تحت git |
🟦 Low | ⚠️ باز |
| 7 | پسورد sandbox درگاه ملت هاردکد | ⬜ Info | باز (از آدیت قبلی، کماهمیت) |
طبق تصمیم کاربر پیش از اجرا: Critical و High همان جلسه رفع میشوند؛ Medium و پایینتر فقط گزارش میشوند. یافتهٔ ۱ رفع شد. یافتههای ۲ و ۳ High هستند ولی رفعشان ارتقای وابستگی است نه تغییر کد این repo، و شکستنِ روتینگ پنل یا ادیتور را در پی دارد — پس تصمیم به کاربر واگذار شد.
ماتریس نقشهایی که آدیت با آن اجرا شد
آدیت ۲۰۲۶-۰۷-۱۹ در بخش «محدودیت پوشش» نوشته بود ماتریس authz ناقص مانده چون فقط admin و
doctor در DB بودند. آن محدودیت اینجا برطرف شد. DB از آن زمان دوباره seed شده، پس جدول قبلی
بیاعتبار بود و از نو ساخته شد:
admin 09120671756 QaTest@1234 ROLE_USER,ROLE_ADMIN
clinic 09390039833 QaTest@1234 ROLE_USER,ROLE_CLINIC (مالک کلینیک ۳)
secretary 0912000109 QaTest@1234 ROLE_USER,ROLE_SECRETARY
doctor 0912000101 QaTest@1234 ROLE_USER,ROLE_DOCTOR
representation 09124000001 QaTest@1234 ROLE_USER,ROLE_REPRESENTATION
staff 09128726723 09128726723 ROLE_USER,ROLE_STAFF (پرسنل کلینیک ۳)
multirole 0912000201 QaTest@1234 ROLE_USER,ROLE_DOCTOR,ROLE_CLINIC (مالک کلینیک ۱)
اکانتهای 09127000000 و 09123456778 که در گزارش قبلی بودند دیگر در DB وجود ندارند، و
09390039833 که «doctor» ثبت شده بود حالا ROLE_CLINIC است. ماتریس در هر دو درایور
(symfony-security-audit و qa-clinicpro) اصلاح و همگام شد. هیچ کاربری ساخته نشد و هیچ پسوردی
عوض نشد — همه از قبل موجود بودند.
جفت tenant برای تست IDOR: مهاجم = multirole (مالک کلینیک ۱)، قربانی = کلینیک ۳ که تنها
tenant دارای دادهٔ Treatment است (۴ پرونده، ۳ جلسه، ۱ پروتکل).
یافتهها
1. 🟧 HIGH — پروتکل درمان بدون هیچ گِیت مجوزی
۱. فایل: src/Treatment/Controller/TreatmentProtocolController.php (خطوط ۳۷، ۵۷، ۷۲ نسخهٔ قبل)
۲. ریسک: هر کاربرِ احرازشده داخل یک tenant — از جمله منشیای که توگل «سرویسها» برایش کاملاً بسته است — میتوانست پروتکل درمانِ هر سرویس همان tenant را بخواند، کل آن را بازنویسی کند، یا حذفش کند. حذف پروتکل یعنی سرویس چندجلسهای به تکجلسهای برمیگردد: برنامهٔ درمان بیمار، فاصلهٔ جلسات و فهرست پرسنل مجاز از بین میرود.
۳. شرح: TreatmentCaseController گیت درست دارد و هر متدش
denyUnlessGranted($user, 'treatment', $action) صدا میزند. TreatmentProtocolController — که در
همان دامنه و همان کامیتها ساخته شده — این کار را نمیکرد. تنها دفاعش requireItem() بود که
فقط مالکیتِ tenant را میسنجد:
private function requireItem(User $user, string $uuid): ServiceItem
{
$item = $this->items->findByUuid($uuid);
[$entityType, $entityId] = $this->branches->pair($user);
if ($item === null
|| $item->getSection()->getEntityType() !== $entityType
|| $item->getSection()->getEntityId() !== $entityId
) {
throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'سرویس یافت نشد', 404);
}
return $item;
}
مالکیت ≠ مجوز. عبور از این تابع فقط ثابت میکند سرویس مالِ همین محیط است، نه اینکه این کاربر اجازهٔ دستزدن به آن را دارد.
۴. کشف چطور شد: ناهمگونی بین دو کنترلرِ یک دامنه در بازرسی کد دیده شد، بعد با پروب واقعی
تأیید شد. درایور آن را نداده بود — guards فقط route بدون #[IsGranted] را میبیند و این کنترلر
#[IsGranted('IS_AUTHENTICATED_FULLY')] سطح-کلاس داشت.
۵. بازتولید (قبل از رفع): منشیِ کلینیک ۱ (0912000209) که در DB
services: {view:false, create:false, update:false, delete:false} دارد، روی سرویسِ همان کلینیک:
sec services:false | GET /api/v1/service-item/<uuid>/treatment-protocol | 200 |
sec services:false | PUT /api/v1/service-item/<uuid>/treatment-protocol | 422 | ERR_VALIDATION_001
sec services:false | DELETE /api/v1/service-item/<uuid>/treatment-protocol | 200 |
sec services:false | GET /api/v1/service-items | 403 | ERR_FORBIDDEN_001
خط آخر شاهدِ ماجراست: همان مجوز روی فهرست سرویسها کار میکند و ۴۰۳ میدهد. یعنی مشکل پیکربندی مجوزِ این منشی نیست، نبودِ گیت در این کنترلر است.
422 روی PUT مهمتر از 200 است: کد خطا ERR_VALIDATION_001 از داخل TreatmentProtocolWriter
میآید، یعنی درخواست از لایهٔ authorization عبور کرده و فقط سر اعتبارسنجیِ فیلد افتاده. با یک
بدنهٔ معتبر، نوشتن انجام میشد.
۶. رفع اعمالشده: همان الگوی ServiceCatalogController و ClinicServiceController —
private function denyUnlessGranted(User $user, string $action): void
{
$this->secretaryAccess->denyUnlessGranted($user, 'services', $action);
$this->clinicDoctorAccess->denyUnlessGranted($user, 'services', $action);
}
و صدا زدنش در ابتدای هر سه متد: show → view، replace → update، delete → update.
۷. علت انتخاب راهحل:
- منبع
servicesنهtreatment: پروتکل یک خاصیتِServiceItemاست. صفحهاش هم داخل کاتالوگ سرویسهاست، نه در پروندهٔ درمان. اگرtreatmentمیگرفت، منشیای که فقط اجازهٔ دیدن دورهٔ درمان دارد میتوانست تعریفِ سرویس را عوض کند. deleteباupdateنهdelete: سرویس حذف نمیشود؛ فقط سوییچِ «طول درمان» روی همان سرویس خاموش میشود. اگرservices.deleteمیگرفت، مدیری که اجازهٔ ویرایش سرویس دارد ولی اجازهٔ حذفش را ندارد نمیتوانست سوییچی را که خودش روشن کرده خاموش کند.- گیت پیش از
requireItem: ترتیب عمدی است. اگر بعد از آن بود، uuid ناموجود ۴۰۴ میداد و همین تفاوت ۴۰۳/۴۰۴ به کاربرِ بدون مجوز میگفت کدام uuidها در این tenant وجود دارند. - مکانیزم موازی نساختیم: همان دو checker موجود تزریق شدند، نه یک voter تازه.
۸. تأیید رفع — هر سه سناریوی معیار پذیرش:
❌ sec services:false | GET | 403 | ERR_FORBIDDEN_001
❌ sec services:false | PUT | 403 | ERR_FORBIDDEN_001
❌ sec services:false | DELETE | 403 | ERR_FORBIDDEN_001
✅ owner (clinic A) | GET | 200 |
✅ owner (clinic B) | GET | 200 |
⚠️ A→B cross-tenant | GET | 404 | ERR_NOT_FOUND_001
مسیر مجاز نشکست و تفکیک tenant همچنان ۴۰۴ میدهد نه ۴۰۳.
۹. تست رگرسیون: چهار تست در tests/Secretary/SecretaryResourceEnforcementTest.php —
testTreatmentProtocolReadDeniedByDefault، testTreatmentProtocolReadAllowedWhenServicesGranted،
testTreatmentProtocolWriteNeedsServicesUpdate،
testTreatmentProtocolWriteAllowedWhenServicesUpdateGranted. کل فایل: ۳۳ تست، ۴۸ assertion، سبز.
۱۰. مستندات: docs/api/treatment.md — هر سه اندپوینت با مجوز جدید و دلیل انتخابش.
2. 🟧 HIGH — react-router با پنج advisory باز
۱. فایل: package.json → react-router (range آسیبپذیر: 6.0.0 - 8.2.0)
۲. ریسک: XSS، open redirect و DoS در لایهٔ روتینگِ پنل ادمین.
۳. شرح: این یافته در آدیت ۲۰۲۶-۰۷-۱۹ نبود — آنجا فقط یک High (lodash) گزارش شده بود. پنج advisory:
GHSA-wrjc-x8rr-h8h6— open redirect با بکاسلش در<Link>وuseNavigate(دور زدن CVE-2025-68470)GHSA-h8fp-f39c-q6mh—RSCErrorHandlerبدون اعتبارسنجی protocol (XSS)GHSA-337j-9hxr-rhxg— تزریق constructor دلخواه درdeserializeErrors()GHSA-chx6-hx7r-mcp5— DoS احرازنشده با route matching ناکارآمدGHSA-qwww-vcr4-c8h2— دور زدن CSRF در حالت RSC
۴. بازتولید:
npm audit --omit=dev --json | python3 -c "import json,sys; print(json.load(sys.stdin)['metadata']['vulnerabilities'])"
# → {'info': 0, 'low': 0, 'moderate': 62, 'high': 2, 'critical': 0, 'total': 64}
۵. وضعیت: باز. پروژه روی React Router v7 است و رفع یعنی bump به نسخهای خارج از range. سه advisory (RSC، SSR hydration، CSRF مود RSC) به این پنل که کلاینتساید محض است مربوط نمیشوند؛ ولی open redirect و DoS مربوطاند. رفع نشد چون ارتقای major روتینگِ ۵۰ صفحهٔ پنل تست دستی میخواهد و در یک پاس امنیتی ریسکش بیشتر از خودِ یافته است. تسک جدا پیشنهاد میشود.
3. 🟧 HIGH — lodash-es هنوز آسیبپذیر
۱. فایل: package.json → lodash-es (<=4.17.23)
۲. شرح: در آدیت ۲۰۲۶-۰۷-۱۹ «نیمهرفع» ثبت شده بود. هنوز باز است، و حالا دو advisory دیگر هم
اضافه شده: prototype pollution در _.unset و _.omit.
۳. بازتولید: همان npm audit بالا — high=2 که یکی react-router است و یکی lodash-es.
۴. وضعیت: باز. transitive است؛ باید ردیابی شود کدام پکیج آن را میکشد.
4. 🟨 MEDIUM — CKEditor (risk پذیرفتهشده)
@ckeditor/ckeditor5-build-classic@44.3.0 آخرین نسخهٔ آن پکیج است و deprecated؛ ۶۲ moderate
دارد (۶۱ در گزارش قبلی، حالا ۶۲). رفع واقعی یعنی مهاجرت به پکیج umbrella ckeditor5 v45+.
تصمیم ۲۰۲۶-۰۸-۰۷: عمداً خارج از محدودهٔ این آدیت. دلیل: refactor ادیتور بلاگ ریسک شکستن دارد و ربطی به کد سه هفتهٔ اخیر ندارد. این XSSها moderate و admin-only هستند. تسک جدا لازم است.
5. 🟨 MEDIUM — dangerouslySetInnerHTML روی بدنهٔ بلاگ
۱. فایل: assets/admin/pages/BlogReviewPage.tsx:205
dangerouslySetInnerHTML={{ __html: full.body }}
۲. ریسک: HTML ذخیرهشدهٔ پست بلاگ بدون sanitize رندر میشود. زنجیرهٔ واقعیِ سوءاستفاده همان CKEditor بالاست: نویسندهای که HTML مخرب paste کند، آن را روی صفحهٔ بازبینی به ادمین میرساند.
۳. بازتولید: grep -rn "dangerouslySetInnerHTML" assets/admin/ → تنها یک hit، همین خط.
۴. وضعیت: باز طبق سیاست (Medium بدون تأیید رفع نمیشود). CSP فعلی script-src 'self' است، پس
<script> تزریقی اجرا نمیشود؛ ولی هندلرهای inline و javascript: را CSP فعلی کامل نمیبندد.
پیشنهاد: sanitize سمت سرور هنگام ذخیره، نه فقط هنگام نمایش.
6. 🟦 LOW — APP_SECRET واقعی در .env.test
git ls-files | grep -E '^\.env' | xargs grep -nE 'APP_SECRET=.+'
# → .env.test:3:APP_SECRET='$ecretf0rt3st'
# بقیهٔ فایلها فقط placeholder دارند: CHANGE_ME…, APP_SECRET=…, APP_SECRET=...
.env.dev که یافتهٔ ۳ گزارش قبلی بود، تمیز است — آن رفع پابرجاست. .env.test فقط محیط تست را
امضا میکند و ارزش عملیاتی ندارد، ولی مقدارِ واقعی در git بهتر است placeholder شود.
7. ⬜ INFO — پسورد sandbox درگاه ملت
src/Payment/Gateway/MellatGateway.php:23 — private const SANDBOX_PASSWORD = '17384843'. همان
یافتهٔ ۵ گزارش قبلی، همچنان باز، همچنان کماهمیت (اعتبارنامهٔ عمومیِ محیط تست درگاه).
آنچه سالم بود (تست شد، یافته نیست)
۱. جداسازی tenant در دامنهٔ Treatment — کاملاً سالم. مهاجم multirole (کلینیک ۱) روی
uuidهای واقعی کلینیک ۳:
A→B item | GET /api/v1/service-item/<uuid>/treatment-protocol | 404 | ERR_NOT_FOUND_001
A→B item | PUT /api/v1/service-item/<uuid>/treatment-protocol | 404 | ERR_NOT_FOUND_001
A→B item | DELETE /api/v1/service-item/<uuid>/treatment-protocol | 404 | ERR_NOT_FOUND_001
A→B case | GET /api/v1/treatment-case/<uuid> | 404 | ERR_NOT_FOUND_001
A→B case | PATCH /api/v1/treatment-case/<uuid> | 404 | ERR_NOT_FOUND_001
A→B plan | GET /api/v1/treatment-case/<uuid>/plan | 404 | ERR_NOT_FOUND_001
A→B session | GET /api/v1/treatment-session/<uuid> | 404 | ERR_NOT_FOUND_001
A→B slots | GET /api/v1/treatment-session/<uuid>/slot-suggestions| 404 | ERR_NOT_FOUND_001
A→own item | GET /api/v1/service-item/<uuid>/treatment-protocol | 200 | (شاهد مثبت)
هیچ نشتی. پیام خطا هم یکدست است — تفاوت «یافت نشد» و «دسترسی ندارید» بهعنوان enumeration oracle
قابل استفاده نیست، چون هر دو حالت 404 / ERR_NOT_FOUND_001 میدهند.
۲. StaffRouteGuardSubscriber در برابر مسیر نرمالنشده — سالم. با توکن کاربرِ «فقط پرسنل»:
allowlist /dashboard/staff/treatment-sessions | 200 |
plain /api/v1/patients | 403 | ERR_FORBIDDEN_001
double// /api//v1/patients | 403 | ERR_FORBIDDEN_001
dotdot /api/v1/dashboard/staff/../../patients| 403 | ERR_FORBIDDEN_001
encoded /api/v1/%2e%2e/v1/patients | 403 | ERR_FORBIDDEN_001
uppercase /API/v1/patients | 404 | ERR_NOT_FOUND_001
semicolon /api/v1/dashboard/staff;/../patients | 404 | ERR_NOT_FOUND_001
trailing /api/v1/patients/ | 403 | ERR_FORBIDDEN_001
getPathInfo() سیمفونی پیش از رسیدن به گارد نرمالسازی میکند، پس ترفندهای traversal کار نمیکنند.
۳. sweep کامل روتها با توکن پرسنل — سالم. هر ۱۲۸ روتِ GET بدون path parameter (از ۴۹۳ روت
/api/، که ۲۲۲ تایشان GET هستند) با توکن پرسنل زده شد. ۹ روت بیرون از allowlist پاسخ ۲۰۰ دادند:
/api/v1/blogs · /api/v1/blogs/tags · /api/v1/clinics · /api/v1/doctors
/api/v1/altcha/challenge · /api/v1/altcha/config
/api/v1/specialties · /api/v1/specialties/doctor-counts · /api/v1/tags
هر ۹ تا با درخواست بدون توکن هم ۲۰۰ میدهند — یعنی عمداً عمومیاند و نشتِ نقش پرسنل نیستند.
دو تای آنها (altcha/*) همان leadهای درایور در بخش «routes without an explicit guard» بودند.
۴. پیشفرضِ مجوزها fail-closed است. SecretaryPermissionChecker::can() با
?? false تمام میشود. منبعی که بعد از ساختِ ردیف به رجیستری اضافه شده،
PermissionCatalog::merge() پیشفرضِ نقش را برایش میگذارد — پس ردیفهای قدیمیِ بدون کلید
treatment عمداً treatment.view = true میگیرند، نه اینکه گیت دور بخورد. رفتار مستند و عمدی است.
۵. یافتههای آدیت قبلی — همه پابرجا:
| یافتهٔ ۲۰۲۶-۰۷-۱۹ | وضعیت امروز | شاهد |
|---|---|---|
نبود CSP روی /admin |
✅ پابرجا | هدر کامل زیر |
APP_SECRET در .env.dev |
✅ پابرجا | .env.dev صفر assignment |
| فلگهای session cookie | ✅ پابرجا | cookie_secure: true, samesite: lax, httponly: true |
| وابستگی npm | ⚠️ بدتر شد | high 1→2، moderate 61→62 |
content-security-policy: default-src 'self'; script-src 'self'; worker-src 'self' blob:;
style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.tile.openstreetmap.org
https://unpkg.com; font-src 'self' data:; connect-src 'self' https://nominatim.openstreetmap.org
https://*.tile.openstreetmap.org; frame-ancestors 'none'; base-uri 'self'; object-src 'none'
permissions-policy: geolocation=(), microphone=(), camera=()
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
x-frame-options: DENY
۶. پیکربندی چارچوب — سالم. composer audit صفر advisory. token_ttl: 900 با
clock_skew: 5. کلیدهای JWT خارج از git. CORS با regex از ALLOWED_FRONTEND_HOSTS و
allow_credentials: false. rate limiter روی send_code (۵ در ساعت) و login (۱۰ در دقیقه).
leadهای درایور که پس از بازرسی رد شدند
درایور ۴۴ lead داد: 🟧 High ۲۹، 🟨 Medium ۴، 🟦 Low ۱۱، Critical صفر. آنچه رد شد:
| lead | چرا رد شد |
|---|---|
ClinicResourceRepository.php:188,213 — «QueryBuilder predicate built by interpolation» |
$field از match (true) روی نوع PHP میآید (Doctor → 'doctor'، ClinicStaff → 'staff')، نه از ورودی کاربر. دو مقدار ممکن، هر دو ثابت. |
PatientRecordRepository.php:173-179 — همان الگو |
زیرکوئریها ثابتاند و پارامترها با setParameter بایند میشوند. |
PurgeDoctorsCommand.php:80 — «Raw SQL with interpolated variable» |
$t از self::TABLES میآید، ثابتِ کلاس. دستور کنسول است و از HTTP قابل فراخوانی نیست. |
PurgeUnclaimedDoctorsCommand.php:120 |
$where داخل خودِ کلاس ساخته میشود و $params جدا بایند است. کنسول. |
| ۱۵ lead «Filesystem mutation / File access from a variable» در آپلود عکس | مسیرها از FileUploadService میآیند که پیش از move با FileValidatorService (magic-bytes) اعتبارسنجی میکند. الگوی rename($tmpPath, …) عمدی است. |
AccountSettingsPage.test.tsx:40 — «Hardcoded credential» |
فایل تست فرانتاند، مقدار ساختگی. |
seed_testdata.php:26 و SeedScenariosCommand.php:66 — QaTest@1234 |
پسورد دادههای تست، عمدی و مستند. |
۶ lead «Non-cryptographic RNG» در SeedDemoDataCommand |
تولید دادهٔ نمایشی. جای random_int نیست. |
DoctorImportService.php:88 — «Weak hash» |
md5 برای ساختِ شناسهٔ یکتای ≤۱۸ کاراکتری، نه برای امنیت. |
.env.*.example — «secret-shaped values» |
فقط placeholder (CHANGE_ME…, …, ...). |
templates/payment/result.html.twig:140 — |raw |
خروجی json_encode است، نه HTML خام. |
altcha/challenge و altcha/config بدون #[IsGranted] |
عمداً عمومی — کپچا پیش از لاگین لازم است. با درخواست anon تأیید شد. |
محدودیت پوشش (صادقانه)
- پرسنلِ tenant دوم تست نشد. فقط یک
ClinicStaffدر DB هست و مالِ کلینیک ۳ است. سناریوی «پرسنل کلینیک A روی جلسهٔ کلینیک B» اجرا نشد. برای اجرایش باید یک پرسنل در کلینیک ۱ ساخته شود. - دستکاری وضعیت جلسه تست نشد —
finishبدونstart،reopenروی ناحیهٔ بسته،completeدوباره. وظیفهٔ ۵-د پرامپت بود و اجرا نشد. - sweep فقط روی ۱۲۸ روتِ
GETبدون parameter اجرا شد. ۹۴ روتGETپارامتردار و همهٔ روتهایPOST/PATCH/DELETEدر sweep نبودند؛ زیرمجموعهشان در تست IDOR دستی پوشش داده شد. switch-contextبا کاربر چندنقشی تست نشد. کاربرmultiroleساخته و تأیید شد، ولی سناریوی «بعد از سوییچ به پرسنل هنوز به مسیر منشی میرسد؟» اجرا نشد.phpstanسبز نیست — ۱۷ خطا. هیچکدام از تغییرات این آدیت نیستند (فایلها:MyAppointmentsController,AuthController,BillingController,ClinicServiceController,ServiceItem,DoctorClaimService,InventoryService,PatientService,RecordNumberGenerator,ResourceController,SecretaryService,HealthController). قبل از این آدیت هم همین ۱۷ تا بودند.- تست تزریق (
inject) روی اندپوینتهای جدید اجرا نشد. درایور آن را بهصورت هدفمند میخواهد و در این پاس فقط روی سطح قدیمی اجرا شده بود.
تغییرات اعمالشده
src/Treatment/Controller/TreatmentProtocolController.php گیت services.view / services.update
tests/Secretary/SecretaryResourceEnforcementTest.php ۴ تست رگرسیون
docs/api/treatment.md مجوز هر سه اندپوینت
.claude/skills/qa-clinicpro/driver.mjs ماتریس نقشهای واقعی
../.claude/skills/symfony-security-audit/driver.mjs ماتریس نقشها + پرسوناهای staff و multirole
تستها: ddev exec php bin/phpunit → ۱۵۴۸ تست، ۴۷۹۶ assertion، سبز (۱۹ PHPUnit notice، همه
از قبل موجود).
دادهٔ DB: پروب DELETE روی سرویسِ کلینیک ۱ اجرا شد؛ آن سرویس پروتکل نداشت، پس چیزی حذف نشد.
تنها پروتکلِ موجود (service_item_id=15، کلینیک ۳) دستنخورده است.
پیشنهادها
- ارتقای
react-router— تسک جدا با تست دستی مسیرهای پنل. یافتهٔ ۲. - ردیابی
lodash-es— کدام dependency آن را میکشد؛ اگر transitive است، override. - sanitize سمت سرور برای بدنهٔ بلاگ — یافتهٔ ۵ را مستقل از CKEditor میبندد.
- یک
ClinicStaffدر tenant دوم — تا آدیت بعدی بتواند IDOR بیننقشیِ پرسنل را واقعاً بزند. - تست ساختاری برای گیت کنترلرها — چیزی شبیه
TenantSchemaCoverageTestکه کنترلر جدیدِ بدون گیت مجوز را قرمز کند. یافتهٔ ۱ دقیقاً از همین شکاف آمد:#[IsGranted('IS_AUTHENTICATED_FULLY')]سطح-کلاس، درایور را راضی میکند ولی هیچ مجوزی را enforce نمیکند.
پایان گزارش — درایور symfony-security-audit + پروب دستی با JWT واقعی هر نقش + بازرسی کد. هر
یافته بازتولید شده است.