شرح ترويسات الأمان: CSP وHSTS وما تبقّى
دليل عملي إلى CSP بقيم nonce وstrict-dynamic، وHSTS والتحميل المسبق، وframe-ancestors وReferrer-Policy وPermissions-Policy وCOOP، مع إعداد لـ Next.js.
· قراءة في 7 دقائق · Lina Source LLC
ترويسات الأمان تعليمات يعطيها خادمك للمتصفح: لا تشغّل إلا هذه السكربتات، ولا تخاطبني إلا عبر HTTPS، ولا تدع مواقع أخرى تضع هذه الصفحة في إطار. وهي لا تصلح الأخطاء في كودك، لكنها تحدّ مما يستطيع المهاجم فعله بها. فثغرة برمجة نصية عابرة للمواقع خلف سياسة محتوى صارمة مشكلة أصغر بكثير من الثغرة نفسها بلا سياسة.
ومعظم هذه الترويسات سطر واحد من الإعدادات. والاستثناء هو CSP التي تحتاج تخطيطًا. يغطي هذا الدليل ما تفعله كل ترويسة، وقيمًا معقولة لتطبيق ويب نموذجي، وكيفية طرحها دون كسر الإنتاج.
Content-Security-Policy
تخبر CSP المتصفح بمصادر السكربتات والأنماط والصور والإطارات والاتصالات المسموح بها. ومهمتها الأساسية منع السكربتات المحقونة من العمل. وقد تبدو قائمة سماح بالنطاقات هي المقاربة البديهية، لكن لها تاريخًا طويلًا من الالتفافات: فأي شبكة CDN مسموح بها تستضيف محتوى مستخدمين أو نسخًا قديمة من المكتبات يمكن استخدامها لتحميل سكربت يتحكم فيه المهاجم.
قيم nonce وstrict-dynamic
المقاربة التي تصمد هي سياسة قائمة على nonce. يولّد الخادم قيمة عشوائية لكل استجابة، ويضعها في ترويسة CSP ويضيفها بوصفها سمة nonce إلى كل وسم script يعرضه. أما وسوم السكربت المحقونة فلا تعرف القيمة العشوائية، ولذلك تُحجب. وإضافة 'strict-dynamic' تتيح للسكربتات التي يحمّلها سكربت موثوق أن تحمّل سكربتات أخرى، مما يبقي أدوات التجميع ومديري الوسوم تعمل دون سرد كل نطاق.
Content-Security-Policy:
default-src 'self';
script-src 'nonce-4AEemGb0xJptoIGFP3Nd' 'strict-dynamic';
style-src 'self' 'nonce-4AEemGb0xJptoIGFP3Nd';
img-src 'self' data: https:;
connect-src 'self';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
upgrade-insecure-requestsتُرسل الترويسة في سطر واحد؛ وقد لُفّت هنا لتسهيل القراءة. وبعض التوجيهات في تلك السياسة تفعل أكثر مما تبدو عليه. فـ object-src 'none' يحجب محتوى الملحقات القديمة. وbase-uri 'none' يمنع وسم base محقونًا من إعادة توجيه روابط السكربتات النسبية. وform-action 'self' يمنع النماذج المحقونة من إرسال بيانات الاعتماد إلى مكان آخر. ويجب أن تكون قيمة nonce غير قابلة للتنبؤ وجديدة في كل استجابة، ما يعني أن الصفحات التي تستخدمها لا يمكن تقديمها من ذاكرة تخزين ثابتة.
اطرحها بوضع Report-Only
أي سياسة CSP صارمة تُنشر على العمياء ستكسر شيئًا: مقتطف تحليلات، أو معالج حدث مضمّن، أو أداة من طرف ثالث. اشحن السياسة أولًا باسم Content-Security-Policy-Report-Only. عندئذٍ لا يطبّق المتصفح شيئًا، لكنه يبلّغ عن كل مخالفة إلى نقطة النهاية المذكورة في توجيه report-to أو report-uri. اجمع التقارير مدةً، وأصلح أو اسمح بما هو مشروع، ثم بدّل اسم الترويسة إلى وضع الإنفاذ. وأبقِ التبليغ مفعّلًا بعد الإنفاذ، لأن المخالفات الجديدة إما انحدارات وإما هجمات. وتوقّع ضجيجًا في التقارير من امتدادات المتصفح التي تحقن سكربتاتها في الصفحات؛ رشّحها بحسب المصدر قبل أن تقرر ما تسمح به.
Strict-Transport-Security
تخبر HSTS المتصفح باستخدام HTTPS لنطاقك مدةً محددة، حتى لو كتب المستخدم http:// أو نقر رابطًا قديمًا. وهذا يغلق النافذة التي يستطيع فيها مهاجم على الشبكة اعتراض أول طلب HTTP عادي وإزالة إعادة التوجيه إلى HTTPS. ولا تحترم المتصفحات هذه الترويسة إلا حين تصل عبر HTTPS، كما ترفض السماح للمستخدمين بتجاوز أخطاء الشهادات على نطاق محمي بـ HSTS، وهو المقصود، لكنه أيضًا الخطر إذا انتهت صلاحية شهادة.
ابدأ بقيمة max-age قصيرة، كيوم واحد، وتأكد من أن شيئًا لم ينكسر، ثم ارفعها إلى سنة أو سنتين. ويوسّع includeSubDomains القاعدة لتشمل كل نطاق فرعي، لذا تحقق أولًا من أن جميعها تقدّم HTTPS صالحًا، بما في ذلك المواقع التسويقية القديمة والأدوات الداخلية على النطاق نفسه.
أما توجيه preload، مقترنًا بتقديم طلب إلى قائمة التحميل المسبق في المتصفحات، فيغرس نطاقك داخل المتصفحات بحيث تستخدم الزيارة الأولى نفسها HTTPS. وهو يتطلب max-age لا يقل عن سنة مع includeSubDomains. عامله بوصفه بابًا باتجاه واحد: الإزالة من القائمة ممكنة لكنها تستغرق وقتًا طويلًا لتصل إلى المستخدمين، وأي نطاق فرعي لا يستطيع تقديم HTTPS يصبح غير قابل للوصول في هذه الأثناء.
الترويسات ذات السطر الواحد
- X-Content-Type-Options: nosniff. يمنع المتصفحات من تخمين نوع محتوى مخالف لما أعلنته، ما يحول دون تنفيذ ملف مرفوع يُقدَّم كنص بوصفه سكربتًا.
- frame-ancestors (داخل CSP) وX-Frame-Options: DENY. يتحكمان بمن يستطيع تضمين صفحاتك في إطار، وهو الدفاع ضد سرقة النقرات. وتستخدم المتصفحات الحديثة frame-ancestors وتتجاهل X-Frame-Options عند ضبط الاثنين؛ أبقِ X-Frame-Options للعملاء القدامى. واستخدم 'self' أو SAMEORIGIN إذا كنت تضع صفحاتك أنت داخل إطار.
- Referrer-Policy: strict-origin-when-cross-origin. يرسل أصلك فقط إلى المواقع الأخرى، لا المسار الكامل وسلسلة الاستعلام. وهذا يمنع تسرّب الرموز والمعرّفات الموجودة في الروابط إلى أطراف ثالثة. استخدم no-referrer للصفحات الحساسة بوجه خاص.
- Permissions-Policy. يعطّل مزايا المتصفح التي لا تستخدمها، مثل camera=() وmicrophone=() وgeolocation=() وpayment=()، حتى لا يستطيع كود محقون أو مضمّن طلبها.
- Cross-Origin-Opener-Policy: same-origin. يضع صفحتك في مجموعة سياق تصفح خاصة بها، فلا تستطيع نافذة فتحها موقع آخر الاحتفاظ بمرجع إليها. استخدم same-origin-allow-popups إذا كنت تعتمد على نوافذ OAuth أو الدفع المنبثقة.
- Cross-Origin-Resource-Policy: same-origin أو same-site. يخبر المتصفحات بألا تسمح لأصول أخرى بتحميل استجاباتك كصور أو سكربتات أو موارد فرعية أخرى. استخدم same-site إذا كنت تقدّم الأصول من نطاق فرعي شقيق، وcross-origin للأصول المقصود تضمينها في مواضع أخرى.
ويمكنك إسقاط X-XSS-Protection. فالمرشّح الذي كانت تتحكم به أُزيل من المتصفحات الحديثة، وCSP هي البديل. وإن أصرّ ماسح ما، فاضبطها على 0. وبالمثل، تجنّب الترويسات التي لا تضيف إلا معلومات للمهاجم: X-Powered-By ولافتات Server التفصيلية تكشف إطار عملك ونسخته مجانًا. وفي Next.js، اضبط poweredByHeader: false في الإعدادات لإزالة الأولى.
إعداد لـ Next.js
الترويسات الثابتة مكانها next.config.ts، حيث تطبّقها دالة headers() على كل مسار. والقيم أدناه إعداد افتراضي معقول لتطبيق لا يضمّن نفسه في إطارات، ولا يستخدم الكاميرا أو الموقع، ويقدّم أصوله بنفسه. اضبط كل قيمة بما يفعله تطبيقك فعلًا بدل نسخها على العمياء.
// next.config.ts
import type { NextConfig } from "next";
const securityHeaders = [
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains",
},
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=(), payment=()",
},
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Resource-Policy", value: "same-origin" },
];
const nextConfig: NextConfig = {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};
export default nextConfig;أما CSP فتحتاج قيمة nonce جديدة لكل طلب، لذا تُضبط في middleware بدلًا من ذلك. يقرأ Next.js قيمة nonce من ترويسة Content-Security-Policy في الطلب ويطبّقها على سكربتات إطار العمل نفسه أثناء العرض. ويمكنك قراءتها في مكوّن خادم عبر headers().get("x-nonce") لوسوم السكربت الخاصة بك.
// middleware.ts (in Next.js 16 the file is proxy.ts and the function is proxy)
import { NextResponse, type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
"img-src 'self' blob: data:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"upgrade-insecure-requests",
].join("; ");
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("Content-Security-Policy", csp);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set("Content-Security-Policy", csp);
return response;
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};والصفحات المعروضة بقيمة nonce يجب أن تُعرض ديناميكيًا، لأن صفحة ثابتة ستعيد استخدام قيمة nonce واحدة لكل زائر. وأثناء الطرح، غيّر اسم الترويسة في الموضعين إلى Content-Security-Policy-Report-Only وأضف نقطة نهاية للتبليغ. وفي بيئة التطوير، قد تحتاج React إلى 'unsafe-eval' ضمن script-src لمزايا التنقيح؛ أضفها فقط حين يكون NODE_ENV هو development.
كيف تتحقق
- شغّل curl -sI https://your-domain.example/ على الصفحة الرئيسية وعلى صفحة ديناميكية ومسار API وأصل ثابت، واقرأ الترويسات في كل منها. فالترويسات المضبوطة على صفحات HTML وحدها ثغرة شائعة.
- افتح أدوات مطوّري المتصفح. تظهر مخالفات CSP في وحدة التحكم مع التوجيه والمورد المحجوب، وتعرض لوحة الشبكة الترويسات التي قُدّمت فعلًا.
- افحص خلف شبكة CDN أو الوكيل العكسي، وكذلك عند المصدر. فالوكلاء أحيانًا يجرّدون الترويسات أو يكررونها أو يتجاوزونها، وترويستا CSP المتعارضتان تُنفَّذان معًا، فتصبح السياسة الفعلية أشد من كلتيهما.
- استخدم فاحصًا عامًا مثل Mozilla HTTP Observatory لرأي ثانٍ سريع، وCSP Evaluator من Google لرصد توجيهات CSP الضعيفة.
- أضف اختبارًا في CI يطلب المسارات الرئيسية ويتحقق من وجود الترويسات، حتى لا تسقطها إعادة هيكلة للإعدادات بصمت.
الترويسات إعدادات، ولهذا تحديدًا تنحرف: معالج مسار جديد يبني استجابته بنفسه، وقاعدة وكيل تتجاوز الافتراضيات، وسياسة CSP خُفّفت بـ 'unsafe-inline' لفكّ حجب إصدار. راجعها كما تراجع الكود. يقرأ CodeAuditAgent الإعدادات في مستودع عام أو مقتطف ملصوق مع بقية الكود، ويبلّغ عن الترويسات الغائبة أو المُضعَفة مع مستوى الخطورة والإعداد المقتبس وإصلاح مقترح.
وترتيب عمل معقول: الترويسات ذات السطر الواحد اليوم، وHSTS بقيمة max-age قصيرة هذا الأسبوع، وسياسة CSP قائمة على nonce في وضع Report-Only حالما تستطيع جمع التقارير.