সরাসরি পর্বে যান
পর্ব ৪৩ · ৫৯-এর মধ্যে · মডিউল ৬

নিরাপদ JavaScript — XSS, CSP, CORS আর CSRF ঠেকানো

Secure JavaScript — Same-Origin Policy, CORS & Preflight, XSS Prevention, Content Security Policy, CSRF, SameSite Cookies, Clickjacking and Supply-Chain Safety

Taught in Bangla, this lesson builds a security mindset for front-end code: the same-origin policy, CORS and preflight requests, how XSS turns data into code and how to stop it with textContent, encoding, sanitization and URL checks, Content Security Policy, CSRF, SameSite cookies and clickjacking, secret keys, supply-chain risk, HTTPS and security headers.

এই পর্বে যা শিখবেন

  • Threat model-এর ভাবনা: কে কী পাঠাতে পারে, আর কেন client-কে কখনো বিশ্বাস করা যায় না
  • Same-origin policy কী আটকায় আর কী দেয়; CORS-এর simple আর preflight অনুরোধ, দরকারি header আর সাধারণ ভুল কনফিগারেশন
  • XSS-এর তিন রূপ (stored, reflected, DOM), বিপজ্জনক sink-এর তালিকা, আর প্রতিটির নিরাপদ বিকল্প
  • CSP-র nonce, hash আর report-only মোড; CSRF ঠেকাতে SameSite, token আর Origin যাচাই; clickjacking আর frame-ancestors
  • Token কোথায় রাখবেন, supply-chain ঝুঁকি, security header-এর চেকলিস্ট, OWASP Top 10 — আর AI-এর লেখা কোডের নিরাপত্তা যাচাই

১ · নিরাপত্তার ভাবনা — কে কী পাঠাতে পারে?

ধরুন আপনি shop.com.bd নামে একটি অনলাইন মুদি দোকান বানাচ্ছেন। পণ্যের পাতায় ক্রেতারা রিভিউ লেখেন, সার্চ বক্সে খোঁজেন, প্রোফাইলে নিজের ওয়েবসাইটের ঠিকানা দেন, আর লগইন করে বিকাশ বা কার্ডে টাকা দেন। এখন একটু অন্যভাবে ভাবুন: এই ঘরগুলোতে যে কেউ যা খুশি লিখতে পারেন। আর আপনার ক্রেতা লগইন অবস্থায় অন্য কোনো সাইট খুললে সেই সাইটের পাতাও তাঁর ব্রাউজারে JavaScript চালায়।

নিরাপত্তার কাজ শুরু হয় এই প্রশ্নগুলো দিয়ে: কে আক্রমণ করতে পারে? কোন পথে কী পাঠাতে পারে? কোন জিনিসটি রক্ষা করা সবচেয়ে জরুরি — ক্রেতার session, টাকা, ব্যক্তিগত তথ্য? এই লিখিত উত্তরের নাম threat model. ওয়েবে এর প্রথম সূত্রটি কঠিন কিন্তু সরল: ব্রাউজারে যা কিছু চলে, তা ব্যবহারকারীর নিয়ন্ত্রণে। তাই পর্ব ৩৯-এর ফর্ম-যাচাই ব্যবহারকারীর সুবিধার জন্য; দাম, অনুমতি আর ইনপুটের আসল যাচাই হয় সার্ভারে।

একটি বড় আবাসিক ভবনের কথা ভাবুন। প্রতিটি ফ্ল্যাটের নিজের তালা আছে — পাশের ফ্ল্যাটের লোক আপনার ঘরে ঢুকতে পারেন না (এটি same-origin policy). গেটে দারোয়ান আছেন, কিন্তু দারোয়ান অতিথিকে ঢুকতে দেন কি না, সেটি ঠিক করেন ফ্ল্যাটের মালিক — “৫বি-তে রহিম সাহেব আসবেন” (এটি CORS). বিপদ আসে দুইভাবে। এক, কেউ একটি “চিঠি” পাঠাল, আর আপনি না পড়েই সেটিকে নির্দেশ ভেবে মেনে নিলেন — “এই চিঠির বাহককে আলমারির চাবি দাও” (এটি XSS: ডেটাকে কোড ভাবা)। দুই, কেউ আপনার নামে দারোয়ানকে ফোন করল, আর দারোয়ান কণ্ঠ না যাচাই করে আপনার নামে কাজ করে দিলেন (এটি CSRF: অন্যের হয়ে কাজ করিয়ে নেওয়া)।

এই পর্বের সব প্রতিরক্ষা আসলে চারটি নীতির প্রয়োগ। (১) সব ইনপুট ডেটা — কখনো কোড নয়। (২) ডেটা যেখানে বসবে, সেই প্রসঙ্গের নিয়মে encode করুন। (৩) পাতায় কী চলতে পারবে তা সীমিত করুন (CSP). (৪) পরিচয়ের প্রমাণ (cookie) সাবধানে রাখুন, আর প্রতিটি সিদ্ধান্ত সার্ভারে আবার যাচাই করুন। একটি স্তর ব্যর্থ হলেও পরের স্তর যেন ক্ষতি থামায় — এই ধারণার নাম defense in depth.

মূল ধারণা

Front-end নিরাপত্তার প্রায় সব বাগ একটি বাক্যে ধরা যায়: যে লেখা বাইরে থেকে এসেছে, সেটি কোথাও HTML, URL বা JavaScript হিসেবে পড়া হয়ে গেছে। “এই লেখা কি কখনো কোড হয়ে যেতে পারে?” — প্রতিটি লাইনে এই প্রশ্ন করার অভ্যাসই সবচেয়ে বড় প্রতিরক্ষা।

২ · Same-origin policy — ব্রাউজারের মূল দেয়াল

পর্ব ৩৫-এ দেখেছেন, একটি origin হলো protocol + host + port-এর জোড়। https://shop.com.bd আর https://api.shop.com.bd আলাদা origin (host আলাদা); http://shop.com.bd আলাদা (protocol আলাদা); https://shop.com.bd:8443-ও আলাদা (port আলাদা)। Same-origin policy বলে: একটি origin-এর পাতার JavaScript অন্য origin-এর জিনিস পড়তে পারবে না। কিন্তু অন্য সাইটের জিনিস জুড়ে দেখানো (embed) ঠিকই চলে — নিয়মটি সূক্ষ্ম:

কাজঅন্য origin-এ চলে?ব্যাখ্যা
<img>, <script src>, <link rel=stylesheet>, <video>হ্যাঁ — দেখানো বা চালানো যায়CDN-এর ছবি বা library এভাবেই আসে; কিন্তু তাদের ভেতরের ডেটা JavaScript পড়তে পারে না
<form> জমা দেওয়া, link-এ যাওয়াহ্যাঁ — পাঠানো যায়অনুরোধ যায় (cookie-সহ হতে পারে!), কিন্তু উত্তর পাঠানো পাতা পড়তে পারে না — CSRF-এর মূল এখানে
fetch-এর উত্তর পড়ানা — যদি সার্ভার CORS-এ অনুমতি না দেয়অনুরোধ হয়তো সার্ভারে পৌঁছায়, কিন্তু ব্রাউজার উত্তরটি JavaScript-কে দেয় না
<iframe>-এর ভেতরের DOM পড়ানাঅন্য origin-এর frame দেখানো যায়, তার ভেতরে হাত দেওয়া যায় না; বার্তা আদান-প্রদানের নিরাপদ পথ postMessage
localStorage, IndexedDB, cookie পড়ানাপ্রতিটি origin-এর storage আলাদা (পর্ব ৪০); cookie-র নিয়ম একটু আলাদা — domain ধরে চলে

এখানে একটি সূক্ষ্ম কিন্তু জরুরি পার্থক্য আছে: same-origin আর same-site এক জিনিস নয়। “Site” মানে scheme আর registrable domain — যে নামটি আপনি টাকা দিয়ে নিবন্ধন করেন। .com.bd নিজে একটি public suffix, তাই shop.com.bd-ই registrable domain; ফলে api.shop.com.bd আর shop.com.bd cross-origin কিন্তু same-site. Cookie-র SameSite নিয়ম (অংশ ৭) চলে “site” ধরে, CORS চলে “origin” ধরে।

Same-origin policy এসেছিল JavaScript-এর সাথেই — ১৯৯৫–৯৬ সালে Netscape Navigator 2-তে। এই একটি নিয়মই আজকের ওয়েব-নিরাপত্তার ভিত্তি। গল্পটি ভাবনার প্রশ্ন ১-এ।

৩ · CORS — সার্ভারের দেওয়া নিয়ন্ত্রিত অনুমতি

বাস্তব অ্যাপে প্রায়ই পাতা আর API আলাদা origin-এ থাকে: পাতা https://shop.com.bd-এ, API https://api.shop.com.bd-এ। Same-origin policy অনুযায়ী পাতার JavaScript API-র উত্তর পড়তে পারার কথা নয়। CORS (Cross-Origin Resource Sharing) এই দেয়ালে একটি দরজা খোলে — কিন্তু চাবি সার্ভারের হাতে। ব্রাউজার প্রতিটি cross-origin অনুরোধে Origin: https://shop.com.bd header জুড়ে দেয়; সার্ভার উত্তরে Access-Control-Allow-Origin: https://shop.com.bd লিখলে ব্রাউজার উত্তরটি JavaScript-কে দেয়, না লিখলে দেয় না।

Simple অনুরোধ বনাম preflight

কিছু অনুরোধ ব্রাউজার সরাসরি পাঠায়, কারণ এগুলো একটি সাধারণ HTML ফর্মও পাঠাতে পারে — এদের বলে simple request: method GET, HEAD বা POST; শুধু নিরাপদ তালিকার header; আর Content-Type হলে কেবল application/x-www-form-urlencoded, multipart/form-data বা text/plain. এর বাইরের যেকোনো কিছু — PUT, DELETE, Content-Type: application/json, Authorization header — পাঠানোর আগে ব্রাউজার সার্ভারকে জিজ্ঞেস করে নেয়। এই আগাম জিজ্ঞাসার নাম preflight request:

  1. JavaScriptfetch(PUT, JSON)shop.com.bd-এর পাতা
  2. PreflightOPTIONS + Originব্রাউজার নিজে পাঠায়
  3. সার্ভারের উত্তরAllow-* headerapi.shop.com.bd
  4. আসল অনুরোধPUT + cookieঅনুমতি মিললে তবেই
  5. উত্তর পড়াres.json()বা TypeError
পাতার কোডJSON body আর cookie — তাই preflight লাগবে
await fetch("https://api.shop.com.bd/cart/7", {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  credentials: "include",            // cookie পাঠাও
  body: JSON.stringify({ qty: 2 }),
});
ব্রাউজারের প্রশ্নbody নেই, cookie নেই
OPTIONS /cart/7 HTTP/1.1
Host: api.shop.com.bd
Origin: https://shop.com.bd
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type
সার্ভারের অনুমতিনির্দিষ্ট origin, "*" নয়
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://shop.com.bd
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 600          ← ১০ মিনিট মনে রাখো
Vary: Origin
আসল অনুরোধএখন cookie আর body যায়
PUT /cart/7 HTTP/1.1
Origin: https://shop.com.bd
Cookie: session=…
Content-Type: application/json

{"qty":2}
দুই সম্ভাব্য শেষসিদ্ধান্ত ব্রাউজারের
উত্তরে Access-Control-Allow-Origin: https://shop.com.bd
  → res.ok, await res.json() — সব স্বাভাবিক
header নেই বা অন্য origin লেখা
  → fetch reject: TypeError: Failed to fetch
  → কারণ শুধু DevTools Console-এ লেখা থাকে
  1. পাতা একটি “জটিল” অনুরোধ চায়। PUT method আর application/json — দুটোর কোনোটিই সাধারণ HTML ফর্ম পাঠাতে পারে না। তাই ব্রাউজার ধরে নেয়, পুরোনো সার্ভার হয়তো এমন অনুরোধের জন্য প্রস্তুত নয়; আগে জিজ্ঞেস করা দরকার।
  2. Preflight — OPTIONS. ব্রাউজার নিজে একটি OPTIONS অনুরোধ পাঠায়: কোন origin থেকে, কোন method আর কোন header নিয়ে আসতে চাই। এতে body বা cookie থাকে না, আর আপনার JavaScript এটি দেখতেও পায় না — দেখবেন শুধু DevTools-এর Network tab-এ।
  3. সার্ভার অনুমতি দেয়। সার্ভার তার অনুমোদিত তালিকা দেখে উত্তর দেয়। Cookie-সহ অনুরোধে (credentials: "include") origin অবশ্যই নির্দিষ্ট হতে হবে আর Allow-Credentials: true লাগবে; "*" দিলে ব্রাউজার উত্তর আটকে দেয়। Max-Age থাকলে কিছুক্ষণ একই প্রশ্ন আর করা হয় না (ব্রাউজার নিজস্ব সর্বোচ্চ সীমা রাখে)। Vary: Origin cache-কে জানায়, উত্তরটি origin-ভেদে আলাদা।
  4. আসল অনুরোধ যায়। অনুমতি মিললে তবেই আসল PUT যায়, এবার cookie আর body-সহ। খেয়াল করুন: simple অনুরোধে preflight হয় না — অনুরোধ সরাসরি সার্ভারে পৌঁছায়, সার্ভার কাজও করে ফেলে; ব্রাউজার শুধু উত্তর পড়তে দেয় না। তাই CORS কখনো “অনুরোধ আটকানোর” ব্যবস্থা নয়।
  5. পড়া, নয়তো TypeError. উত্তরে সঠিক Access-Control-Allow-Origin থাকলে JavaScript উত্তর পড়ে। না থাকলে fetch সাধারণ TypeError দিয়ে reject হয় — নিরাপত্তার কারণে কারণটি JavaScript-কে জানানো হয় না। Console-এ Edge/Chrome লেখে এরকম: … has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

সবচেয়ে বিপজ্জনক ভুল কনফিগারেশন

CORS error দেখে বিরক্ত হয়ে অনেকে সার্ভারে যা খুশি বসিয়ে দেন। তিনটি ধরন সবচেয়ে বেশি দেখা যায়। (১) যেকোনো origin ফিরিয়ে দেওয়া: অনুরোধের Origin যা-ই হোক, হুবহু Access-Control-Allow-Origin-এ বসিয়ে দেওয়া, সাথে Allow-Credentials: true. এর মানে পৃথিবীর যেকোনো সাইট আপনার ক্রেতার cookie দিয়ে তাঁর অর্ডার আর ঠিকানা পড়তে পারবে। (২) "*" দিয়ে সব খুলে দেওয়া: পাবলিক, লগইন-ছাড়া ডেটার (যেমন পণ্যের দামের তালিকা) জন্য ঠিক আছে, অন্য কিছুর জন্য নয়। (৩) আলগা মিল: “origin-এ shop.com.bd শব্দটি আছে কি না” দেখা — তাহলে shop.com.bd.evil.example-ও পাস করে যায়। নিরাপদ পথ একটাই: অনুমোদিত origin-এর হুবহু তালিকা, আর null origin কখনো অনুমোদিত নয়।

“CORS error সারানোর” নামে যে পরামর্শগুলো এড়িয়ে চলবেন: ব্রাউজারের নিরাপত্তা বন্ধ করে চালানো, “CORS unblock” ধরনের extension, mode: "no-cors" (পর্ব ৩৫: এতে শুধু একটি পড়া-যায়-না এমন opaque উত্তর আসে), আর অপরিচিত “CORS proxy” সেবা — যার মধ্য দিয়ে আপনার ব্যবহারকারীর ডেটা অন্যের সার্ভার ঘুরে আসে। CORS error মানে সার্ভার এখনো আপনার origin-কে অনুমতি দেয়নি। সমাধান সার্ভারের কনফিগারেশনে।

ডেভেলপমেন্টে একটি বৈধ শর্টকাট আছে: dev proxy. Vite-এর মতো dev server (পর্ব ৪৬) নিজের /api পথের অনুরোধ পেছনে আসল API-তে পাঠিয়ে দিতে পারে; ব্রাউজারের চোখে তখন সব একই origin, তাই CORS প্রশ্নই ওঠে না।

এবার সত্যিকারের একটি সার্ভার দিয়ে দেখি। নিচের Node.js প্রোগ্রামে api.shop.com.bd-এর মতো একটি ছোট API শুধু https://shop.com.bd-কে অনুমতি দেয়। আমরা দুটি origin থেকে preflight পাঠাই, তারপর অননুমোদিত origin থেকে একটি সাধারণ GET:

const http = require("node:http");
const ALLOWED = new Set(["https://shop.com.bd"]);              // অনুমোদিত origin-এর তালিকা
const server = http.createServer((req, res) => {
  const origin = req.headers.origin, ok = ALLOWED.has(origin);  // হুবহু মিল চাই
  const h = { "Content-Type": "application/json", Vary: "Origin", Connection: "close" };
  if (ok) Object.assign(h, { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" });
  if (req.method !== "OPTIONS") return res.writeHead(200, h).end(JSON.stringify({ items: 3 }));
  if (ok) Object.assign(h, { "Access-Control-Allow-Methods": "GET, POST",   // preflight-এর উত্তর
    "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Max-Age": "600" });
  res.writeHead(204, h).end();
});
const show = (r, ...names) => names.map((n) => `${n}=${r.headers.get(n) ?? "(নেই)"}`).join(" ");
server.listen(0, "127.0.0.1", async () => {
  const api = `http://127.0.0.1:${server.address().port}/api/cart`;
  for (const origin of ["https://shop.com.bd", "https://evil.example"]) {
    const pre = await fetch(api, { method: "OPTIONS", headers: { Origin: origin,
      "Access-Control-Request-Method": "POST", "Access-Control-Request-Headers": "content-type" } });
    console.log(origin, pre.status, show(pre, "access-control-allow-origin", "access-control-allow-methods"));
  }
  const res = await fetch(api, { headers: { Origin: "https://evil.example" } });
  console.log("Node পড়ল:", res.status, await res.json(), show(res, "access-control-allow-origin"));
  server.close();
});

প্রথম লাইন https://shop.com.bd 204 access-control-allow-origin=https://shop.com.bd access-control-allow-methods=GET, POST — অনুমোদিত origin পেল নিজের নাম আর method-এর তালিকা। দ্বিতীয় লাইন https://evil.example 204 access-control-allow-origin=(নেই) access-control-allow-methods=(নেই) — অনুমতির header নেই; ব্রাউজার হলে এখানেই থামত। আসল শিক্ষা শেষ লাইনে: Node পড়ল: 200 { items: 3 } access-control-allow-origin=(নেই). অনুমতি না থাকা সত্ত্বেও Node.js পুরো উত্তর পড়ে ফেলল! কারণ CORS প্রয়োগ করে ব্রাউজার — নিজের ব্যবহারকারীকে অন্য সাইটের পাতা থেকে রক্ষা করতে। Node, curl বা মোবাইল অ্যাপে CORS নেই। তাই CORS কখনো API-র পাহারাদার নয়; কে কী পড়তে পারবে, তা ঠিক করে সার্ভারের লগইন আর অনুমতি-যাচাই। (ব্রাউজারে Origin header JavaScript নিজে বসাতে পারে না, ব্রাউজার বসায়; Node-এ আমরা হাতে বসিয়েছি।)

৪ · XSS — যখন ডেটা কোড হয়ে যায়

XSS (Cross-Site Scripting)-এর মূল কথা: অন্য কারও দেওয়া লেখা আপনার পাতায় HTML বা JavaScript হিসেবে পড়া হয়ে গেছে, আর সেটি চলছে আপনার origin-এর সব ক্ষমতা নিয়ে। Same-origin policy এখানে সাহায্য করে না, কারণ ঢোকানো কোড এখন আপনার পাতারই অংশ — সে ব্যবহারকারীর হয়ে অনুরোধ পাঠাতে বা নকল লগইন ফর্ম দেখাতে পারে। পরিচিত নিরীহ উদাহরণ হলো এই লেখাটি, যা আমরা এখানে শুধু লেখা হিসেবে দেখাচ্ছি: <img src=x onerror=alert(1)>. কোনো পাতা এটিকে HTML হিসেবে বসালে ব্রাউজার একটি ছবি বানাতে চেষ্টা করে, ছবি লোড ব্যর্থ হয়, আর onerror-এর ভেতরের কোড চলে। XSS তিন রূপে আসে:

🗄️ Stored XSS

Stored XSS: ক্ষতিকর লেখা সার্ভারে জমা থাকে — পণ্যের রিভিউ, মন্তব্য, প্রোফাইলের নাম। যে-ই পাতাটি খোলেন, তাঁর ব্রাউজারে চলে। সবচেয়ে বিপজ্জনক, কারণ ক্ষতিগ্রস্তকে কিছুই করতে হয় না।

উদাহরণ: রিভিউ সেকশন

🔁 Reflected XSS

Reflected XSS: অনুরোধের অংশ — যেমন ?q=… — পাতা তখনই ফিরিয়ে দেখায়: “‘চাল’-এর জন্য ১২টি ফল”. আক্রমণকারী একটি সাজানো link ছড়ায়, যিনি চাপেন তাঁর ব্রাউজারে চলে।

উদাহরণ: সার্চ পাতা

🌳 DOM-based XSS

DOM-based XSS: সার্ভার নির্দোষ; পুরো ঘটনা ব্রাউজারের JavaScript-এ। location.hash, postMessage-এর ডেটা বা API-র JSON গিয়ে পড়ে কোনো বিপজ্জনক API-তে। আধুনিক SPA-তে এটিই সবচেয়ে প্রচলিত।

উদাহরণ: hash দেখে ট্যাব খোলা

যে API-তে লেখা দিলে তা HTML বা কোড হয়ে যায়, তাদের বলে sink. কোড রিভিউতে এই নামগুলো দেখলেই থামুন:

বিপজ্জনক sinkকী হয়নিরাপদ বিকল্প
el.innerHTML = s, outerHTML, insertAdjacentHTMLলেখা HTML হিসেবে parse হয়; event attribute চলেtextContent, createElement + append (পর্ব ৩৭)
document.write(s)লেখা সরাসরি HTML parser-এ যায়ব্যবহারই করবেন না
eval(s), new Function(s), setTimeout("…")লেখা সরাসরি JavaScript হিসেবে চলেfunction পাঠান: setTimeout(() => …); ডেটার জন্য JSON.parse
a.href = s, iframe.src = sjavascript: URL চাপলে কোড চলেnew URL() দিয়ে যাচাই — শুধু http:/https:
el.setAttribute("onclick", s), HTML-এ onclick="…"attribute-এর লেখা কোড হয়ে যায়addEventListener (পর্ব ৩৮)

একটি চমকপ্রদ তথ্য: innerHTML দিয়ে বসানো <script> tag HTML5-এর নিয়মে চলে না. কিন্তু <img onerror>, <svg onload>-এর মতো event attribute ঠিকই চলে — তাই innerHTML-এ অবিশ্বস্ত লেখা দেওয়া সবসময়ই বিপজ্জনক। এবার দেখুন একটি stored XSS-এর পুরো জীবন, আর প্রতিটি ধাপে কোন প্রতিরক্ষা কাজ করে:

  1. ইনপুটPOST /reviewsযাচাই
  2. জমাINSERT … (?)কাঁচা ডেটা হিসেবে
  3. ফেরতGET /reviews → JSONঅন্য ক্রেতার ব্রাউজারে
  4. দেখানোtextContentআসল প্রতিরক্ষা
  5. শেষ স্তরCSPভুল হলেও থামায়
ইনপুট যাচাইসাহায্য করে, কিন্তু যথেষ্ট নয়
রিভিউ: "<img src=x onerror=alert(1)> চাল ভালো"
সার্ভার: দৈর্ঘ্য ≤ ১০০০? রেটিং ১–৫? ধরন string?
// “< থাকলে বাতিল” — তাহলে "ভাত < বিরিয়ানি" লেখা যায় না
// ইনপুট যাচাই ব্যবসার নিয়মের জন্য; XSS-এর মূল সমাধান নয়
জমা রাখাযেমন এসেছে তেমনই
db.run("INSERT INTO reviews (text) VALUES (?)", [text]);
// placeholder (?) — SQL injection নেই
// আগেই HTML-escape করে রাখবেন না: JSON API, মোবাইল অ্যাপ,
// ইমেইল — প্রতিটির প্রসঙ্গ আলাদা; encode হয় দেখানোর সময়
অন্য ক্রেতা পাতা খুললেনলেখাটি এখনো নিছক ডেটা
{ "user": "করিম", "text": "<img src=x onerror=alert(1)> চাল ভালো" }
// JSON-এর ভেতরে এটি একটি string মাত্র — এখনো কোনো বিপদ নেই
// বিপদ শুরু হবে কেবল যদি পাতা এটিকে HTML হিসেবে পড়ে
দেখানো — সিদ্ধান্তের মুহূর্তএকটি property-র পার্থক্য
// ✗ li.innerHTML = r.text;   → img তৈরি হয়, onerror চলে
li.textContent = r.text;      // ✓ পর্দায় অক্ষরগুলোই দেখা যায়
// framework হলে: React-এর {r.text}, Vue-র {{ r.text }} — নিজে escape করে
শেষ প্রতিরক্ষা-স্তরকোথাও ভুল থেকে গেলেও
Content-Security-Policy: script-src 'self' 'nonce-…'
→ inline event handler (onerror) চলবে না, Console-এ সতর্কবার্তা
Set-Cookie: session=…; HttpOnly; Secure; SameSite=Lax
→ ঢোকানো কোড চললেও session cookie পড়তে পারত না
  1. ইনপুট — যাচাই করুন, কিন্তু ভরসা করবেন না। সার্ভারে দৈর্ঘ্য, ধরন আর ব্যবসার নিয়ম যাচাই হবেই। কিন্তু “বিপজ্জনক অক্ষর” খুঁজে বাদ দেওয়ার চেষ্টা (blocklist) প্রায় সবসময় ব্যর্থ হয় — হয় নিরীহ লেখা আটকায়, নয়তো কোনো নতুন রূপ ফসকে যায়।
  2. জমা — কাঁচা ডেটা হিসেবে। ডেটাবেসে লেখাটি যেমন এসেছে তেমনই রাখুন, placeholder দিয়ে, যাতে SQL injection না হয়। আগেই HTML-escape করে রাখলে একই ডেটা মোবাইল অ্যাপে &lt;img হয়ে দেখাবে, আর কোথাও দুবার escape হয়ে যাবে।
  3. ফেরত — এখনো ডেটা। JSON-এর ভেতরে লেখাটি নিছক একটি string. এখানে সমস্যা নেই — যতক্ষণ না কেউ এটিকে HTML হিসেবে পড়ে।
  4. দেখানো — আসল প্রতিরক্ষা এখানে। লেখা যে প্রসঙ্গে বসবে, সেই প্রসঙ্গের নিয়মে বসান। পাতার লেখা হিসেবে বসাতে textContent — ব্রাউজার তখন <-কে tag নয়, শুধু একটি অক্ষর হিসেবে দেখায়। একে বলে output encoding-এর মূল ভাবনা।
  5. শেষ স্তর — CSP আর HttpOnly. মানুষ ভুল করে; কোনো এক কোণে কেউ innerHTML লিখে ফেলবেন। ঠিকমতো লেখা CSP সেই ভুলেও inline কোড চলতে দেয় না, আর HttpOnly cookie ঢোকানো কোডের নাগালের বাইরে থাকে। এগুলো ক্ষতি কমায়, মূল সমাধানের বদলি নয়।

৫ · XSS ঠেকানো — textContent, encoding, sanitization আর URL যাচাই

প্রথম নিয়ম: ডেটা দেখান ডেটা হিসেবে

বেশিরভাগ ক্ষেত্রে সমাধান খুব সরল: লেখা দেখাতে textContent, attribute বসাতে setAttribute বা সরাসরি property (img.alt = name), আর কাঠামো বানাতে createElement + append. সার্চ পাতার উদাহরণে results.innerHTML = `“${q}”-এর জন্য ফল` না লিখে লিখুন title.textContent = `“${q}”-এর জন্য ফল` — reflected XSS শেষ। Template literal-এ HTML জোড়া দেওয়া (`<li>${r.text}</li>`) AI আর পুরোনো tutorial-এ সবচেয়ে বেশি দেখা XSS-এর উৎস।

প্রসঙ্গ বুঝে encoding

কখনো সার্ভারে HTML বানাতে হয়, বা পুরোনো কোডে string-এ HTML জোড়া এড়ানো যায় না। তখন লেখা বসানোর আগে encode করতে হয় — আর encode-এর নিয়ম প্রসঙ্গভেদে আলাদা। HTML লেখার ভেতরে < হবে &lt;, & হবে &amp;; attribute-এর ভেতরে quote-ও encode করতে হবে, আর attribute-এর মান সবসময় quote-এর ভেতরে রাখতে হবে; URL-এর query-তে encodeURIComponent বা URLSearchParams (পর্ব ৩৫); <script>-এর ভেতরে ডেটা বসাতে হলে JSON হিসেবে, আর তারও < escape করে। একটি প্রসঙ্গের encoding অন্য প্রসঙ্গে কাজ করে না: HTML-escape করা লেখাও href-এ বসালে javascript: URL ঠিকই চলবে।

HTML সত্যিই লাগলে — sanitization

ব্লগের মন্তব্যে bold বা link রাখতে দিতে চান? তখন textContent চলবে না; দরকার sanitization: অনুমোদিত tag আর attribute-এর তালিকা (allowlist) রেখে বাকি সব ছেঁটে ফেলা। নিজে regex দিয়ে sanitizer লিখবেন না — HTML-এর parse করার নিয়ম এত জটিল যে ব্রাউজার যেভাবে পড়বে, আপনার regex সেভাবে পড়বে না (পর্ব ৩০-এর সতর্কতা মনে করুন)। প্রচলিত পথ DOMPurify: el.innerHTML = DOMPurify.sanitize(comment). এটি ব্রাউজারের নিজের parser দিয়ে লেখাটি ভাঙে, তারপর allowlist-এর বাইরের সব — <script>, on… attribute, javascript: URL — সরিয়ে দেয়। ব্রাউজারে নিজস্ব HTML Sanitizer API (el.setHTML(input)) মানদণ্ড হওয়ার পথে, আর কিছু ব্রাউজারে আসতে শুরু করেছে; সব ব্রাউজারে না আসা পর্যন্ত প্রোডাকশনে DOMPurify-ই নির্ভরযোগ্য, আর ব্যবহারের আগে MDN-এ বর্তমান সমর্থন দেখে নিন।

Framework-এর স্বয়ংক্রিয় escape — আর তার পেছনের দরজা

React, Vue, Svelte, Angular (পর্ব ৫৪) ডিফল্টে লেখা escape করে বসায় — {review.text} বা {{ review.text }} নিরাপদ। কিন্তু প্রতিটির একটি ইচ্ছাকৃত পেছনের দরজা আছে, যার নামেই সতর্কবার্তা: React-এর dangerouslySetInnerHTML, Vue-র v-html, Svelte-এর {@html …}, Angular-এর bypassSecurityTrustHtml. এগুলোতে অবিশ্বস্ত ডেটা গেলে framework আপনাকে বাঁচাবে না। আর framework সাধারণত href-এর মান যাচাই করে না।

URL যাচাই আর Trusted Types

প্রোফাইলের “website” ঘরে কেউ লিখলেন javascript:alert(1). আপনি সেটি <a href>-এ বসালেন — যিনি link-এ চাপবেন, তাঁর পাতায় কোড চলবে। “javascript: দিয়ে শুরু হলে বাতিল” — এমন blocklist ফসকে যায় (বড় হাতের অক্ষর, শুরুতে ফাঁকা, মাঝে tab). সঠিক পথ allowlist: new URL() দিয়ে ব্রাউজারের নিজের নিয়মে parse করুন, তারপর protocol শুধু "https:" বা "http:" হলে গ্রহণ করুন। এর ওপরে আরেকটি স্তর আছে: Trusted Types. CSP-তে require-trusted-types-for 'script' দিলে innerHTML-এর মতো sink সাধারণ string নেওয়াই বন্ধ করে দেয় — শুধু আপনার অনুমোদিত policy-র (যেমন DOMPurify-চালিত) বানানো object নেয়। Chromium-ভিত্তিক ব্রাউজারে এটি ২০২০ সাল (Chrome 83) থেকে আছে; অন্য ব্রাউজারে সমর্থন পরে এসেছে বা আসছে।

এবার তিনটি নিরাপদ কৌশল একসাথে চালিয়ে দেখুন: রিভিউয়ের “ক্ষতিকর” লেখাটি textContent দিয়ে বসানো, একটি escapeHTML function, আর website ঘরের জন্য safeURL:

const review = '<img src=x onerror=alert(1)> চাল ভালো';   // আক্রমণকারীর "রিভিউ"
const website = "javascript:alert(1)";                         // প্রোফাইলের "website" ঘর

const li = document.createElement("li");
li.textContent = review;                                       // HTML নয়, শুধু লেখা
document.querySelector("#reviews").append(li);
console.log("element তৈরি হলো?", li.children.length, "| দেখাচ্ছে:", li.textContent);

const escapeHTML = (s) => s.replace(/[&<>"']/g, (c) =>
  ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
console.log(escapeHTML(review));

function safeURL(input) {
  try {
    const u = new URL(input, "https://shop.com.bd");
    return ["https:", "http:"].includes(u.protocol) ? u.href : null;
  } catch { return null; }
}
for (const w of [website, " JavaScript:alert(1)", "https://rahim.com.bd", "/about", "data:text/html,hi"])
  console.log(JSON.stringify(w), "→", safeURL(w));

ওপরের ছোট পাতায় রিভিউটি হুবহু অক্ষর হিসেবে দেখা যাচ্ছে, কোনো ছবি নেই। প্রথম লাইন element তৈরি হলো? 0 | দেখাচ্ছে: <img src=x onerror=alert(1)> চাল ভালো — li-এর ভেতরে কোনো child element নেই (0), পুরোটা একটি text node. দ্বিতীয় লাইন &lt;img src=x onerror=alert(1)&gt; চাল ভালো — HTML-এ বসালে ব্রাউজার < অক্ষর দেখাবে, tag বানাবে না; সব অক্ষর এক পাসে বদলানো হয়েছে বলে দুবার escape হয়নি। তারপর URL-গুলো: "javascript:alert(1)" → null আর " JavaScript:alert(1)" → null — বড় হাতের অক্ষর আর শুরুর ফাঁকা দিয়েও ফাঁকি দেওয়া গেল না, কারণ URL ব্রাউজারের নিয়মে parse করে protocol-কে ছোট হাতের "javascript:" বানায়। "https://rahim.com.bd" → https://rahim.com.bd/ গৃহীত; "/about" → https://shop.com.bd/about — আপেক্ষিক পথ base ধরে পূর্ণ হলো; "data:text/html,hi" → null — data: URL-ও বাদ, কারণ allowlist-এ নেই।

৬ · CSP — পাতায় কী চলতে পারবে, তার তালিকা

Content Security Policy হলো সার্ভারের পাঠানো একটি response header, যা ব্রাউজারকে বলে: এই পাতায় কোন উৎসের script, style, ছবি, frame চলতে পারবে। XSS-এর বাগ থেকে গেলেও ঢোকানো কোড চালানো তখন কঠিন হয়ে যায়। একটি আধুনিক, কড়া CSP দেখতে এরকম:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0mB4se64';
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'self';
  report-to csp-endpoint

লাইন ধরে পড়ুন। default-src 'self': আলাদা করে না বলা সব কিছু শুধু নিজের origin থেকে। script-src 'self' 'nonce-…': script চলবে নিজের origin-এর ফাইল থেকে, আর সেই inline script, যার tag-এ ঠিক এই nonce লেখা আছে — <script nonce="r4nd0mB4se64">. Nonce প্রতিটি response-এ সার্ভার নতুন করে এলোমেলোভাবে বানায়; আক্রমণকারী আগে থেকে জানে না, তাই তার ঢোকানো script-এ সঠিক nonce থাকে না। Nonce-এর বিকল্প হলো hash: 'sha256-…' লিখে একটি নির্দিষ্ট inline script-এর বিষয়বস্তুকে অনুমোদন দেওয়া — static পাতার জন্য সুবিধাজনক। object-src 'none' পুরোনো plugin বন্ধ করে; base-uri 'none' ঢোকানো <base> tag দিয়ে আপেক্ষিক script ঠিকানা ঘুরিয়ে দেওয়া আটকায়; frame-ancestors ঠিক করে কে আপনার পাতাকে frame-এ দেখাতে পারবে (অংশ ৭)। report-to নিয়ম ভাঙার খবর আপনার সার্ভারে পাঠায় (পুরোনো রূপ report-uri).

ধাপ ১সার্ভার header আর nonce পাঠাল
Content-Security-Policy: script-src 'self' 'nonce-Xy7q'; object-src 'none'
// এই response-এর জন্য নতুন nonce: Xy7q (পরের response-এ অন্যটি)
ধাপ ২নিজের script — চলে
<script src="/app.js"></script>              ← 'self' ✓
<script nonce="Xy7q">initCart();</script>  ← nonce মিলেছে ✓
ধাপ ৩ঢোকানো লেখা — থামে
রিভিউয়ের ভেতর থেকে: <img src=x onerror=alert(1)>
→ inline event handler, nonce নেই → চলবে না
Console (Edge/Chrome, সংক্ষেপে):
Refused to execute inline event handler because it violates
the following Content Security Policy directive: "script-src …"
ধাপ ৪খবর আপনার কাছে
ব্রাউজার report পাঠায় → আপনার /csp-report
{ "blockedURL": "inline", "effectiveDirective": "script-src-attr", … }
// এখন আপনি জানেন কোন পাতায় innerHTML-এর বাগ আছে — সেটি ঠিক করুন
  1. Header আর nonce. প্রতিটি পাতা পাঠানোর সময় সার্ভার একটি এলোমেলো nonce বানায় (অন্তত ১২৮ bit, base64), header-এ লেখে, আর একই মান নিজের inline script tag-এ বসায়। Nonce কখনো স্থির বা পুনর্ব্যবহৃত হবে না — তাহলে আক্রমণকারী সেটি জেনে নিতে পারে।
  2. নিজের কোড চলে। নিজের origin-এর ফাইল আর সঠিক nonce-ওয়ালা inline script স্বাভাবিকভাবে চলে। পাতার কোড যদি নতুন script লোড করে, CSP Level 3-এর 'strict-dynamic' সেই বিশ্বাস পরের script-এ পৌঁছে দিতে পারে।
  3. ঢোকানো কোড থামে। HTML-এ ঢুকে পড়া event attribute বা nonce-ছাড়া <script> চলে না; ব্রাউজার Console-এ এমন একটি বার্তা লেখে। এজন্যই পর্ব ৩৮-এর পরামর্শ: onclick="…" attribute নয়, addEventListener — কড়া CSP-তে inline attribute এমনিতেই ভেঙে যায়।
  4. রিপোর্ট। নিয়ম ভাঙলে ব্রাউজার JSON রিপোর্ট পাঠায়। এই রিপোর্ট দেখে আপনি XSS-এর বাগ খুঁজে পান — CSP তখন শুধু ঢাল নয়, একটি সতর্কঘণ্টাও।

পুরোনো সাইটে হঠাৎ কড়া CSP চালু করলে নিজের পাতাই ভেঙে যেতে পারে — কোথাও inline onclick, কোথাও তৃতীয় পক্ষের script. তাই শুরু করুন report-only মোডে: Content-Security-Policy-Report-Only header ব্রাউজারকে নিয়ম মানতে নয়, শুধু ভাঙার রিপোর্ট পাঠাতে বলে। কয়েক সপ্তাহ রিপোর্ট দেখে নিজের কোড ঠিক করুন, তারপর আসল header চালু করুন। <meta> tag দিয়েও CSP দেওয়া যায়, কিন্তু তাতে frame-ancestors আর report-only কাজ করে না।

CSP error সারাতে 'unsafe-inline' বা 'unsafe-eval' যোগ করা মানে দরজার তালা খুলে চাবি ঝুলিয়ে রাখা — inline কোড চলতে দিলে XSS-এর বিরুদ্ধে CSP-র মূল সুরক্ষাই চলে যায়। একইভাবে script-src https: বা বড় CDN-এর পুরো domain অনুমোদন করলে সেখানকার যেকোনো ফাইল চালানো যায়। Nonce বা hash-ভিত্তিক “strict CSP” এখন প্রস্তাবিত পথ (ভাবনার প্রশ্ন ৩)।

৭ · CSRF, clickjacking আর cookie-র নিরাপত্তা

CSRF-এ আক্রমণকারী আপনার পাতায় ঢোকে না — বাইরে থেকেই ব্যবহারকারীর নামে কাজ করিয়ে নেয়। মূল কারণ পর্ব ৪০-এর cookie-র স্বভাব: কোনো সাইটের cookie থাকলে ব্রাউজার সেই সাইটে যাওয়া অনুরোধে cookie জুড়ে দেয় — অনুরোধ কোন পাতা থেকে শুরু হলো, ঐতিহ্যগতভাবে তা না দেখেই। আর অংশ ২-এর টেবিলে দেখেছেন: অন্য সাইট ফর্ম পাঠাতে পারে, উত্তর পড়তে না পারলেও। একটি ব্যাংকের টাকা পাঠানোর ফর্ম দিয়ে পুরো ঘটনা আর প্রতিরক্ষা দেখুন:

ধাপ ১ক্রেতা ব্যাংকে লগইন
bank.com.bd → Set-Cookie: session=a1b2…; HttpOnly; Secure; SameSite=Lax
আসল ফর্মে লুকানো ঘর: <input type="hidden" name="csrf" value="9fK2…">
ধাপ ২অন্য ট্যাবে একটি অচেনা পাতা
“আপনি পুরস্কার জিতেছেন!” — prize.example
পাতার ভেতরে লুকানো একটি ফর্ম, গন্তব্য:
POST https://bank.com.bd/transfer  (to=…, amount=…)
// পাতাটি ব্যাংকের উত্তর পড়তে পারবে না — শুধু পাঠাতে পারে
ধাপ ৩ফর্ম নিজে থেকে জমা পড়ল
ব্রাউজার ভাবে: অনুরোধ prize.example থেকে শুরু, যাচ্ছে bank.com.bd-এ
→ এটি একটি cross-site POST
প্রশ্ন: session cookie কি সাথে যাবে?
ধাপ ৪SameSite=Lax-এর উত্তর
SameSite=Lax: cross-site POST-এ cookie যায় না
ব্যাংকের সার্ভার: “কে আপনি?” → 401 — টাকা গেল না
// Lax-এ শুধু ওপরের স্তরের GET navigation-এ (link চাপা) cookie যায়
ধাপ ৫দ্বিতীয় স্তর — token আর Origin
ধরুন cookie কোনোভাবে গেল (পুরোনো ব্রাউজার, বা same-site subdomain থেকে):
csrf ঘর নেই / ভুল → 403
Origin: https://prize.example ≠ https://bank.com.bd → 403
  1. লগইন আর প্রস্তুতি। ব্যাংক session cookie দেয় তিনটি flag-সহ (নিচে বিস্তারিত), আর প্রতিটি অবস্থা-বদলানো ফর্মে একটি অনুমান-অযোগ্য CSRF token বসায়, যা session-এর সাথে বাঁধা।
  2. অচেনা পাতা। ক্রেতা লগইন থাকা অবস্থায় অন্য ট্যাবে একটি অচেনা পাতা খুললেন। সেই পাতা ব্যাংকের কিছু পড়তে পারে না (same-origin policy), কিন্তু ব্যাংকের ঠিকানায় একটি ফর্ম পাঠাতে পারে।
  3. Cross-site POST. ফর্মটি নিজে থেকে জমা পড়ল। আগে, ব্রাউজার চোখ বুজে cookie জুড়ে দিত — আর সার্ভার ভাবত ক্রেতা নিজেই টাকা পাঠাচ্ছেন।
  4. SameSite=Lax থামায়। SameSite cookie-র বৈশিষ্ট্য: Strict হলে অন্য সাইট থেকে শুরু হওয়া কোনো অনুরোধে cookie যায় না; Lax হলে শুধু link চেপে আসার মতো ওপরের স্তরের GET-এ যায়, POST বা লুকানো অনুরোধে নয়; None হলে সবখানে যায় (তখন Secure বাধ্যতামূলক)। ২০২০ সাল থেকে Chrome (আর Edge) SameSite না লেখা cookie-কে Lax ধরে।
  5. দ্বিতীয় স্তর। SameSite একা যথেষ্ট নয়: সব ব্রাউজার একই ডিফল্ট মানে না, আর same-site subdomain (যেমন কোনো ঝুঁকিপূর্ণ blog.bank.com.bd) থেকে আসা অনুরোধ “same-site” গণ্য হয়। তাই সার্ভার token মেলায়, Origin (বা আধুনিক Sec-Fetch-Site) header দেখে, আর অচেনা উৎস হলে বাতিল করে।

আরও দুটি নিয়ম CSRF-কে সহজে আটকায়। কখনো GET দিয়ে অবস্থা বদলাবেন না — GET /cart/delete?id=7 থাকলে একটি <img src> দিয়েই কাজটি করিয়ে নেওয়া যায়, আর Lax cookie-ও GET navigation-এ যায় (পর্ব ৩৫-এর “GET safe” নিয়মটি এখানে নিরাপত্তার নিয়মও)। আর যে API শুধু Authorization header-এ পরিচয় নেয়, সেখানে CSRF-এর ঝুঁকি কম, কারণ ব্রাউজার এই header নিজে থেকে জোড়ে না।

Clickjacking — অদৃশ্য পাতার ওপর ক্লিক

Clickjacking-এ অন্য সাইট আপনার পাতাকে একটি স্বচ্ছ <iframe>-এ নিজের পাতার ওপর বসায়। ব্যবহারকারী দেখেন “ভিডিও চালান”, কিন্তু চাপেন নিচে লুকানো আপনার “অ্যাকাউন্ট মুছুন” বোতাম। প্রতিরক্ষা হলো ব্রাউজারকে বলে দেওয়া, কে আপনার পাতাকে frame-এ দেখাতে পারবে: CSP-র frame-ancestors 'none' (কেউ না) বা 'self' (শুধু নিজে), আর পুরোনো ব্রাউজারের জন্য X-Frame-Options: DENY বা SAMEORIGIN. দুটি থাকলে আধুনিক ব্রাউজার frame-ancestors মানে।

Cookie-র তিনটি flag, আর token কোথায় রাখবেন

পর্ব ৪০-এ cookie-র গঠন দেখেছেন; নিরাপত্তার দিক থেকে session cookie-তে তিনটি flag দরকার: HttpOnly (JavaScript-এর document.cookie পড়তে পারে না — XSS হলেও cookie চুরি হয় না), Secure (শুধু HTTPS-এ যায়), আর SameSite=Lax বা Strict (CSRF কমায়)।

লগইনের token কোথায় রাখবেন? localStorage-এ রাখা সহজ, আর CSRF-এর ঝুঁকি নেই (ব্রাউজার নিজে পাঠায় না)। কিন্তু একই origin-এর যেকোনো JavaScript সেটি পড়তে পারে — একটি XSS বা দূষিত npm package মানে token চুরি। HttpOnly cookie-তে রাখলে JavaScript token দেখতেই পায় না; তবে CSRF-এর প্রতিরক্ষা (SameSite + token) লাগে। তবে XSS হলে ঢোকানো কোড দুই ক্ষেত্রেই পাতা খোলা থাকা পর্যন্ত ব্যবহারকারীর হয়ে অনুরোধ পাঠাতে পারে — HttpOnly চুরি থামায়, সব ক্ষতি নয়। তাই অধিকাংশ ওয়েব অ্যাপের জন্য প্রস্তাবিত পথ: HttpOnly + Secure + SameSite session cookie, স্বল্পমেয়াদি session, আর আগে XSS ঠেকানো।

৮ · আপনার কোডের বাইরের ঝুঁকি — গোপন key, supply chain, HTTPS আর security header

গোপন key front-end-এ থাকে না

পর্ব ৩৫-এর নিয়মটি আবার: ব্রাউজারে পাঠানো সব কোড প্রকাশ্য। Minify করা bundle-ও পড়া যায়, আর .env ফাইলের মান build-এর সময় bundle-এ ঢুকে গেলে (যেমন Vite-এর VITE_ উপসর্গের মান) সেটিও প্রকাশ্য। পেমেন্টের secret, AI সেবার API key, ডেটাবেসের পাসওয়ার্ড — সব থাকবে সার্ভারে; ব্রাউজার ডাকবে আপনার নিজের API, আর সেটি key ব্যবহার করবে।

Supply chain — আপনি যার ওপর নির্ভর করেন

একটি সাধারণ React প্রকল্পে শত শত, কখনো হাজারের বেশি npm package থাকে (পর্ব ৪৬) — আর প্রতিটির কোড আপনার পাতায় আপনার origin-এর পূর্ণ ক্ষমতা নিয়ে চলে। আক্রমণকারী তাই প্রায়ই সরাসরি আপনার কোডে নয়, আপনার নির্ভরতায় হাত দেয় — একে বলে supply-chain attack. ২০১৮ সালের event-stream ঘটনা (ভাবনার প্রশ্ন ৫) এর পরিচিত উদাহরণ। ২০২৪ সালে পুরোনো ব্রাউজারের জন্য polyfill দেওয়া জনপ্রিয় CDN সেবা polyfill.io-র domain নতুন মালিকের হাতে যায়, এবং জুন মাসে দেখা যায় সেখান থেকে কিছু দর্শকের কাছে ক্ষতিকর কোড পাঠানো হচ্ছে, যা তাঁদের অন্য সাইটে পাঠিয়ে দিত।

প্রতিরক্ষার অভ্যাসগুলো সরল, কিন্তু নিয়মিত পালন করতে হয়:

  • Lockfile commit করুন (package-lock.json), আর CI-তে npm install নয়, npm ci — ঠিক একই version আর hash ইনস্টল হবে; কেউ নতুন version প্রকাশ করলেও আপনার build নিজে থেকে বদলাবে না।
  • npm audit নিয়মিত চালান (বা GitHub-এর Dependabot-এর মতো স্বয়ংক্রিয় ব্যবস্থা); নতুন package যোগের আগে দেখুন কে রক্ষণাবেক্ষণ করেন, কত পুরোনো, আসলেই দরকার কি না — ছোট কাজে নিজের ছোট function অনেক সময় নিরাপদ।
  • CDN থেকে script নিলে SRI দিন: <script src="https://cdn…/lib.min.js" integrity="sha384-…" crossorigin="anonymous">. ব্রাউজার ফাইলের hash মেলায়; এক byte বদলালেও ফাইলটি চলে না। SRI-র সীমা: যে সেবা ব্রাউজারভেদে আলাদা ফাইল পাঠায় (polyfill.io ঠিক এমন ছিল), সেখানে স্থির hash দেওয়াই যায় না — তখন ফাইলটি নিজের সার্ভারে রাখাই নিরাপদ।

আগের পর্বের দুটি ঝুঁকি, নিরাপত্তার চোখে

Prototype pollution (পর্ব ১৮ আর ২৫): বাইরের JSON-এর __proto__ key দিয়ে কোনো merge function Object.prototype-এ মান বসিয়ে দিলে পুরো অ্যাপের প্রতিটি object-এ সেই property দেখা দেয় — কখনো isAdmin-এর মতো যাচাই ভেঙে যায়। প্রতিরক্ষা: বাইরের key-এর জন্য Map বা Object.create(null), আর বিশ্বস্ত, হালনাগাদ merge library. ReDoS (পর্ব ৩০): দুর্বল regex-এ একটি লম্বা ইনপুট main thread বা Node সার্ভারকে মিনিটের পর মিনিট আটকে রাখতে পারে — ইনপুটের দৈর্ঘ্য সীমিত করুন আর nested quantifier এড়ান। আর eval — কখনো বাইরের ডেটায় নয়।

HTTPS সবখানে, আর HSTS

HTTPS ছাড়া উপরের সবকিছু অর্থহীন: পাবলিক Wi-Fi-তে পথের যে কেউ http পাতায় নিজের script ঢুকিয়ে দিতে বা cookie পড়ে নিতে পারে। Service worker-এর মতো অনেক আধুনিক Web API (পর্ব ৪৪) শুধু HTTPS-এ চলে। কিন্তু প্রথমবার কেউ shop.com.bd লিখলে ব্রাউজার http:// দিয়ে শুরু করতে পারে, আর সেই প্রথম অনুরোধটি পথে আটকানো যায়। HSTS header (Strict-Transport-Security: max-age=31536000; includeSubDomains) ব্রাউজারকে বলে: এক বছর ধরে এই সাইটে শুধু HTTPS.

Security header-এর চেকলিস্ট

Headerউদাহরণ মানকী ঠেকায়
Content-Security-Policyঅংশ ৬-এর strict CSPXSS-এর ঢোকানো কোড, clickjacking (frame-ancestors)
Strict-Transport-Securitymax-age=31536000; includeSubDomainshttp-তে নামিয়ে আনা, পথের আড়িপাতা
X-Content-Type-Optionsnosniffব্রাউজারের “অনুমান করে” ফাইলকে script ভাবা
Referrer-Policystrict-origin-when-cross-originURL-এর token বা ব্যক্তিগত path অন্য সাইটে ফাঁস
Permissions-Policycamera=(), geolocation=(self)তৃতীয় পক্ষের frame-এর ক্যামেরা, অবস্থান ইত্যাদিতে হাত
Set-Cookie flagHttpOnly; Secure; SameSite=Laxcookie চুরি, CSRF

এই security header-গুলো বসে সার্ভার, CDN বা hosting-এর কনফিগারেশনে (যেমন Apache-র .htaccess, Nginx, বা Express-এর helmet middleware — পর্ব ৫০)। পুরোনো পরামর্শে আরেকটি header দেখবেন — X-XSS-Protection. এটি ব্রাউজারের পুরোনো “XSS auditor” চালু করত, যা Chrome ২০১৯ সালে সরিয়ে দিয়েছে; আজ এর কোনো উপকার নেই, CSP-ই উত্তরসূরি।

OWASP Top 10 — front-end-এর চোখে

OWASP Top 10 হলো ওয়েব অ্যাপের প্রধান ঝুঁকির বহুল ব্যবহৃত তালিকা। এর ২০২১ সংস্করণের কয়েকটি বিভাগ এই পর্বের সাথে সরাসরি মেলে: Injection (XSS এখানে অন্তর্ভুক্ত) → অংশ ৪–৫; Broken Access Control → “client-কে বিশ্বাস নয়”, অনুমতি-যাচাই সার্ভারে, CORS ভুল কনফিগারেশন; Security Misconfiguration → CORS-এ *, দুর্বল CSP, header না থাকা; Vulnerable and Outdated Components আর Software and Data Integrity Failures → npm, CDN, SRI, lockfile; Cryptographic Failures → HTTPS, HSTS. পরের সংস্করণে software supply chain-এর ঝুঁকি আরও গুরুত্ব পেয়েছে; owasp.org-এ সর্বশেষ সংস্করণটি দেখে নিন।

C/C++/Python/Java জানলে

Java-য় একই প্রতিরক্ষা সার্ভারের দিক থেকে লেখা হয়: Spring Security ডিফল্টে CSRF token চায়, CORS-এর অনুমোদিত origin-এর তালিকা নেয় আর security header বসায় (Spring Security). XSS আর SQL injection একই রোগের দুই রূপ — ডেটা আর কোড এক string-এ মিশে যাওয়া; SQL-এ এর সমাধান prepared statement-এর placeholder (Python-এর SQLite পর্ব), HTML-এ textContent আর প্রসঙ্গ-ভিত্তিক encoding. C/C++-এর buffer overflow-ও আসলে “ডেটা কোডের জায়গা দখল করল” — সেখানে memory-র স্তরে, ওয়েবে লেখার স্তরে।

পরীক্ষা ও ইন্টারভিউতে
  • XSS আর CSRF-এর পার্থক্য? — XSS-এ আক্রমণকারীর কোড আপনার পাতার ভেতরে চলে; CSRF-এ অন্য সাইট ব্যবহারকারীর cookie-র জোরে আপনার সার্ভারে অনুরোধ পাঠায়, কোড আপনার পাতায় ঢোকে না।
  • CORS কি সার্ভারকে রক্ষা করে? — না; এটি ব্রাউজারের নিয়ম, যা অন্য সাইটের পাতাকে উত্তর পড়তে দেয় না। curl বা Node-এর জন্য প্রযোজ্য নয় — সার্ভারকে রক্ষা করে authentication আর authorization.
  • innerHTML আর textContent-এর পার্থক্য? — প্রথমটি লেখাকে HTML হিসেবে parse করে (XSS-এর ঝুঁকি), দ্বিতীয়টি শুধু লেখা হিসেবে বসায়।
  • Preflight কখন হয়? — cross-origin অনুরোধ “simple” না হলে: যেমন PUT/DELETE, application/json body বা Authorization header থাকলে; ব্রাউজার আগে OPTIONS পাঠায়।

৯ · AI-এর যুগে — AI-এর লেখা কোডে নিরাপত্তার ফাঁক

AI-কে “রিভিউ দেখানোর একটি অংশ বানাও” বললে কোড চমৎকার চলবে — পরীক্ষায় কেউ তো <img onerror> লেখেন না। নিরাপত্তার বাগ ডেমোতে দেখা যায় না; দেখা যায় যখন কেউ ইচ্ছা করে খোঁজেন। আর AI শিখেছে ইন্টারনেটের পুরোনো, অসাবধান কোড থেকেও। যে ভুলগুলো বারবার দেখা যায়:

AI যা প্রায়ই লেখেসমস্যাযা চাইবেন
list.innerHTML += `<li>${r.text}</li>`ব্যবহারকারীর লেখা HTML হয়ে যায় — stored XSScreateElement + textContent
CORS error-এ “Access-Control-Allow-Origin: * দিন” বা origin হুবহু ফেরত + credentialsযেকোনো সাইট ব্যবহারকারীর ডেটা পড়তে পারে, নয়তো cookie-সহ অনুরোধ এমনিতেই ভাঙেঅনুমোদিত origin-এর হুবহু তালিকা, Vary: Origin
CSP error-এ 'unsafe-inline' যোগCSP-র XSS-সুরক্ষা কার্যত শেষinline handler সরিয়ে addEventListener, দরকারে nonce
const OPENAI_KEY = "sk-…" ব্রাউজার-কোডেযে কেউ key নিয়ে ব্যবহার করে — বিল আপনারkey সার্ভারে, ব্রাউজার ডাকে নিজের API
token localStorage-এ, কোনো CSP নেইএকটি XSS = token চুরিHttpOnly + Secure + SameSite cookie, CSRF সুরক্ষা
X-XSS-Protection: 1; mode=block, regex দিয়ে “sanitize”পুরোনো বা অকার্যকর পরামর্শCSP, DOMPurify, প্রসঙ্গ-ভিত্তিক encoding

AI-কে কোড লিখতে বলার সময়ই নিরাপত্তার শর্ত দিন, আর শেষে তাকে দিয়েই নিজের কোডের নিরাপত্তা-পর্যালোচনা করিয়ে নিন:

আমি shop.com.bd নামে একটি অনলাইন দোকানের পণ্য-পাতার ব্রাউজার-কোড লিখছি (আধুনিক JavaScript, কোনো framework নয়)। API আছে api.shop.com.bd-তে, লগইন session চলে cookie দিয়ে। নিচের শর্ত মেনে রিভিউ দেখানো আর নতুন রিভিউ জমা দেওয়ার কোড লিখে দাও: ১. ব্যবহারকারীর কোনো লেখা innerHTML, insertAdjacentHTML বা template literal-এর HTML-এ বসাবে না; createElement আর textContent ব্যবহার করবে। ২. রিভিউকারীর "website" link বসানোর আগে new URL দিয়ে যাচাই করবে — শুধু http: আর https: গ্রহণ করবে, link-এ rel="noopener noreferrer nofollow" দেবে। ৩. কোনো inline event handler (onclick="…") নয়; সব addEventListener — কারণ সাইটে nonce-ভিত্তিক strict CSP আছে, 'unsafe-inline' দেওয়া যাবে না। ৪. fetch-এ credentials: "include"; POST-এ সার্ভারের দেওয়া CSRF token একটি header-এ পাঠাবে। কোনো API key বা গোপন তথ্য কোডে রাখবে না। ৫. সার্ভারের দিকের জন্য (Express) শুধু shop.com.bd-কে অনুমতি দেওয়া CORS কনফিগারেশন দেখাও — "*" বা origin হুবহু ফেরত দেওয়া নয়। শেষে নিজের কোডটি একজন নিরাপত্তা-পর্যালোচকের চোখে দেখে বলো: কোন লাইনে XSS, CSRF বা CORS-এর ঝুঁকি থাকতে পারত, আর কীভাবে এড়ানো হলো। নিশ্চিত না হলে "নিশ্চিত নই" বলো।

AI-এর লেখা কোড — নিরাপত্তা যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Same-origin policy-র জন্ম Netscape-এ, ১৯৯৫ সালের দিকে। ত্রিশ বছর পরেও ওয়েবের নিরাপত্তা কেন এই একটি ধারণার ওপর দাঁড়িয়ে — আর CORS কেন “ব্যতিক্রম” হিসেবে এল, নিয়ম বদলে নয়?

১৯৯৫–৯৬ সালে Netscape Navigator 2-তে JavaScript আসার সাথে সাথে ব্রাউজার একটি নতুন সমস্যায় পড়ল। আগে পাতা ছিল শুধু দেখার জিনিস; এখন পাতা কোড চালাতে, অন্য window খুলতে আর তার ভেতরের document-এ হাত দিতে পারে। কেউ একটি ট্যাবে নিজের ব্যাংক খুলে রেখেছেন, অন্য ট্যাবে একটি অচেনা সাইট — অচেনা সাইটের script কি ব্যাংকের পাতার লেখা পড়তে পারবে? Netscape-এর দল একটি সহজ নিয়ম বেছে নিল: script শুধু সেই document-এ হাত দিতে পারবে, যার origin তার নিজের পাতার সাথে মেলে।

নিয়মটি সময়ের সাথে প্রসারিত হলো — XMLHttpRequest, cookie-ভিত্তিক storage, localStorage, canvas-এর pixel পড়া — প্রতিটি নতুন ক্ষমতা একই প্রশ্নের মুখে পড়ল: অন্য origin-এর ডেটা পড়া যাবে কি? কিন্তু একটি ঐতিহাসিক আপস থেকে গেল: <img>, <script src> আর ফর্ম-জমা আগে থেকেই cross-origin চলত, আর পুরো ওয়েব সেগুলোর ওপর নির্ভর করত। সেগুলো বন্ধ করলে ওয়েব ভেঙে যেত। এই আপসের ফলেই CSRF সম্ভব (ফর্ম পাঠানো যায়), আর একসময় JSONP-এর মতো কৌশল জন্মায় — <script> দিয়ে অন্য সাইটের ডেটা আনা, যা আসলে অন্য সাইটকে আপনার পাতায় কোড চালানোর পূর্ণ অধিকার দিত।

২০০০-এর দশকে Ajax-নির্ভর অ্যাপ বাড়ার সাথে আলাদা domain-এর API ডাকার চাহিদা তীব্র হলো। নিয়মটি শিথিল করার বদলে মানদণ্ড-নির্মাতারা একটি ব্যতিক্রম-ব্যবস্থা বানালেন — CORS, যা ২০১৪ সালে W3C Recommendation হয় এবং পরে WHATWG-এর Fetch মানদণ্ডের অংশ হয়ে যায়। এর মূল নকশা-সিদ্ধান্ত হলো opt-in: ডিফল্ট “না”, আর অনুমতি দিতে পারে কেবল সেই সার্ভার, যার ডেটা। Preflight-এর অস্তিত্বও একই যুক্তির ফল: যে সার্ভার CORS-এর কথা কখনো শোনেনি, তাকে নতুন ধরনের অনুরোধ (PUT, JSON) দিয়ে অবাক করা যাবে না — আগে জিজ্ঞেস করতে হবে।

মূল উপলব্ধি: নিরাপদ ডিফল্ট আর স্পষ্ট opt-in ব্যতিক্রম — এই নকশা দশকের পর দশক টেকে। নিয়ম শিথিল করা সহজ, কিন্তু একবার শিথিল করলে আর ফেরানো যায় না; ব্যতিক্রম-ব্যবস্থা ডিফল্টকে অক্ষত রাখে।

প্র ০২২০০৫ সালে MySpace-এর “Samy worm” কুড়ি ঘণ্টারও কম সময়ে দশ লক্ষের বেশি প্রোফাইলে ছড়িয়েছিল। একটি “ছোট” XSS কীভাবে এত বড় হলো, আর এটি ওয়েব-নিরাপত্তার ভাবনাকে কী শেখাল?

২০০৫ সালে MySpace ছিল সবচেয়ে জনপ্রিয় সামাজিক যোগাযোগের সাইট। ব্যবহারকারীরা নিজের প্রোফাইল পাতা সাজাতে HTML আর CSS লিখতে পারতেন, তাই MySpace একটি ফিল্টার রেখেছিল যা বিপজ্জনক অংশ ছেঁটে ফেলার চেষ্টা করত — একটি blocklist. Samy Kamkar নামের এক তরুণ ডেভেলপার দেখলেন, ব্রাউজারগুলো HTML আর CSS যেভাবে পড়ে, ফিল্টার ঠিক সেভাবে পড়ে না; কয়েকটি অসংগতি জুড়ে তিনি নিজের প্রোফাইলে এমন কোড রাখতে পারলেন, যা ফিল্টার পেরিয়ে দর্শকের ব্রাউজারে চলত।

কোডটির কাজ ছিল প্রায় নিরীহ, কিন্তু স্ব-প্রতিলিপি-যোগ্য: যিনি Samy-র প্রোফাইল দেখতেন, তাঁর অ্যাকাউন্ট থেকে Samy-কে বন্ধুত্বের অনুরোধ যেত, আর তাঁর নিজের প্রোফাইলে একটি বাক্য — “but most of all, samy is my hero” — সাথে কোডটির একটি কপি বসে যেত। ২০০৫ সালের অক্টোবরে ছাড়ার পর প্রতি ঘণ্টায় সংখ্যা জ্যামিতিক হারে বাড়তে থাকে, কুড়ি ঘণ্টার কম সময়ে দশ লক্ষের বেশি প্রোফাইল আক্রান্ত হয়। MySpace সাইটটি সাময়িকভাবে বন্ধ করে পরিষ্কার করতে বাধ্য হয়। Samy পরে আইনি মামলার মুখোমুখি হন এবং দোষ স্বীকার করে প্রোবেশন আর কমিউনিটি সার্ভিসের শাস্তি পান — এটি মনে করিয়ে দেয় যে “শুধু মজা” বলে কোনো আক্রমণ আইনের চোখে মজা নয়।

এই ঘটনা কয়েকটি শিক্ষা স্থায়ীভাবে প্রতিষ্ঠা করে। প্রথমত, stored XSS শুধু একজনের সমস্যা নয় — এটি একটি worm হয়ে পুরো প্ল্যাটফর্মে ছড়াতে পারে, কারণ ঢোকানো কোড ব্যবহারকারীর নামে সব কাজ করতে পারে, এমনকি নিজেকে কপি করতেও। দ্বিতীয়ত, blocklist-ভিত্তিক ফিল্টার মৌলিকভাবে দুর্বল: ফিল্টার আর ব্রাউজার দুটি আলাদা parser, আর তাদের মধ্যে যেকোনো অমিলই একটি ফাঁক। এজন্যই আজকের DOMPurify ব্রাউজারের নিজের parser ব্যবহার করে আর allowlist মানে। এরপরের বছরগুলোতে HttpOnly cookie, CSP আর framework-এর স্বয়ংক্রিয় escape-এর মতো প্রতিরক্ষা দ্রুত জনপ্রিয় হয়।

মূল উপলব্ধি: XSS-এর ক্ষমতা হলো “ব্যবহারকারী যা করতে পারেন, ঢোকানো কোডও তা পারে”। আর “খারাপ জিনিস খুঁজে বাদ দাও” পদ্ধতি সবসময় একটি অমিলের কাছে হারে — “শুধু ভালো জিনিস রাখো” পদ্ধতি টেকে।

প্র ০৩CSP-র ধারণা এসেছিল Mozilla থেকে, ২০০০-এর দশকের শেষে। XSS তো কোড ঠিক করলেই সারে — তবু ব্রাউজারে একটি “নীতি”-ব্যবস্থার দরকার হলো কেন, আর প্রথম প্রজন্মের CSP কেন প্রায়ই ব্যর্থ হতো?

২০০০-এর দশকের মাঝামাঝি নিরাপত্তা-গবেষকেরা একটি হতাশাজনক সত্য মেনে নিচ্ছিলেন: বড় ওয়েবসাইটে হাজার হাজার জায়গায় ব্যবহারকারীর ডেটা দেখানো হয়, আর প্রতিটি জায়গায় নিখুঁতভাবে encode করা মানুষের পক্ষে প্রায় অসম্ভব। একটি ভুলই যথেষ্ট। প্রশ্ন উঠল: সাইটের মালিক কি ব্রাউজারকে আগে থেকে বলে দিতে পারেন, “আমার পাতায় শুধু এই উৎসগুলোর script চলবে” — যাতে কোথাও ভুল থেকে গেলেও ঢোকানো কোড না চলে? Mozilla-র Brandon Sterne এই ধারণাকে একটি নির্দিষ্ট প্রস্তাবে রূপ দেন, যা ২০০৯ সালের দিকে প্রকাশ্যে আলোচিত হয়। Firefox 4 (২০১১) প্রথম এটি X-Content-Security-Policy নামে চালু করে, অন্য ব্রাউজার নিজস্ব উপসর্গে।

W3C-তে কাজটি মানদণ্ডে রূপ নেয়: CSP 1.0, তারপর CSP Level 2 (২০১৬ সালে Recommendation), যাতে nonce আর hash যুক্ত হয়, আর এখনো চলমান Level 3, যা 'strict-dynamic' আর রিপোর্টিংয়ের নতুন ব্যবস্থা আনে। প্রথম প্রজন্মের নকশা ছিল allowlist of hosts: “script চলবে self, cdn.example.com আর analytics.example থেকে।” শুনতে যুক্তিসংগত, কিন্তু বাস্তবে দুটি সমস্যা দেখা দিল। এক, পুরোনো সাইটে inline script আর onclick এত বেশি ছিল যে প্রায় সবাই 'unsafe-inline' যোগ করত — আর তাতে XSS-এর বিরুদ্ধে মূল সুরক্ষাই চলে যেত। দুই, অনুমোদিত বড় CDN বা Google-এর মতো domain-এ এমন পুরোনো library বা JSONP endpoint থাকত, যা দিয়ে নীতি পাশ কাটানো যেত।

২০১৬ সালে Google-এর একদল গবেষক কোটি কোটি পাতার CSP বিশ্লেষণ করে দেখান, বাস্তবে ব্যবহৃত CSP-গুলোর বিশাল অংশ XSS ঠেকাতে কার্যত অকার্যকর। তাঁদের প্রস্তাব ছিল host-এর তালিকা ছেড়ে nonce-ভিত্তিক “strict CSP” আর 'strict-dynamic': কোন ঠিকানা থেকে script এল তা নয়, script-টি পাতার মালিকের দেওয়া গোপন চিহ্ন বহন করে কি না — সেটাই বিচার। আজ Google-সহ বড় প্রতিষ্ঠানগুলো এই ধাঁচ ব্যবহার করে, আর তার ওপর Trusted Types যোগ করে sink-এর স্তরেই string নিষিদ্ধ করে।

মূল উপলব্ধি: নিখুঁত মানুষের ওপর নির্ভর করা নিরাপত্তা টেকে না; ক্ষতি-সীমিত করার স্তর লাগে। কিন্তু সেই স্তর ব্যবহারযোগ্য না হলে মানুষ তা দুর্বল করে ফেলে — CSP-র প্রথম দশক তার প্রমাণ।

প্র ০৪২০২০ সালে Chrome 80 থেকে SameSite না লেখা cookie ডিফল্টে “Lax” ধরা শুরু হয়। একটি ডিফল্ট বদলে কীভাবে CSRF-এর মতো দুই দশকের পুরোনো সমস্যা অনেকটা কমে গেল — আর কেন CSRF token এখনো দরকারি?

CSRF-এর মূল কারণ cookie-র একটি পুরোনো স্বভাব: অনুরোধ কোন সাইট থেকে শুরু হলো, তা না দেখে গন্তব্য সাইটের cookie জুড়ে দেওয়া। ১৯৯০-এর দশকে cookie যখন বানানো হয়, তখন এটি সুবিধাজনক মনে হয়েছিল — অন্য সাইটের link থেকে এলেও আপনি লগইন অবস্থায় থাকবেন। দুই দশক ধরে এর মাশুল দিতে হয়েছে প্রতিটি ওয়েব-ডেভেলপারকে: প্রতিটি ফর্মে CSRF token, প্রতিটি framework-এ middleware, আর যেখানে কেউ ভুলে গেছেন, সেখানে বাগ।

২০১৬ সালের দিকে Google-এর Mike West প্রমুখের প্রস্তাবিত SameSite attribute সমস্যাটিকে মূল জায়গায় ধরল: সাইট নিজেই বলে দেবে, তার cookie অন্য সাইট থেকে শুরু হওয়া অনুরোধে যাবে কি না। Chrome 51-এ এটি আসে, কিন্তু opt-in হওয়ায় খুব কম সাইট ব্যবহার করত — ডিফল্ট সবসময় জেতে। তাই Chrome দল একটি সাহসী সিদ্ধান্ত নিল: SameSite না লেখা cookie-কে Lax ধরা, আর SameSite=None চাইলে Secure বাধ্যতামূলক। ২০২০ সালের ফেব্রুয়ারিতে Chrome 80-এ ধাপে ধাপে চালু শুরু হয়; মহামারির সময় অনেক জরুরি সাইট (সরকারি সেবা, ব্যাংক) ভেঙে যাওয়ার আশঙ্কায় কয়েক মাস স্থগিত রেখে পরে আবার চালু করা হয়।

ফল উল্লেখযোগ্য: পুরোনো ধাঁচের “লুকানো ফর্ম জমা” আক্রমণ Chromium-ভিত্তিক ব্রাউজারে ডিফল্টেই থেমে যায়। তবু CSRF token বাদ দেওয়ার সময় আসেনি। প্রথমত, সব ব্রাউজার একই ডিফল্ট গ্রহণ করেনি। দ্বিতীয়ত, Lax-এ ওপরের স্তরের GET navigation-এ cookie যায় — GET দিয়ে অবস্থা বদলানো অ্যাপ এখনো ঝুঁকিতে। তৃতীয়ত, “site” মানে registrable domain; blog.bank.com.bd-এর মতো একটি দুর্বল subdomain থেকে আসা অনুরোধ same-site, তাই SameSite কিছুই আটকায় না।

এজন্য আধুনিক পরামর্শ হলো স্তরে স্তরে: স্পষ্ট SameSite=Lax (বা সম্ভব হলে Strict), অবস্থা-বদলানো কাজ শুধু POST/PUT/DELETE-এ, সার্ভারে Origin বা Sec-Fetch-Site যাচাই, আর framework-এর CSRF token চালু রাখা।

মূল উপলব্ধি: নিরাপত্তায় ডিফল্টের ক্ষমতা বিশাল — লক্ষ লক্ষ ডেভেলপারকে শেখানোর চেয়ে একটি ডিফল্ট বদলানো বেশি কার্যকর। কিন্তু ডিফল্ট একটি স্তর মাত্র; প্রতিটি স্তরের সীমা জানা থাকলে তবেই নির্ভর করা যায়।

প্র ০৫২০১৬ সালে মাত্র এগারো লাইনের left-pad সরিয়ে নেওয়ায় হাজারো প্রকল্পের build ভেঙে গেল; ২০১৮ সালে event-stream-এ লুকানো কোড পাওয়া গেল। npm-এর ওপর আমরা যে বিশ্বাস রাখি, তার প্রকৃতি কী — আর একজন ডেভেলপার কী করতে পারেন?

২০১৬ সালের মার্চে একটি package-এর নাম নিয়ে বিরোধের পর একজন ডেভেলপার npm থেকে নিজের প্রায় তিনশো package সরিয়ে নেন। তার একটি ছিল left-pad — string-এর বাঁয়ে ফাঁকা বসানোর এগারো লাইনের একটি function. দেখা গেল, Babel আর React-এর মতো বিশাল প্রকল্প পরোক্ষভাবে এর ওপর নির্ভর করত; কয়েক ঘণ্টা ধরে পৃথিবীজুড়ে অসংখ্য build ব্যর্থ হলো। npm package-টি ফিরিয়ে আনে আর পরে নিয়ম বদলায়, যাতে অন্যরা নির্ভর করে এমন package ইচ্ছামতো সরানো না যায়। এটি ছিল প্রাপ্যতার (availability) শিক্ষা: আপনার build এমন মানুষের সিদ্ধান্তের ওপর নির্ভর করে, যাঁদের আপনি চেনেন না।

২০১৮ সালের event-stream ঘটনা ছিল আরও গভীর — বিশ্বাসের শিক্ষা। Package-টি সপ্তাহে প্রায় কুড়ি লক্ষবার ডাউনলোড হতো। এর মূল লেখক বহু বছর বিনা পারিশ্রমিকে রক্ষণাবেক্ষণ করে ক্লান্ত ছিলেন; এক অপরিচিত ব্যক্তি সাহায্যের প্রস্তাব দিলে তিনি প্রকাশের অধিকার দিয়ে দেন। নতুন রক্ষণাবেক্ষণকারী একটি ছোট dependency যোগ করেন, যার প্রকাশিত সংস্করণে এনক্রিপ্ট করা কোড ছিল; সেটি শুধু একটি নির্দিষ্ট bitcoin wallet অ্যাপের build-এর ভেতরে সক্রিয় হয়ে wallet-এর তথ্য চুরির চেষ্টা করত। কয়েক সপ্তাহ পর একজন ডেভেলপার একটি অদ্ভুত deprecation সতর্কবার্তা দেখে খোঁজ শুরু করলে বিষয়টি ধরা পড়ে।

আমরা আসলে কাকে বিশ্বাস করি? একটি npm install মানে শত শত অচেনা মানুষ, তাঁদের অ্যাকাউন্টের পাসওয়ার্ড, তাঁদের build যন্ত্র আর ভবিষ্যৎ সিদ্ধান্তকে বিশ্বাস করা — আর package-এর install script আপনার যন্ত্রে কোড চালাতেও পারে। পরের বছরগুলোতে বাস্তুতন্ত্র কিছু উত্তর দিয়েছে: lockfile আর npm ci (হঠাৎ নতুন version আসা বন্ধ), npm audit আর স্বয়ংক্রিয় সতর্কতা, জনপ্রিয় package-এর রক্ষণাবেক্ষণকারীদের জন্য দুই-ধাপের লগইন, আর package কোন source code ও build থেকে এসেছে তার যাচাইযোগ্য প্রমাণ (provenance). ব্রাউজারের দিকে SRI আর CSP একই প্রশ্নের উত্তর দেয়: “যা চালাচ্ছি, তা কি সেটাই যা আমি অনুমোদন করেছিলাম?”

মূল উপলব্ধি: প্রতিটি dependency একটি বিশ্বাসের সম্পর্ক, শুধু একটি কোডের টুকরো নয়। আধুনিক নিরাপত্তা সেই বিশ্বাসকে যাচাইযোগ্য করার দিকে যাচ্ছে — hash, lockfile, provenance.

অনুশীলন

বেশিরভাগ কাজ ভাবনার আর DevTools-এ দেখার — উত্তর আগে নিজে লিখুন, তারপর মিলিয়ে নিন।

  1. Same-origin কি না: পাতা https://shop.com.bd. নিচের প্রতিটি ঠিকানা same-origin, নাকি শুধু same-site, নাকি দুটোর কোনোটিই নয়? (ক) https://shop.com.bd/api/cart (খ) https://api.shop.com.bd (গ) http://shop.com.bd (ঘ) https://shop.com.bd:8443 (ঙ) https://shopbd.com

    (ক) same-origin — protocol, host, port একই; path গোনা হয় না। (খ) cross-origin কিন্তু same-site — host আলাদা, registrable domain একই (shop.com.bd). (গ) cross-origin; আধুনিক “schemeful” নিয়মে same-site-ও নয়, কারণ scheme আলাদা। (ঘ) cross-origin (port আলাদা), কিন্তু same-site — site-এর হিসাবে port গোনা হয় না। (ঙ) দুটোর কোনোটিই নয় — সম্পূর্ণ আলাদা domain. শিক্ষা: (খ) আর (ঘ)-তে CORS লাগবে, কিন্তু SameSite cookie তাদের অনুরোধ আটকাবে না।

  2. বাগ খুঁজুন: একটি সার্চ পাতার কোড: const p = new URLSearchParams(location.search);
    heading.innerHTML = "ফল: " + p.get("q");
    link.href = p.get("next");
    — কোথায় কী ঝুঁকি, আর ঠিক রূপ কী?

    (১) innerHTML-এ URL থেকে আসা লেখা — reflected/DOM-based XSS; কেউ একটি সাজানো link পাঠালে চাপামাত্র তার HTML পাতায় বসবে। ঠিক রূপ: heading.textContent = "ফল: " + p.get("q");. (২) link.href-এ যাচাইহীন URL — javascript: URL হলে চাপলে কোড চলবে, আর https://evil.example হলে ব্যবহারকারী আপনার সাইটের নামে অন্য সাইটে চলে যাবেন (open redirect, ফিশিংয়ে কাজে লাগে)। ঠিক রূপ: অংশ ৫-এর safeURL, আর “next”-এর মতো ক্ষেত্রে আরও কড়া — শুধু নিজের origin-এর পথ গ্রহণ (new URL(v, location.origin).origin === location.origin).

  3. CSP পড়ুন: একটি সাইটের header: Content-Security-Policy: default-src *; script-src 'self' 'unsafe-inline' https:. এই নীতি XSS-এর বিরুদ্ধে কতটা কার্যকর? কী কী বদলাবেন?

    প্রায় অকার্যকর। 'unsafe-inline' থাকায় ঢোকানো inline script আর onerror-এর মতো handler চলে — stored XSS পুরোপুরি কাজ করে। https: মানে পৃথিবীর যেকোনো HTTPS সার্ভারের script চলতে পারে, আক্রমণকারীর নিজের সার্ভারসহ। default-src * সব কিছু খোলা রাখে। বদলানো রূপ: default-src 'self'; script-src 'self' 'nonce-{প্রতিবার নতুন}'; object-src 'none'; base-uri 'none'; frame-ancestors 'self', আর পাতার inline onclick সরিয়ে addEventListener. চালু করার আগে কয়েক সপ্তাহ Content-Security-Policy-Report-Only দিয়ে রিপোর্ট দেখুন।

  4. DevTools অভিযান: আপনার পরিচিত দুটি বড় ওয়েবসাইট (একটি ব্যাংক বা মোবাইল ফিনান্স সেবা, একটি খবরের সাইট) খুলে F12 → Network → প্রথম document অনুরোধটি বেছে Response Headers দেখুন। কোন কোন security header আছে? তারপর Application → Cookies-এ গিয়ে session-জাতীয় cookie-র HttpOnly, Secure, SameSite কলাম দেখুন।

    সাধারণত দেখবেন strict-transport-security প্রায় সব বড় সাইটে আছে; x-content-type-options: nosniff আর x-frame-options বা CSP-র frame-ancestors অনেক সাইটে; পূর্ণ CSP তুলনামূলক কম, আর থাকলেও কখনো 'unsafe-inline'-সহ। আর্থিক সেবার session cookie-তে সাধারণত HttpOnly আর Secure টিক দেওয়া থাকে। SameSite কলাম খালি থাকলে Chrome সেটিকে Lax ধরে। কোনো সাইটের নিরাপত্তা-ত্রুটি সন্দেহ হলে নিজে পরীক্ষা না করে সাইটের দায়িত্বশীল প্রকাশ (responsible disclosure) ঠিকানায় জানান।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমshop.com.bd-এর পাতা api.shop.com.bd-তে JSON body-সহ একটি PUT fetch করলে যা যা ঘটে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ব্রাউজার দেখে অনুরোধটি simple নয়, তাই আগে অনুমতি চাইতে হবে
  2. ব্রাউজার Origin আর Access-Control-Request-Method-সহ একটি OPTIONS অনুরোধ পাঠায়
  3. সার্ভার Access-Control-Allow-Origin আর Allow-Methods header দিয়ে উত্তর দেয়
  4. ব্রাউজার আসল PUT অনুরোধ body-সহ পাঠায়
  5. উত্তরে অনুমতির header দেখে ব্রাউজার JavaScript-কে উত্তর পড়তে দেয়

ব্যবহারকারীর লেখা একটি রিভিউ পাতায় দেখানোর সবচেয়ে নিরাপদ উপায় কোনটি?

  • উপাদানের innerHTML-এ রিভিউটি বসানো
  • রিভিউ থেকে <script> শব্দটি মুছে innerHTML-এ বসানো
  • textContent দিয়ে রিভিউটি বসানো
  • রিভিউটি insertAdjacentHTML দিয়ে যোগ করা

একটি API ব্রাউজার থেকে আসা অনুরোধে CORS header দেয় না। Node.js-এর fetch দিয়ে একই API ডাকলে কী হবে?

  • Node-ও একই CORS error দিয়ে reject হবে
  • Node উত্তরটি স্বাভাবিকভাবে পড়তে পারবে
  • সার্ভার নিজে থেকে 403 ফেরত দেবে
  • Node আগে একটি preflight পাঠিয়ে অনুমতি না পেলে থেমে যাবে

Cookie-সহ cross-origin অনুরোধের (credentials: "include") জন্য সার্ভারের কোন CORS কনফিগারেশন সঠিক ও নিরাপদ?

  • Access-Control-Allow-Origin: * আর Allow-Credentials: true
  • অনুরোধের যেকোনো Origin হুবহু ফেরত দেওয়া, সাথে Allow-Credentials: true
  • Origin-এ shop.com.bd শব্দটি থাকলেই অনুমতি দেওয়া
  • অনুমোদিত তালিকায় হুবহু মিললে সেই origin ফেরত, সাথে Allow-Credentials: true আর Vary: Origin

CSP-তে script-src 'self' 'nonce-…' দেওয়া পাতায় HTML-এর ভেতরে ঢুকে পড়া <img src=x onerror=…>-এর কী হবে?

  • ছবির ঘর তৈরি হলেও onerror-এর কোড চলবে না, Console-এ CSP-র সতর্কবার্তা আসবে
  • onerror-এর কোড স্বাভাবিকভাবে চলবে, কারণ এটি script tag নয়
  • পুরো পাতা লোড হওয়া বন্ধ হয়ে যাবে
  • ব্রাউজার রিভিউটি সার্ভার থেকে মুছে দেবে

Session cookie-তে SameSite=Lax থাকলে অন্য সাইটের পাতা থেকে স্বয়ংক্রিয়ভাবে জমা পড়া একটি POST ফর্মের কী হয়?

  • Cookie যায়, কারণ ফর্ম জমা একটি সাধারণ navigation
  • ব্রাউজার ফর্মটি পাঠানোই বন্ধ করে দেয়
  • অনুরোধ যায়, কিন্তু session cookie ছাড়া — তাই সার্ভার ব্যবহারকারীকে চেনে না
  • Cookie যায় না, তাই এখন আর CSRF token-এর কোনো প্রয়োজন নেই

প্রোফাইলের “website” ঘরের মান <a href>-এ বসানোর আগে সবচেয়ে নির্ভরযোগ্য যাচাই কোনটি?

  • মানটি "javascript:" দিয়ে শুরু হলে বাতিল করা
  • new URL() দিয়ে parse করে protocol শুধু "http:" বা "https:" হলে গ্রহণ করা
  • মানটি HTML-escape করে বসানো
  • মানটি encodeURIComponent দিয়ে encode করে বসানো

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Same-origin policy অন্য origin-এর উত্তর ও DOM পড়া আটকায়, embed আর ফর্ম-পাঠানো নয়। CORS সার্ভারের দেওয়া opt-in অনুমতি — প্রয়োগ করে ব্রাউজার, ঠিক করতে হয় সার্ভারে।
  2. Simple নয় এমন অনুরোধে আগে OPTIONS preflight; cookie-সহ অনুরোধে * নয়, অনুমোদিত origin-এর হুবহু তালিকা।
  3. XSS ঠেকানোর মূল: ডেটা দেখান textContent দিয়ে, প্রসঙ্গ বুঝে encode, HTML লাগলে DOMPurify, URL-এ শুধু http:/https:; sink-গুলো চিনে রাখুন।
  4. CSP (nonce/hash, 'unsafe-inline' নয়) শেষ প্রতিরক্ষা-স্তর; শুরু করুন report-only দিয়ে। frame-ancestors clickjacking থামায়।
  5. CSRF-এর বিরুদ্ধে SameSite + CSRF token + Origin যাচাই, GET-এ অবস্থা বদল নয়; cookie-তে HttpOnly; Secure; secret সার্ভারে; lockfile, npm audit, SRI, HTTPS আর HSTS.

পরের পর্বে: মেট্রোরেলে সুড়ঙ্গে ঢুকলেন, নেটওয়ার্ক নেই — তবু আপনার অ্যাপ খুলছে, আগের ডেটা দেখাচ্ছে, আর সংযোগ ফিরলে জমে থাকা কাজ পাঠিয়ে দিচ্ছে। Service worker আর PWA — যেখানে এই পর্বের HTTPS-এর নিয়ম বাধ্যতামূলক, কারণ service worker প্রতিটি অনুরোধের মাঝখানে বসে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান