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

অ্যাপের নিরাপত্তা তালিকা — CORS, CSRF, XSS, header আর dependency

The App Security Checklist — Auditing and Hardening an Express and React Shop

Taught in Bangla, this lesson is a builder's audit of the Bazarghar shop against the OWASP Top 10:2025 with real runs: what JSX escapes and what it does not, DOMPurify and CSP with helmet, nonces and Report-Only, CSRF beyond SameSite with tokens and Fetch Metadata, CORS and security headers before and after, SSRF in an image-by-URL feature, secrets in the client bundle and SQL injection, dependencies, rate limits, logging and a pass-fail list.

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

  • OWASP Top 10:2025-এর প্রতিটি বিভাগকে নিজের অ্যাপের নির্দিষ্ট জায়গায় মেলানো
  • React-এ কোথায় escaping কাজ করে, কোথায় করে না, আর DOMPurify-এর ব্যবহার
  • helmet দিয়ে nonce-সহ CSP বসানো, browser console-এর বার্তা পড়া
  • SameSite, CSRF token আর Sec-Fetch-Site মিলিয়ে CSRF ঠেকানো
  • SSRF, bundle-এ secret, dependency আর rate limit যাচাই করা
  • একটি পাস-ফেল script আর এক পাতার threat model বানানো

১ · তালিকা ধরে অ্যাপের ভেতর দিয়ে হাঁটা

“বাজারঘর”-এর login, permission, order, সব তৈরি। শুক্রবার সকালে প্রথম বড় অফার। বৃহস্পতিবার রাতে একটিই প্রশ্ন: যা বানিয়েছি, তা কি নিরাপদ? “মনে হয়” এর উত্তর নয়। এখানে যাচাইয়ের মতো একটি তালিকা লাগে।

সদরঘাটে লঞ্চ ছাড়ার আগে মাস্টার মুখস্থের ওপর ভরসা করেন না। লাইফ জ্যাকেট কয়টি, ইঞ্জিন, যাত্রীর সংখ্যা, আবহাওয়ার সংকেত, প্রতিটি মিলিয়ে তবেই ছাড়েন। অভিজ্ঞ মানুষও ভুলে যান, তাই তালিকা। এই পর্বে আমরা বাজারঘরকে ঘাটে দাঁড় করিয়ে তালিকা মেলাব, আর প্রতিটি টিকের পেছনে একটি প্রমাণ রাখব।

তত্ত্ব এখানে আবার শেখানো হবে না। ঝুঁকির পুরো মানচিত্র আছে OWASP Top 10 পর্বে, browser-এর নিয়ম Web-এর নিরাপত্তা মডেল পর্বে। আমরা শুধু নির্মাতার কাজটি করব: নিজের কোডে খোঁজা, সারানো, আবার মাপা। সব run নিজের লোকাল ল্যাবে, নিজের অ্যাপের ওপর। অন্যের সিস্টেমে অনুমতি ছাড়া পরীক্ষা করা অন্যায় আর বেআইনি হতে পারে।

OWASP-এর সাইটে লেখার সময়ের সর্বশেষ তালিকাটি Top 10:2025. প্রতিটি বিভাগ বাজারঘরের কোথায় পড়ে, দেখে নিন:

বিভাগবাজারঘরে কোথায়কোথায় দেখবেন
A01 Broken Access Controlঅন্যের order, CSRF, SSRFপর্ব ৪৮, এখানে ৪ ও ৬ অংশ
A02 Security Misconfigurationheader, CORS, cookie flagএখানে ৩ ও ৫ অংশ
A03 Software Supply Chain Failuresnpm package, buildএখানে ৮ অংশ
A04 Cryptographic Failurespassword hash, TLSপর্ব ৪৫, HSTS এখানে ৫ অংশে
A05 InjectionXSS, SQL injectionএখানে ২ ও ৭ অংশ, পর্ব ৩৪, পর্ব ৩৮
A06 Insecure Designকুপন, stock, ব্যবসার নিয়মএখানে ৯ অংশের threat model
A07 Authentication Failureslogin, rate limit, কোডে বসানো secretপর্ব ৪৫–৪৯, এখানে ৭ ও ৯ অংশ
A08 Software or Data Integrity Failureslockfile, webhook-এর যাচাইএখানে ৮ অংশ, পর্ব ৬৫
A09 Security Logging and Alerting Failuresনিরাপত্তার ঘটনার logপর্ব ৩১, এখানে ৯ অংশ
A10 Mishandling of Exceptional Conditionsভুল ইনপুটে stack traceপর্ব ২৮, এখানে ৯ অংশ

বাজারঘরে কোন যাচাই কোন জায়গায়

Where each check sits in the app

বাজারঘরে নিরাপত্তা যাচাইয়ের অবস্থান ক্রেতার browser · React JSX escaping · DOMPurify CSP · SameSite cookie trust boundary Express API ১ helmet · ২ CORS ৩ rate limit · ৪ login ৫ CSRF · ৬ অনুমতি · ৭ যাচাই ৮ log · ৯ নিরাপদ error Database parameterized query বাইরের ছবির URL SSRF: allowlist, ঠিকানা যাচাই নিচের ভিত: npm package আর build lockfile · npm ci · npm audit · grep
ছবি ৫০.১ — প্রতিটি স্তরে আলাদা যাচাই। একটি ভাঙলে পরেরটি ধরবে, এই আশাতেই এত স্তর।

ল্যাবের অ্যাপ একটিই, দুটি রূপে। MODE=before-এ সাধারণ তাড়াহুড়োর কোড; MODE=after-এ শক্ত করা। এতে প্রতিটি সারানোর প্রভাব সরাসরি মাপা যায়। লেখার সময়ের version: Node 24.15.0, Express 5.3.0, React 19.3.1, Vite 8.3.5, helmet 8.3.0, cors 2.8.6, csrf-csrf 4.1.0, DOMPurify 3.4.17, express-rate-limit 8.7.2, pino 10.4.0, npm 11.12.1, Edge 156 (Playwright দিয়ে চালানো)। এগুলো বদলায়, তাই নিজের package.json মিলিয়ে নেবেন।

২ · React-এ XSS — JSX কী ঢাকে, কী ঢাকে না

পণ্যের নিচে ক্রেতার মন্তব্য, এই একটি ফিচারেই stored XSS-এর ঝুঁকি। XSS কীভাবে কাজ করে, তা XSS প্রতিরক্ষা পর্বে আছে। JSX-এর escaping-এর ভিত্তি দেখেছেন পর্ব ৮-এ। এখন নিরীক্ষকের চোখে দেখা যাক, একটি মন্তব্য database থেকে পর্দায় আসার পথে কোথায় কী ঘটে। নিচের মানগুলো ল্যাবের আসল run-এর। মন্তব্যের ভেতরের কোড ক্ষতিহীন: এটি শুধু পাতার শিরোনাম বদলানোর চেষ্টা করে।

  1. POST/api/commentsকাঁচা লেখা
  2. Databasecommentsলেখা হিসেবেই
  3. JSONGETএখনো ডেটা
  4. JSX{c.text}text node
request bodyঅবিশ্বস্ত ইনপুট
{"text":"<img src=x onerror=\"…\"><b>ভালো চাল</b>"}
commentsকিছু বদলানো হয়নি
id │ text
 1 │ <img src=x onerror="…"><b>ভালো চাল</b>
responseapplication/json
[{"id":1,"text":"<img src=x onerror=\"…\">…"}]
browser এটিকে HTML হিসেবে পড়ে না
Edge-এ DOMview=safe
<p id="c">&lt;img src=x onerror="…"&gt;&lt;b&gt;ভালো চাল&lt;/b&gt;</p>
banner: বাজারঘর
Edge-এ DOMview=raw
<p id="c"><img src="x" onerror="…"><b>ভালো চাল</b></p>
banner: CHANGED
  1. ইনপুট আসে। ক্রেতা একটি মন্তব্য পাঠালেন, যার ভেতরে HTML. Server এখানে এটিকে শুধু লেখা হিসেবে নেয়, আর দৈর্ঘ্য সীমিত করে।
  2. Database-এ লেখা হিসেবে। কাঁচা লেখাই জমা (ল্যাবে memory-র একটি array, বাস্তবে table). এটি ক্ষতিকর নয়, কারণ database HTML চালায় না। Escape করা হয় দেখানোর সময়, জমা করার সময় নয়।
  3. API JSON দেয়। Content-Type JSON, তাই browser একে পাতা হিসেবে পড়ে না। এই ধাপ পর্যন্ত এটি শুধুই ডেটা।
  4. JSX text node বানায়। {c.text}-এ React লেখাটি DOM-এর text node-এ বসায়। < অক্ষরটি অক্ষরই থাকে, শিরোনাম অপরিবর্তিত।
  5. একই ডেটা, ভুল পথে। dangerouslySetInnerHTML সেই লেখাকে HTML হিসেবে পড়ল। img তৈরি হলো, onerror চলল, শিরোনাম হলো CHANGED. ডেটা একই, শুধু দেখানোর পথ আলাদা।

নিজের চোখে দেখুন। নিচের ব্লকে আসল React চলছে। একই লেখা দুই পথে দেখানো হলো। লেখাটি বদলে পরীক্ষা করতে পারেন।

<div id="root"></div>
<p id="status">অবস্থা: কিছু বদলায়নি</p>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
// ক্ষতিহীন নমুনা: এটি শুধু নিচের লেখাটি বদলানোর চেষ্টা করে
const change = "document.getElementById('status').textContent='অবস্থা: লেখা বদলে গেছে'";
const comment = `<b>ভালো চাল</b><img src="data:,x" onerror="${change}">`;

function Shop() {
  useEffect(() => {
    const count = (id) => document.getElementById(id).children.length;
    console.log("JSX child elements:", count("safe"));
    console.log("dangerouslySetInnerHTML child elements:", count("raw"));
    setTimeout(() => console.log(document.getElementById("status").textContent), 60);
  }, []);
  return (
    <>
      <h4>JSX: {"{comment}"}</h4>
      <p id="safe">{comment}</p>
      <h4>dangerouslySetInnerHTML</h4>
      <p id="raw" dangerouslySetInnerHTML={{ __html: comment }} />
    </>
  );
}
createRoot(document.getElementById('root')).render(<Shop />);
</script>

প্রথম সারিতে পুরো লেখা অক্ষর হিসেবে দেখা যায়; console বলে JSX child elements: 0. দ্বিতীয় সারিতে dangerouslySetInnerHTML child elements: 2, অর্থাৎ b আর img আসল element হয়েছে। শেষ লাইন অবস্থা: লেখা বদলে গেছে দেখায়, পাতার ভেতরে কোড চলেছে। লেখা বদলানো ক্ষতিহীন, কিন্তু একই পথে চলা অন্য কোড এই পাতার নামে প্রায় সবই করতে পারত।

HTML সত্যিই দেখাতে হলে: DOMPurify

ধরুন বিক্রেতা পণ্যের বিবরণে <b> ব্যবহার করতে পারবেন। তখন নিজে regex লিখবেন না। DOMPurify browser-এর নিজের parser দিয়ে HTML ভাঙে আর শুধু অনুমোদিত অংশ রাখে। Cure53 দলের এই library বহুল ব্যবহৃত। সংক্ষিপ্ত allowlist দিন:

import DOMPurify from 'dompurify';

const PURIFY = { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'], ALLOWED_ATTR: [] };

function RichText({ html }) {
  return <p dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html, PURIFY) }} />;
}

সত্যিকারের run (Edge 156, DOMPurify 3.4.17, একই ডেটা, তিনটি পথ):

== safe
  banner: বাজারঘর
  html: <p id="c">&lt;img src=x onerror="…"&gt;&lt;b&gt;ভালো চাল&lt;/b&gt;</p>
== raw
  banner: CHANGED
  html: <p id="c"><img src="x" onerror="…"><b>ভালো চাল</b></p>
== clean
  banner: বাজারঘর
  html: <p id="c"><b>ভালো চাল</b></p>

DOMPurify img ফেলে দিয়েছে, <b> রেখেছে। এটিই sanitize: বৈধ অংশ থাকে, বাকিটা যায়।

href="javascript:" — React 19 কী করে

দোকানের ওয়েবসাইট ঘরে কেউ javascript: দিয়ে শুরু হওয়া লেখা দিতে পারেন। JSX-এর escaping এখানে কাজ করে না, কারণ মানটি বৈধ attribute. Edge-এ আসল ফল:

== link  (<a href={c.website}>, React 19.3.1)
  href attr: javascript:throw new Error('React has blocked a javascript: URL as a security precaution.')
  click করার পর:
  pageerror: React has blocked a javascript: URL as a security precaution.
  banner: বাজারঘর
== safelink  (<a href={safeHref(c.website)}>)
  href attr: None
  banner: বাজারঘর

React 19 নিজেই মানটি বদলে দিল। পুরোনো version-এ এই সুরক্ষা ছিল না, শুধু development warning ছিল। তবু নিজের যাচাই রাখুন, কারণ location.href = url বা window.open(url)-এ React হাত দেয় না। আর allowlist সবচেয়ে পরিষ্কার পথ:

export function safeHref(raw) {
  try {
    const u = new URL(raw, location.origin);
    return ['http:', 'https:'].includes(u.protocol) ? u.href : undefined;
  } catch { return undefined; }
}

নিরীক্ষার কাজ: কোডে এই শব্দগুলো খুঁজুন: dangerouslySetInnerHTML, innerHTML, href={, location.href, eval(. প্রতিটির ক্ষেত্রে জিজ্ঞেস করুন, মান কোথা থেকে আসে? ব্যবহারকারী থেকে আসতে পারলে যাচাই বা sanitize আছে কি? XSS পর্বের context-এর নিয়ম এখানেই কাজে লাগে।

৩ · CSP — helmet, nonce আর Report-Only

ধরে নিন, সব মিলিয়েও একটি ভুল থেকে গেল। Content Security Policy হলো দ্বিতীয় দেয়াল: কোন script চলতে পারবে, সেটি browser-কে আগেই বলে দেওয়া। helmet এই header বসায়। Vite-এর build-এ script আলাদা ফাইলে, তাই 'self' অনেকটা কাজ সারে। কিন্তু পাতায় ছোট inline script থাকলে দরকার nonce, প্রতি response-এ নতুন একটি এলোমেলো মান।

app.use((req, res, next) => {
  res.locals.cspNonce = randomBytes(16).toString('base64');
  next();
});

app.use(helmet({
  contentSecurityPolicy: {
    reportOnly: MODE === 'reportonly',
    directives: {
      scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
      styleSrc: ["'self'"],
      imgSrc: ["'self'", 'data:'],
      connectSrc: ["'self'"],
      reportUri: ['/api/csp-report'],
    },
  },
}));

// নিজের static template-এর জানা জায়গায় nonce বসানো
app.get('/', (req, res) =>
  res.type('html').send(html.replaceAll('__CSP_NONCE__', res.locals.cspNonce)));

helmet-এর docs অনুযায়ী আপনার directives ডিফল্টের সাথে মেশে। তাই script-src-attr 'none', object-src 'none', base-uri 'self' নিজে থেকেই থাকে। আমরা ডিফল্টের ঢিলা style-src-ও কড়া করেছি। ল্যাব HTTP-তে চলে বলে upgrade-insecure-requests বন্ধ রাখা হয়েছিল; production-এ ডিফল্ট রাখুন। নিচে helmet ছাড়াই nonce-এর মূল কৌশলটি দেখুন।

const http = require("node:http");
const { randomBytes } = require("node:crypto");

const cspHeader = (nonce) => [
  "default-src 'self'",
  `script-src 'self' 'nonce-${nonce}'`,
  "script-src-attr 'none'",
  "object-src 'none'",
  "base-uri 'self'",
].join("; ");

const server = http.createServer((req, res) => {
  const nonce = randomBytes(16).toString("base64"); // প্রতি response-এ নতুন
  res.setHeader("Content-Security-Policy", cspHeader(nonce));
  res.end(`<script nonce="${nonce}">startShop()</script>`);
});

server.listen(0, "127.0.0.1", async () => {
  const url = `http://127.0.0.1:${server.address().port}/`;
  const seen = [];
  for (let i = 1; i <= 2; i++) {
    const r = await fetch(url);
    const header = r.headers.get("content-security-policy");
    const inHeader = header.match(/nonce-([^']+)/)[1];
    const inHtml = (await r.text()).match(/nonce="([^"]+)"/)[1];
    seen.push(inHeader);
    console.log(`#${i} match: ${inHeader === inHtml}, length: ${inHeader.length}`);
    if (i === 1) console.log(header.replace(inHeader, "<nonce>"));
  }
  console.log("same nonce twice:", seen[0] === seen[1]);
  server.close();
});

#1 match: true, length: 24: header আর HTML-এর nonce একই, আর ১৬ byte Base64-এ ২৪ অক্ষর, অর্থাৎ ১৬ × ৮ = ১২৮ bit. তারপর পুরো header ছাপা হয়েছে। শেষ লাইন same nonce twice: false: দুটি response-এ আলাদা মান। ল্যাবের আসল server-এও তাই: পরপর দুই curl-এ nonce-dQyWWb/GB3N+S8xRW/FYyA== আর nonce-BAC+WkdXu1H6H450SaKl1g==. Cache করা পাতায় nonce পুরোনো হয়ে যায়, তাই এমন HTML পাতা shared cache-এ রাখবেন না।

এবার Edge-এ একই মন্তব্য, কাঁচা পথে (view=raw), কিন্তু CSP চালু। ইচ্ছা করেই ভুল কোড রাখা হয়েছে, দেয়াল পরীক্ষার জন্য। সত্যিকারের console (লম্বা অংশ ছোট করা):

MODE=after, enforce
  console: log nonce script ran
  console: error Executing inline event handler violates the following Content Security Policy directive 'script-src-attr 'none''. … The action has been blocked.
  banner: বাজারঘর

MODE=reportonly
  console: log nonce script ran
  console: info Executing inline event handler violates the following Content Security Policy directive 'script-src-attr 'none''. … The policy is report-only, so the violation has been logged but no further action has been taken.
  banner: CHANGED

তিনটি জিনিস লক্ষ করুন। nonce script ran: বৈধ inline script চলেছে। DOM-এ img ঢুকেছিল, অর্থাৎ React-এর ভুলটি রয়ে গেছে; CSP শুধু কোড চলা থামিয়েছে। আর Report-Only-এ কিছুই থামে না, শিরোনাম বদলে গেছে, শুধু খবর আসে। তাই এটি মাপার যন্ত্র, সুরক্ষা নয়। Rollout-এর কৌশল আছে XSS পর্বের ৮ নম্বর অংশে।

ব্যস্ত দিনে এমন একটি বার্তা এলে কীভাবে পড়বেন? নিচের ধাপে ল্যাবের আসল বার্তা আর আসল report.

  1. Consoleviolatesকী ঘটল
  2. Reportcsp-reportserver-এ খবর
  3. উৎসsource-fileকোথা থেকে
  4. সিদ্ধান্তকোড বদলানpolicy নয়
Edge consoleerror
Executing inline event handler violates the
following Content Security Policy directive
'script-src-attr 'none''. …
The action has been blocked.
POST /api/csp-reportআসল body, ছোট করা
{"csp-report":{
 "document-uri":"http://localhost:47510/?view=raw",
 "violated-directive":"script-src-attr",
 "disposition":"report",
 "blocked-uri":"inline", …}}
একই report-এর শেষ অংশকোডের ঠিকানা
"source-file":"http://localhost:47510/assets/
  index-vowwMYDM.js",
"line-number":9, "column-number":1792
ভুল সমাধান বনাম ঠিক সমাধানকোডে
script-src-attr 'unsafe-inline'   ✗ দেয়াল ভাঙা
dangerouslySetInnerHTML + DOMPurify  ✓
অথবা শুধু {c.text}                ✓
  1. তিনটি শব্দ খুঁজুন। কী হয়েছিল (inline event handler), কোন নিয়ম ভেঙেছে (script-src-attr 'none'), আর ফল কী (blocked). Report-only হলে শেষ শব্দটি আলাদা হতো।
  2. Server-এর খবর। report-uri-তে আসা JSON. blocked-uri: inline মানে পাতার ভেতরের লেখা কোড; কোনো URL হলে সেখানে ঠিকানা থাকত। disposition: report মানে এই run ছিল Report-Only.
  3. কোন কোড? source-file বলছে, ঘটনাটি ঘটার সময় আমাদের bundle চলছিল। অর্থাৎ React যখন HTML বসাচ্ছিল, তখনই। এখান থেকেই খোঁজ যায় dangerouslySetInnerHTML-এ।
  4. Policy নয়, কোড বদলান। বার্তা থামাতে 'unsafe-inline' যোগ করলে দেয়ালই সরে যায়। আসল সমাধান: JSX-এর {c.text}, অথবা DOMPurify. Report-এর পরিমাণ হঠাৎ বাড়লে সেটিও খেয়াল করুন; extension-এর শব্দও আসে, তাই সব report ঘটনা নয়।

৪ · CSRF — SameSite, token আর Fetch Metadata

Cookie-ভিত্তিক login (পর্ব ৪৫) থাকলে browser নিজে থেকে cookie পাঠায়। CSRF-এর মূল সমস্যা এটিই; তত্ত্ব আছে CSRF আর cookie সুরক্ষা পর্বে। প্রশ্ন একটিই: SameSite=Lax দিলেই কি হয়ে গেল? ল্যাবে মেপে দেখি। বাজারঘরে login করা একটি Edge-এ, অন্য একটি লোকাল পাতার সাধারণ HTML form নিজের ল্যাবের wishlist-এ POST করল। ল্যাবে 127.0.0.1 থেকে localhost হলো অন্য site, আর localhost-এর অন্য port হলো একই site, আলাদা origin; যেমন blog.bazarghar.test আর shop.bazarghar.test.

Server-এর অবস্থাঅন্য site থেকে POSTএকই site, অন্য origin থেকে POST
Cookie-তে SameSite লেখা নেই, login-এর ঠিক পরেযোগ হলো (cookie গেছে)যোগ হলো
SameSite=Lax, token নেই401: cookie যায়নিযোগ হলো (cookie গেছে)
Lax + CSRF token403 invalid_csrf_token403 invalid_csrf_token
Lax + Fetch Metadata + token403 cross_site_blocked403 cross_site_blocked

সত্যিকারের run-এর log থেকে দ্বিতীয় সারি (Edge 156):

{"level":"info","event":"form_post","cookieSent":false,"site":"cross-site","msg":"form post"}
{"level":"info","event":"form_post","cookieSent":true,"site":"same-site","msg":"form post"}

দুটি আসল শিক্ষা। এক: Lax আলাদা origin-কে আটকায় না, শুধু আলাদা site-কে। একটি subdomain দখল হলে, বা সেখানে কারও লেখা HTML চললে, তা একই site. দুই: প্রথম সারিটি অবাক করার মতো। SameSite না লিখলে Chromium ধরে নেয় Lax, তবু login-এর ঠিক পরে অন্য site-এর POST-এ cookie গেল। আমরা ১৩০ সেকেন্ড অপেক্ষা করে আবার চালালাম; এবার cookieSent:false. Chromium-এর FAQ-তে একে ২ মিনিটের সাময়িক ছাড় (Lax+POST) বলা হয়েছে। তাই ডিফল্টের ওপর ভরসা না করে SameSite নিজে লিখুন।

OWASP-এর CSRF cheat sheet-ও বলে, SameSite defense in depth হিসেবে ভালো, তবে বেশিরভাগ deployment-এ আসল প্রতিরক্ষার বদলি নয়। তাহলে CSRF token কখন বাদ দেওয়া যায়? শুধু Authorization: Bearer header-এ login চললে, cookie নেই বলে browser কিছু নিজে পাঠায় না, তাই CSRF প্রযোজ্য নয়। Cookie থাকলেই token বা Fetch Metadata রাখুন।

Token-এর কৌশল: server HMAC দিয়ে session-এর সঙ্গে বাঁধা একটি মান বানায়। পাতার JavaScript সেটি header-এ পাঠায়, যা অন্য site পড়তে বা বসাতে পারে না। node:http-এ পুরো কৌশলটি:

const http = require("node:http");
const { createHmac, timingSafeEqual } = require("node:crypto");
const SECRET = "example-secret-not-real";
const sign = (sid, random) =>
  createHmac("sha256", SECRET).update(`${sid}!${random}`).digest("hex");
const makeToken = (sid, random) => `${sign(sid, random)}.${random}`;
function validToken(sid, token = "") {
  const [tag, random] = token.split(".");
  if (!tag || !random) return false;
  const a = Buffer.from(tag), b = Buffer.from(sign(sid, random));
  return a.length === b.length && timingSafeEqual(a, b);
}

const server = http.createServer((req, res) => {
  const sid = (req.headers.cookie ?? "").replace("sid=", "");
  const ok = validToken(sid, req.headers["x-csrf-token"]);
  res.statusCode = ok ? 201 : 403;
  res.end(ok ? "added" : "invalid csrf token");
});

server.listen(0, "127.0.0.1", async () => {
  const url = `http://127.0.0.1:${server.address().port}/wishlist`;
  async function send(label, sid, token) {
    const headers = { cookie: `sid=${sid}`, ...(token && { "x-csrf-token": token }) };
    const r = await fetch(url, { method: "POST", headers });
    console.log(label.padEnd(24), r.status, await r.text());
  }
  const mine = makeToken("sess-rahim", "r4nd0m");
  console.log("token:", mine.slice(0, 16) + "..." + mine.slice(-7));
  await send("no token", "sess-rahim");
  await send("wrong token", "sess-rahim", "abc.r4nd0m");
  await send("token of other session", "sess-karim", mine);
  await send("own token", "sess-rahim", mine);
  server.close();
});

আউটপুট: প্রথম লাইনে token: 97f0a2b6c2b3ea48....r4nd0m, তারপর no token 403, wrong token 403, token of other session 403, আর শেষে own token 201 added. তৃতীয় সারিটিই session-এ বাঁধার কারণ: করিমের cookie-র সাথে রহিমের সঠিক token চলে না। timingSafeEqual তুলনার সময় থেকে তথ্য ফাঁস হওয়া ঠেকায়।

  1. Cookiesid=sess-rahimsession
  2. Headerx-csrf-tokentag.random
  3. HMACsign(sid, random)আবার হিসাব
  4. তুলনাtimingSafeEqual201 বা 403
requestown token
POST /wishlist
cookie: sid=sess-rahim
x-csrf-token: 97f0a2b6c2b3ea48….r4nd0m
token ভাগ করাsplit(".")
tag    = 97f0a2b6c2b3ea48…
random = r4nd0m
server-এর হিসাবSECRET শুধু server জানে
HMAC(SECRET, "sess-rahim!r4nd0m")
= 97f0a2b6c2b3ea48…
ফলown token
tag === হিসাব → 201 added
একই token, অন্য sessiontoken of other session
cookie: sid=sess-karim
HMAC(SECRET, "sess-karim!r4nd0m") ≠ 97f0a2b6c2b3ea48…
→ 403 invalid csrf token
  1. Request এলো। Browser cookie নিজে পাঠিয়েছে, কিন্তু x-csrf-token header শুধু আমাদের পাতার কোড বসাতে পারে।
  2. Token ভাগ। বিন্দুর আগে সই, পরে এলোমেলো অংশ। কোনোটি না থাকলে সঙ্গে সঙ্গে false, যেমন no token-এর ক্ষেত্রে।
  3. Server আবার সই করে। Cookie-র session আর token-এর random মিলিয়ে নিজের গোপন key দিয়ে HMAC. Token server-এ জমা রাখতে হয়নি।
  4. মিলে গেলে কাজ। দুই byte-সারি সমান, তাই status 201, wishlist-এ পণ্য যোগ।
  5. অন্য session. Token বৈধ দেখালেও করিমের cookie দিয়ে হিসাব অন্য মান দেয়। 403. Token একজনের জন্যই বৈধ।

বাস্তবে: csrf-csrf আর Fetch Metadata

পুরোনো csurf package এখন archived, npm-এ লেখা আছে এটি আর রক্ষণাবেক্ষণ হয় না। csrf-csrf তার একটি পরিচিত বিকল্প, যা signed double-submit cookie করে (লেখার সময় 4.1.0). README বলে, পুরো session-নির্ভর app-এ লেখকের csrf-sync (synchronizer token) বেশি মানায়; JWT বা stateless হলে এটি। হাতে লেখা কোড শেখার জন্য ভালো, production-এ পরীক্ষিত library নিন।

import { doubleCsrf } from 'csrf-csrf';

const { doubleCsrfProtection, generateCsrfToken } = doubleCsrf({
  getSecret: () => config.CSRF_SECRET,
  getSessionIdentifier: (req) => req.sessionId ?? '',
  cookieName: '__Host-bazarghar.csrf',
  cookieOptions: { sameSite: 'lax', secure: true, path: '/' },
});

function fetchMetadataGuard(req, res, next) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
  const site = req.get('sec-fetch-site');
  const origin = req.get('origin');
  // header না এলে Origin-এ ফেরা (পুরোনো browser, curl)
  const ok = site ? site === 'same-origin' : (!origin || origin === APP_ORIGIN);
  if (ok) return next();
  log.warn({ event: 'csrf_rejected', site, origin, path: req.path }, 'blocked');
  res.status(403).json({ error: 'cross_site_blocked' });
}

app.get('/api/csrf-token', (req, res) =>
  res.json({ csrfToken: generateCsrfToken(req, res) }));
app.post('/api/wishlist', fetchMetadataGuard, doubleCsrfProtection, addToWishlist);

Sec-Fetch-Site browser নিজে বসায়, পাতার JavaScript বদলাতে পারে না। এটি খুব সস্তা একটি প্রথম ছাঁকনি। OWASP-এর cheat sheet শুধু আধুনিক browser-এর জন্য এর ওপর নির্ভরের অনুমতি দেয়, তবে Origin-এ fallback বাধ্যতামূলক রেখে। মনে রাখবেন, এই header শুধু নিরাপদ context-এ আসে, মানে HTTPS বা localhost. উপরের কোডের __Host- prefix আর secure: true হলো HTTPS-এর রূপ, যা README-র পরামর্শ। লোকাল HTTP-তে ল্যাব সাধারণ নাম আর secure: false-এ চলেছে, তাই এই দুটি মান এখানে run করা হয়নি। আর XSS থাকলে CSRF-এর সব দেয়াল অর্থহীন, কারণ কোড তখন আপনার origin থেকেই চলে। Next.js Server Action-এর নিজস্ব সুরক্ষা পর্ব ৫৪-তে আসবে।

৫ · CORS আর security header — আগে ও পরে

CORS-এর পুরো গল্প পর্ব ২৬-এ। এখানে শুধু নিরীক্ষা। সবচেয়ে বিপজ্জনক ভুলটি origin: true আর credentials: true একসাথে, যা যেকোনো origin-কে ফিরিয়ে দেয়। সত্যিকারের run, অচেনা Origin পাঠিয়ে:

# আগে
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://other.example
Access-Control-Allow-Credentials: true

# পরে
HTTP/1.1 200 OK
(Access-Control-Allow-Origin নেই)

“পরে”-তেও status 200! CORS server-এ request আটকায় না, শুধু browser-কে বলে উত্তরটি পাতাকে পড়তে দেবে কি না। তাই CORS কখনো অনুমতির বিকল্প নয়।

এবার helmet-এর আসল প্রভাব। একই অ্যাপ, curl -sI দিয়ে দুবার:

# আগে (নিরাপত্তার কিছু নেই, বরং পরিচয় জানানো)
HTTP/1.1 200 OK
X-Powered-By: Express
Vary: Origin
Access-Control-Allow-Credentials: true
Content-Type: text/html; charset=utf-8
…

# পরে (helmet 8.3.0, CSP ছোট করা)
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self';base-uri 'self';…;script-src 'self' 'nonce-QFJ3ZW21f1v+vJyqecq2Tg==';script-src-attr 'none';…
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Origin-Agent-Cluster: ?1
Referrer-Policy: no-referrer
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 0
Vary: Origin
…

১২টি header যোগ হলো, আর X-Powered-By গেল। প্রতিটির অর্থ আছে Web hardening পর্বে। তিনটি খেয়াল রাখুন। X-XSS-Protection: 0 ভুল নয়; পুরোনো browser-এর ত্রুটিপূর্ণ ফিল্টারটি ইচ্ছা করেই বন্ধ। Cross-Origin-Resource-Policy: same-origin অন্য origin থেকে আপনার ছবি লোড আটকাবে; CDN বা আলাদা frontend থাকলে এটি বদলাতে হতে পারে। আর header শুধু browser-কে দেওয়া নির্দেশ; API-তে curl চালালে এগুলো কিছুই আটকায় না।

HSTS — একবার দিলে ফেরা কঠিন

Strict-Transport-Security browser-কে বলে: এই site-এ শুধু HTTPS. max-age=31536000 মানে 31,536,000 ÷ 86,400 = ৩৬৫ দিন। RFC 6797 (২০১২) অনুযায়ী browser HTTP-তে পাওয়া এই header উপেক্ষা করে, তাই লোকাল ল্যাবে এর প্রভাব দেখা যায় না, শুধু header-টি যাচাই হয়। এটি যাচাই করতে হয় আসল HTTPS domain-এ। ঝুঁকি হলো, includeSubDomains-এ একটি subdomain HTTP-তে থাকলে সেটি এক বছর ভেঙে থাকবে। তাই ছোট মান (যেমন ৫ মিনিট) দিয়ে শুরু করে ধাপে বাড়ান। TLS-এর গভীরে যেতে Computer Networks কোর্স দেখুন।

৬ · SSRF — “URL দিয়ে ছবি” ফিচার

বিক্রেতা চান, ছবি upload না করে একটি URL দেবেন, আর বাজারঘর সেখান থেকে ছবি এনে নেবে। সহজ ফিচার, কিন্তু এখন server অন্যের কথায় বাইরে যাচ্ছে। Server নিজে ভেতরের নেটওয়ার্কে বসে থাকে, তাই সে এমন ঠিকানায় পৌঁছায়, যেখানে বাইরের কেউ পৌঁছায় না। একেই বলে SSRF, আর OWASP 2025-এ এটি A01-এর ভেতরে।

অফিসের পিয়নকে যে কেউ চিরকুট দিয়ে যেখানে খুশি পাঠাতে পারলে, তিনি ক্যাশ কাউন্টার থেকেও ফাইল এনে দেবেন, কারণ সেখানে সবাই তাঁকে চেনে। পিয়নের হাতে তাই নির্দিষ্ট ঠিকানার তালিকা দিতে হয়। পূর্ণ তত্ত্ব আছে SSRF পর্বে।

ল্যাবে 127.0.0.1:47513-এ একটি নকল “ভেতরের সেবা” রাখা হলো। “আগে”-র কোড শুধু fetch(url). সত্যিকারের run:

# আগে: ছবির ফিচার ভেতরের সেবার উত্তর ফিরিয়ে দিল
http://127.0.0.1:47513/ => {"status":200,"type":"application/json","preview":"{\"service\":\"internal-admin\",…"} [200]
http://127.0.0.1:47514/moved.png => {"status":200,"type":"application/json","preview":"{\"service\":\"internal-admin\",…"} [200]

# পরে
http://127.0.0.1:47513/ => {"error":"image_url_refused","reason":"bad_scheme"} [400]
https://img.bazarghar.test.evil.example/a.png => {…,"reason":"host_not_allowed"} [400]
http://127.0.0.1:47514/ok.png => {"ok":true,"type":"image/png","bytes":70} [200]
http://127.0.0.1:47514/moved.png => {…,"reason":"redirect_refused"} [400]
http://127.0.0.1:47514/page.html => {…,"reason":"bad_type"} [400]
http://127.0.0.1:47514/big.png => {…,"reason":"too_large"} [400]

দ্বিতীয় সারিটি দেখুন: একটি ছবির ঠিকানা redirect হয়ে ভেতরে চলে গেল। তাই শুরুর URL যাচাই যথেষ্ট নয়। 47514 ল্যাবের নকল CDN; শুধু এই একটি origin-এ ল্যাবের কোডে HTTPS আর host-এর শর্ত শিথিল ছিল। নিচের ব্লক আসল DNS বা network ছাড়াই যাচাইয়ের নিয়ম দেখায়; fakeDns একটি সাধারণ object.

const net = require("node:net");
const ALLOWED_HOSTS = new Set(["img.bazarghar.test", "img2.bazarghar.test"]);
const blocked = new net.BlockList();
for (const [ip, bits] of [["127.0.0.0", 8], ["10.0.0.0", 8], ["172.16.0.0", 12],
  ["192.168.0.0", 16], ["169.254.0.0", 16]]) blocked.addSubnet(ip, bits, "ipv4");

// নকল DNS: কোনো network call নেই। 203.0.113.0/24 শুধু উদাহরণের ঠিকানা
const fakeDns = { "img.bazarghar.test": "203.0.113.10", "img2.bazarghar.test": "10.0.0.5" };

function check(raw) {
  let url;
  try { url = new URL(raw); } catch { return "REFUSE bad_url"; }
  if (url.protocol !== "https:") return "REFUSE bad_scheme";
  if (url.username || url.password) return "REFUSE userinfo";
  const host = url.hostname.replace(/[[\]]/g, "");
  if (net.isIP(host)) return `REFUSE ip_literal (${host})`;
  if (!ALLOWED_HOSTS.has(host)) return "REFUSE host_not_allowed";
  const ip = fakeDns[host];
  if (blocked.check(ip, "ipv4")) return `REFUSE blocked_address (${ip})`;
  return `ALLOW ${ip}`;
}

for (const u of [
  "https://img.bazarghar.test/chal.png",
  "http://img.bazarghar.test/chal.png",
  "https://127.0.0.1/admin",
  "https://2130706433/admin",
  "https://[::1]/admin",
  "https://img.bazarghar.test@other.example/a.png",
  "https://img.bazarghar.test.other.example/a.png",
  "https://img2.bazarghar.test/a.png",
  "file:///etc/hosts",
]) console.log(u.padEnd(47), check(u));

শুধু প্রথম লাইনে ALLOW 203.0.113.10. লক্ষ করুন https://2130706433/admin: URL parser এটিকে নিজেই 127.0.0.1 করে দিল, তাই REFUSE ip_literal (127.0.0.1). String-এর regex এখানে ঠকে যেত, এজন্যই parser ব্যবহার করা। userinfo সারিতে আসল host other.example. img2 নাম তালিকায় থাকলেও তার ঠিকানা ভেতরের, তাই REFUSE blocked_address (10.0.0.5): allowlist একা নয়, ঠিকানাও দেখতে হয়।

একটি সূক্ষ্ম ফাঁক বাকি। নাম যাচাইয়ের সময় DNS এক ঠিকানা দিল, আর সংযোগের সময় আরেকটি। একে DNS rebinding বলে। সমাধান হলো যাচাইকে সংযোগের মুহূর্তে আনা: node:http-এর lookup option-এ নিজের ফাংশন দিন।

// socket সংযোগের ঠিক আগে এটি ডাকে, তাই যাচাই করা ঠিকানাতেই সংযোগ
function safeLookup(host, options, cb) {
  dns.lookup(host, { ...options, all: true }, (err, list) => {
    if (err) return cb(err);
    if (list.some((a) => isBlocked(a.address))) {
      return cb(new SsrfError('blocked_address', host));
    }
    if (options.all) return cb(null, list);
    cb(null, list[0].address, list[0].family);
  });
}

const req = https.get(url, { lookup: safeLookup, timeout: 3000 }, (res) => {
  if (res.statusCode >= 300 && res.statusCode < 400) {
    res.destroy();
    return reject(new SsrfError('redirect_refused', url.hostname));
  } // এরপর content-type-এর allowlist আর ২ MB সীমা
});

সত্যিকারের run: localhost-কে সাময়িকভাবে তালিকায় রেখে চালালে উত্তর এলো allowlisted name, loopback answer => blocked_address. net.BlockList ::ffff:127.0.0.1-কেও ধরল। ভালো ব্যবস্থা আরও একটি স্তর চায়: server-এর বাইরে যাওয়ার পথে firewall, আর ছবিগুলো নতুন করে encode করা (পর্ব ৩০)। সবচেয়ে সহজ পথ হলো ফিচারটিই না রাখা; বিক্রেতার browser থেকে সরাসরি upload.

৭ · Client bundle-এ secret আর SQL injection

Browser-এ যা যায়, তা সবার। পর্ব ৫-এ দেখেছিলেন, VITE_ prefix-ওয়ালা মান build-এ ঢুকে যায়। এটি ফাঁকি নয়, Vite-এর নকশা। একজন তাড়াহুড়ো করে payment key এভাবে লিখলে কী হয়? .env-এ দুটি নকল মান রেখে আসল build:

$ cat .env
VITE_PAYMENT_SECRET=example-secret-gateway-key-0001
PAYMENT_SERVER_KEY=example-server-only-key-0002

$ npx vite build
$ grep -rlE 'example-secret|example-server-only' dist
dist/assets/index-BoiqcFIz.js
$ grep -oE '.{30}example-secret[a-z0-9-]*.{10}' dist/assets/*.js
),window.__g={url:`/api`,key:`example-secret-gateway-key-0001`,server:v
$ grep -c 'example-server-only' dist/assets/*.js
0

# VITE_ মানটি সরিয়ে, payment-এর কাজ server-এ নিয়ে আবার build
$ grep -rlE 'example-secret|example-server-only' dist
$ echo $?
1

Prefix-ওয়ালা মান minify হওয়া ফাইলেও খোলা। Prefix ছাড়া PAYMENT_SERVER_KEY ঢোকেনি। grep-এর exit code 1 মানে কিছু মেলেনি, আর CI-তে এই একটি লাইনই build থামাতে পারে। Source map production-এ দিলে পুরো মূল কোডও পাঠানো হয়। Secret-এর মানগুলো পর্ব ৭৮-এ আরও গুছিয়ে আসবে। মনে রাখবেন, ফাঁস হওয়া key মুছলেই শেষ নয়; নতুন key বানিয়ে পুরোনোটি বাতিল করুন (পর্ব ৩১)। VITE_-এ রাখা যায় শুধু যা প্রকাশ্য, যেমন API-র URL.

SQL injection — আগেই সামলানো

SQL injection-এর সমাধান নতুন করে লাগে না, শুধু যাচাই লাগে। ORM আর parameterized query আছে পর্ব ৩৮-এ, আর filter-এর allowlist পর্ব ৩৪-এ। নিরীক্ষায় খুঁজুন $queryRawUnsafe, $executeRawUnsafe আর SQL-এর ভেতরে string জোড়া। Sort-এর কলামের নাম placeholder-এ বসে না, তাই সেখানে allowlist. তত্ত্ব আছে Injection প্রতিরক্ষা পর্বে।

৮ · Dependency — অন্যের কোডের হিসাব

ল্যাবের ছোট অ্যাপেই npm ls --all ২২১ লাইন দেখাল। এর বেশিরভাগ কোড আপনি কখনো পড়েননি। A03 Software Supply Chain Failures ২০২৫ সালের তালিকায় তিন নম্বরে। প্রথম কাজ npm audit: এটি আপনার lockfile-এর version-গুলো পরিচিত দুর্বলতার তালিকার সাথে মেলায়। পুরোনো package রেখে একটি আলাদা ফোল্ডারে আসল run:

$ npm audit
# npm audit report

esbuild  <=0.24.2
Severity: moderate
esbuild enables any website to send any requests to the development server and read the response - https://git…
fix available via `npm audit fix --force`
Will install vite@5.4.21, which is outside the stated dependency range
node_modules/esbuild
  vite  <=6.4.3
  Depends on vulnerable versions of esbuild
  node_modules/vite

lodash  <=4.17.23
Severity: high
Command Injection in lodash - https://github.com/advisories/GHSA-35jh-r3h4-6jhm
…
minimist  1.0.0 - 1.2.5
Severity: critical
Prototype Pollution in minimist - https://github.com/advisories/GHSA-xvch-5gv4-984h
…

4 vulnerabilities (1 moderate, 2 high, 1 critical)
$ echo $?
1

কীভাবে পড়বেন? esbuild-এর সমস্যা dev server-এর, যা production-এ চলে না। Production-এর ছবি দেখায় npm audit --omit=dev. আর --audit-level=high ঠিক করে কোন মাত্রায় CI ব্যর্থ হবে। “critical” মানে সবসময় “আপনার অ্যাপে এখনই বিপদ” নয়; দুর্বল ফাংশনটি আপনি ডাকেন কি না, সেটিও দেখতে হয়। আবার অন্ধভাবে npm audit fix --force চালালে major version লাফ দিতে পারে, অ্যাপ ভাঙতে পারে। আমরা তিনটি package নিয়ন্ত্রিতভাবে বদলালাম (lodash@4.18.1, minimist@1.2.8, vite@8.3.5), তারপর found 0 vulnerabilities. আর মূল ল্যাবে:

$ npm audit
found 0 vulnerabilities
$ npm audit signatures
107 packages have verified registry signatures
22 packages have verified attestations

Lockfile আর npm ci

Lockfile ঠিক কোন version আর hash, তা লিখে রাখে (পর্ব ৫)। CI আর production-এ npm install নয়, npm ci: এটি lockfile মেনে চলে, না মিললে থেমে যায়। আসল run, package.json-এ একটি নতুন package হাতে যোগ করে:

$ npm ci
npm error code EUSAGE
npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. …
npm error Missing: dayjs@1.11.13 from lock file

Supply chain — যখন package নিজেই ক্ষতিকর

npm audit জানা দুর্বলতা খোঁজে। কিন্তু কেউ যদি জনপ্রিয় package-এর নতুন version-এ ক্ষতিকর কোড ঢোকায়? প্রকাশিত কয়েকটি ঘটনা:

  • event-stream, ২০১৮: দায়িত্ব নতুন একজনের হাতে যাওয়ার পর যুক্ত dependency-তে একটি নির্দিষ্ট cryptocurrency wallet-কে লক্ষ্য করা কোড পাওয়া যায়।
  • Shai-Hulud, ২০২৫: GitHub-এর ব্লগ অনুযায়ী তারা ১৪ সেপ্টেম্বর খবর পায়। দখল হওয়া maintainer account দিয়ে post-install script ছড়ানো এই worm-এর কারণে ৫০০-র বেশি package সরানো হয়।
  • axios, ২০২৬: Datadog Security Labs-এর বিশ্লেষণ অনুযায়ী ৩১ মার্চ maintainer account থেকে দুটি ক্ষতিকর version বের হয়, যা নতুন একটি dependency-র postinstall চালাত। প্রায় তিন ঘণ্টা পরে npm সেগুলো সরায়। পেছনে কে ছিল, এ নিয়ে বিভিন্ন সংস্থার মূল্যায়ন আছে; চূড়ান্ত কিছু ধরে নেবেন না।

React নিজেও বাইরে নয়। React-এর blog অনুযায়ী ২০২৫ সালের ডিসেম্বরে Server Components-এ CVE-2025-55182 প্রকাশ পায়, CVSS 10.0; সংশোধন আসে 19.0.1, 19.1.2 আর 19.2.1-এ। Client-only অ্যাপ এতে আক্রান্ত ছিল না, তবে শিক্ষা হলো: জরুরি patch দ্রুত নেওয়ার পথ তৈরি রাখুন।

ঘটনাগুলোর একটি মিল: postinstall আর “মাত্র বের হওয়া version”. প্রতিরক্ষাও সেখানেই। দুটিই আসল run:

# একটি ক্ষতিহীন নিজের package, যার postinstall একটি লাইন ছাপে
$ npm i ../pkg/lab-postinstall-1.0.0.tgz --foreground-scripts
postinstall script ran
$ npm i ../pkg/lab-postinstall-1.0.0.tgz --ignore-scripts --foreground-scripts
added 1 package, and audited 2 packages in 544ms

# ৭৭ দিন আগের সর্বশেষ dayjs 1.11.23; অপেক্ষার নিয়ম ১০০ দিন
$ npm i dayjs --package-lock-only --min-release-age=100
$ node -p "require('./package-lock.json').packages['node_modules/dayjs'].version"
1.11.21

min-release-age দিনের হিসাবে একটি cooldown (npm 11.12.1-এ পরীক্ষিত)। Datadog-এর লেখাতেও কয়েক দিনের এই বিরতির পরামর্শ আছে। ignore-scripts কিছু package ভেঙে দিতে পারে, যেগুলো install-এর সময় native কোড বানায়, তাই নিজের প্রকল্পে পরীক্ষা করে নিন। আর update হাতে না করে Dependabot বা Renovate-এর মতো টুল PR খোলে, দুটিতেই ঠান্ডা হওয়ার সময় ঠিক করা যায় (Dependabot-এ cooldown, যা তার docs অনুযায়ী security update-এ খাটে না; Renovate-এ minimumReleaseAge). পুরো ছবি আছে Supply chain পর্বে; CI-তে বসানো পর্ব ৮১-তে।

৯ · Rate limit, log, পাস-ফেল তালিকা আর threat model

Rate limit

Rate limit-এর middleware দেখেছেন পর্ব ২৬-এ, আর login-এর দিক পর্ব ৪৯-এ। নিরীক্ষায় প্রমাণ চাই। ১৫ মিনিটে ৫টি চেষ্টা, আসল run (express-rate-limit 8.7.2):

HTTP/1.1 401 Unauthorized RateLimit: "5-in-15min"; r=4; t=900
HTTP/1.1 401 Unauthorized RateLimit: "5-in-15min"; r=3; t=900
HTTP/1.1 401 Unauthorized RateLimit: "5-in-15min"; r=2; t=900
HTTP/1.1 401 Unauthorized RateLimit: "5-in-15min"; r=1; t=900
HTTP/1.1 401 Unauthorized RateLimit: "5-in-15min"; r=0; t=899
HTTP/1.1 429 Too Many Requests RateLimit: "5-in-15min"; r=0; t=899 Retry-After: 899

এবার হিসাব। প্রতি ঠিকানায় ১৫ মিনিটে ৫টি মানে দিনে ৯৬ × ৫ = ৪৮০টি চেষ্টা। ধরে নেওয়া সংখ্যা: কেউ ১,০০০টি ঠিকানা থেকে চেষ্টা করলে দিনে ৪,৮০,০০০টি। ৬ অঙ্কের OTP-র ১০,০০,০০০টি সম্ভাবনার প্রায় অর্ধেক একদিনে! তাই ঠিকানার সাথে account অনুযায়ী সীমাও লাগে, আর OTP-র মেয়াদ ছোট রাখতে হয়। Proxy-র পেছনে trust proxy ঠিক না হলে সবাই এক ঠিকানা হয়ে যায়। একাধিক server থাকলে গণনা Redis-এর মতো ভাগ করা store-এ রাখুন, নইলে প্রতি server আলাদা গোনে।

নিরাপত্তার ঘটনার log

পর্ব ৩১-এর pino-তে একটি event field যোগ করলেই চলে। Password বা token নয়, শুধু কী ঘটল। আসল লাইন (সময় বাদ দিয়ে):

{"level":"warn","event":"login_rate_limited","ip":"127.0.0.1","msg":"too many logins"}
{"level":"warn","event":"csrf_rejected","reason":"fetch_metadata","site":"cross-site","origin":"http://127.0.0.1:47511","path":"/form/wishlist","msg":"blocked"}
{"level":"warn","event":"ssrf_blocked","reason":"bad_scheme","host":"127.0.0.1","msg":"image url refused"}
{"level":"warn","event":"csp_violation","directive":"script-src-attr","blocked":"inline","disposition":"enforce","sample":"","msg":"csp report"}

A09 শুধু log নয়, alert-ও চায়। একটি ঠিকানা থেকে অনেক login_failed, বা হঠাৎ csrf_rejected-এর ঢেউ, এমন নিয়মে কারও ফোনে খবর যাবে। তত্ত্ব আছে Security logging পর্বে।

চূড়ান্ত পাস-ফেল তালিকা

হাতে মেলানো তালিকা বাসি হয়ে যায়। তাই ১৬টি যাচাইকে একটি script করা হলো, যা চলমান লোকাল অ্যাপে request পাঠায়, dist পড়ে আর npm audit চালায়। একই script, দুই রূপে, আসল ফল:

বিভাগযাচাইআগেপরে
A02CSP headerFAILPASS
A02HSTS headerFAILPASS
A02nosniffFAILPASS
A02no X-Powered-ByFAILPASS
A02CORS refuses unknown originFAILPASS
A01/api/me needs loginFAILPASS
A02session cookie HttpOnly+SameSiteFAILPASS
A01POST without CSRF token refusedFAILPASS
A01POST with CSRF token worksPASSPASS
A01SSRF: internal URL refusedFAILPASS
A10bad JSON: no stack traceFAILPASS
A07login error hides which fieldFAILPASS
A07login rate limit (429)FAILPASS
A07no secret in client bundlePASSPASS
A03npm audit: no high/criticalPASSPASS
A09security events in logFAILPASS
মোট3/1616/16

সততার সাথে পড়ুন। “আগে”-র bundle আর dependency পাস করেছে, কারণ এই দুটি আমরা ৭ ও ৮ অংশে আলাদা ফোল্ডারে দেখিয়ে আগেই সারিয়েছি। A04, A06 আর A08-এর কোনো সারি নেই; সেগুলো script দিয়ে সহজে মাপা যায় না। “bad JSON” সারিতে “আগে” Express-এর ডিফল্ট পাতা ফাইলের পূর্ণ path-সহ stack trace দেখিয়েছিল, কারণ NODE_ENV=development; “পরে” শুধু {"error":"bad_request"} (পর্ব ২৮)। Script-টি শুধু লোকাল বা নিজের staging-এ চালান, আর এটি পর্ব ৮১-র CI-তে বসানো যায়।

এক পাতার threat model

Threat model তালিকার ফাঁক ধরে। Threat modeling পর্বের চারটি প্রশ্নের একটি ছোট রূপ: কী বানাচ্ছি, কী ভুল হতে পারে, কী করছি, কাজটি ভালো হলো কি? STRIDE-এর অক্ষর মনে করিয়ে দেয় কী কী ভাবতে হবে। বাজারঘরের এক পাতা:

সম্পদ বা পথSTRIDEকী ভুল হতে পারেব্যবস্থাপ্রমাণ বা বাকি
LoginSঅন্যের account-এ ঢোকাhash, rate limit, MFA ঐচ্ছিক429 run হয়েছে; MFA বাকি
Order-এর দামTClient থেকে দাম বদলানোদাম server হিসাব করেবাকি: দাম বদলে API test
RefundR“আমি করিনি”কে, কখন, কোন order-এর logবাকি: log লাইনের test
ক্রেতার ঠিকানাIঅন্যের order দেখা, log-এ ফাঁসমালিকানা যাচাই, redactঅন্য user দিয়ে API test (পর্ব ৪৮)
অফারের দিনDSearch-এ ভারী চাপrate limit, cache, body limitবাকি: load test
Admin routeEসাধারণ ক্রেতা admin হওয়াserver-এ role যাচাইঅন্য role দিয়ে API test (পর্ব ৪৮)
কুপননকশাএকই কুপন বারবারtransaction, একবারের নিয়মবাকি: সিদ্ধান্ত দরকার

শেষ কলামটিই আসল। যেখানে প্রমাণ নেই, সেখানে “বাকি” লিখে রাখা সবচেয়ে সৎ কাজ। ব্যবসার নিয়মের ভুল, যেমন কুপন, কোনো header ধরে না। এই পাতাটি প্রতিটি বড় ফিচারের আগে আধা ঘণ্টায় আবার দেখুন।

আরও গভীরে: Top 10 সচেতনতার দলিল, যাচাইয়ের মান নয়। মিলিয়ে দেখার মতো মান হলো OWASP ASVS, আর API-র জন্য আলাদা API Security Top 10. CSP-তে strict-dynamic আর Trusted Types পরের ধাপ। Software-এর উপাদানের তালিকা (SBOM) আর SLSA-র মতো ব্যবস্থা supply chain-কে যাচাইযোগ্য করে। API security পর্বে আরও আছে।

পরীক্ষা ও ইন্টারভিউতে:

  • React কি XSS পুরো ঠেকায়? না। JSX-এর লেখা escape হয়, কিন্তু dangerouslySetInnerHTML, URL আর DOM-এর সরাসরি কাজ আলাদা।
  • CSRF আর XSS-এর পার্থক্য? CSRF-এ browser-এর cookie দিয়ে অনুরোধ পাঠানো হয়, XSS-এ আপনার পাতায় কোড চলে।
  • CORS কি server রক্ষা করে? না। এটি browser-এর পড়ার ছাড়; request server-এ পৌঁছায়।
  • npm ci আর npm install-এর পার্থক্য? npm ci lockfile হুবহু মানে, অমিলে থেমে যায়।
  • SSRF ঠেকাতে শুধু URL-এর লেখা দেখা কেন যথেষ্ট নয়? Redirect, DNS আর সংখ্যায় লেখা IP আসল গন্তব্য বদলে দিতে পারে।

১০ · AI-এর যুগে অ্যাপের নিরাপত্তা

AI-এর লেখা কোড চলে, আর নিরাপত্তার ভুল চলার সময় চোখে পড়ে না। CORS-এর error এলে AI প্রায়ই origin: true দিয়ে “সমাধান” করে। CSP-র বার্তা এলে 'unsafe-inline' যোগ করে। অর্থাৎ সে error থামায়, ঝুঁকি নয়। নিচের ভুলগুলো খেয়াল রাখুন।

AI-এর ভুলকীভাবে ধরবেন
Deprecated csurf বসানোnpm view csurf deprecated দেখুন
নেই এমন package বা optionnpm আর official docs-এ নিজে খুঁজুন, নামের বানানও দেখুন
Error থামাতে CSP বা CORS ঢিলা করাDiff-এ unsafe-inline, *, origin: true খুঁজুন
Regex দিয়ে HTML পরিষ্কারDOMPurify ব্যবহার করে ক্ষতিহীন নমুনায় test লিখুন
Blocklist দিয়ে SSRF “ঠেকানো”Redirect আর সংখ্যায় লেখা IP দিয়ে test করুন
Frontend-এ API key বসানোBuild-এ grep চালান
“Login আছে” ধরে নিয়ে অনুমতি বাদ দেওয়াঅন্য user দিয়ে অন্যের id-তে request পাঠান
অপ্রয়োজনীয় package যোগআগে দেখুন built-in দিয়ে হয় কি না; lockfile-এর diff পড়ুন

AI-কে নিরীক্ষকের কাজ দিন, শেষ সিদ্ধান্ত নয়। আসল secret, production log বা ক্রেতার তথ্য prompt-এ দেবেন না।

আমার Express 5 আর React 19 অ্যাপের নিচের কোড OWASP Top 10:2025 ধরে নিরীক্ষা করো। শর্ত: ১. প্রতিটি সমস্যার জন্য ফাইল, লাইন, বিভাগ আর ঝুঁকি বলো। অনুমান হলে "অনিশ্চিত" লেখো। ২. CSP বা CORS ঢিলা করে কোনো সমাধান দেবে না। ৩. কোনো নতুন package প্রস্তাব করলে npm-এ তার নাম, সর্বশেষ version, deprecated কি না আর কেন লাগবে, লেখো। আমি নিজে মিলিয়ে নেব। ৪. আগে বলো কোন version-এর docs ধরে লিখছ। ৫. প্রতিটি সারানোর জন্য এমন একটি test দাও, যা শুধু আমার localhost-এ চলে আর আমি নিজে চালিয়ে পাস-ফেল দেখতে পারি। নিজের দেওয়া test কখনো "পাস করেছে" বলবে না, কারণ তুমি সেটি চালাওনি। আমার কোডে কোনো আসল secret নেই।

AI-এর লেখা কোড মেলানোর চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১Allowlist-ভিত্তিক CSP তো host-এর তালিকা দিয়েই চলত। তাহলে nonce-এর দিকে যাওয়া হলো কেন?

CSP-র যাত্রা শুরু হয় ২০০০-এর দশকের শেষে Mozilla-র উদ্যোগে, আর Firefox 4-এ (২০১১) এটি প্রথম বড় পরিসরে আসে। মূল চিন্তা ছিল সরল: কোন host থেকে script আসতে পারবে, সেই তালিকা server দেবে। কাগজে এটি শক্ত দেয়াল মনে হয়।

বাস্তবে তালিকা দ্রুত বড় হলো। Analytics, বিজ্ঞাপন, CDN, প্রতিটির জন্য নতুন host. আর একটি বিশ্বস্ত host-এও এমন ফাইল থাকতে পারে, যা তালিকার উদ্দেশ্যই ব্যর্থ করে, যেমন পুরোনো JSONP endpoint বা পুরোনো library. ২০১৬ সালে Google-এর গবেষকরা (Weichselbaum ও সহকর্মীরা) “CSP Is Dead, Long Live CSP!” গবেষণাপত্রে বাস্তব জগতের বিপুল সংখ্যক policy বিশ্লেষণ করে দেখান, বেশিরভাগ allowlist-ভিত্তিক policy কার্যত এড়ানো সম্ভব। তাঁরা nonce আর strict-dynamic-এর পথ প্রস্তাব করেন।

Nonce প্রশ্নটিই বদলে দেয়। “এই host বিশ্বস্ত কি?” নয়, “এই নির্দিষ্ট script tag কি আমি নিজে এই response-এ বসিয়েছি?” বসানো হয় প্রতি response-এ নতুন মান দিয়ে, তাই মানটি আগে থেকে অনুমান করা যায় না।

দাম আছে। Nonce-এর জন্য server-কে প্রতিবার HTML বানাতে বা অন্তত বদলাতে হয়। সম্পূর্ণ static সাইটে, বা CDN-এ cache করা পাতায় এটি কঠিন। সেখানে hash-ভিত্তিক policy একটি বিকল্প। আমাদের ল্যাবে Vite-এর আলাদা ফাইল 'self'-এই চলত, nonce লেগেছে শুধু একটি inline script-এর জন্য। অনেক SPA-র ক্ষেত্রে inline script বাদ দিয়ে 'self' রাখাই সবচেয়ে সরল সমাধান, যদিও একই origin-এ ব্যবহারকারীর upload করা ফাইল থাকলে তা ঝুঁকি।

মূল উপলব্ধি: বিশ্বাসের একক যত ছোট, দেয়াল তত শক্ত। পুরো host-কে বিশ্বাস করার চেয়ে একটি নির্দিষ্ট script-কে বিশ্বাস করা ভালো, তবে সেই খরচ আপনার অ্যাপের নকশায় মানায় কি না, তা দেখে নিন।

প্র ০২npm audit-এ লাল সংখ্যা দেখলেই কি বিপদ? আর ০ দেখলেই কি নিরাপদ?

দুটির কোনোটিই নয়। ২০২১ সালে React দলের Dan Abramov “npm audit: Broken by Design” লেখায় দেখান, Create React App-এর মতো টুলে বহু “high” সতর্কতা আসলে build-এর সময়ের regex-এর ধীরগতি নিয়ে, যা অ্যাপের ব্যবহারকারীর কাছে পৌঁছায়ই না। নতুন শিক্ষার্থী ভয় পান, অভিজ্ঞরা অভ্যাসবশত উপেক্ষা করেন। এটিই alert fatigue. একটি সত্যিকারের সতর্কতা তখন ভিড়ে হারিয়ে যায়।

আমাদের run-এই দেখুন। esbuild-এর সতর্কতা dev server-এর। তা গুরুত্বহীন নয়, কারণ ডেভেলপারের যন্ত্রও লক্ষ্য হতে পারে, তবে production-এর জরুরি কাজ নয়। minimist “critical”, কিন্তু আমাদের কোড যদি অবিশ্বস্ত ইনপুট দিয়ে এটি না ডাকে, বাস্তব ঝুঁকি কম হতে পারে। প্রশ্ন তিনটি: কোডটি production-এ যায় কি? দুর্বল ফাংশনটি ডাকা হয় কি? ইনপুট বাইরের কি?

অন্যদিকে ০ মানে শুধু “জানা তালিকায় এই মুহূর্তে কিছু মেলেনি”। ২০২৬ সালের axios ঘটনার প্রথম কয়েক ঘণ্টায়, বা event-stream-এর মাসগুলোতে, audit সবুজই দেখানোর কথা, কারণ তখনও কোনো advisory লেখা হয়নি। আপনার নিজের কোডের ভুলও এর আওতায় নেই।

তাহলে কী করবেন? Production dependency-তে high ও critical-এ CI থামান (--omit=dev --audit-level=high), কিন্তু সতর্কতা উপেক্ষার একটি লিখিত নিয়ম রাখুন, কারণসহ, মেয়াদসহ। অপ্রয়োজনীয় package কমান, কারণ যে কোড নেই তার সমস্যাও নেই। Lockfile, npm ci, cooldown আর ignore-scripts ক্ষতিকর নতুন version-এর পথ সরু করে। আর জরুরি patch দ্রুত পাঠানোর একটি পরীক্ষিত পথ রাখুন।

মূল উপলব্ধি: npm audit একটি সংকেত, রায় নয়। তার সংখ্যা পড়তে হয় আপনার অ্যাপের প্রসঙ্গে, আর সে যা দেখে না তার জন্য আলাদা ব্যবস্থা লাগে।

প্র ০৩CORS খোলা রাখলে কি CSRF হয়? দুটি বিষয় অনেকে গুলিয়ে ফেলেন কেন?

দুটি আলাদা প্রশ্নের উত্তর। Same-origin policy-র জন্ম ১৯৯০-এর দশকে Netscape-এ, যখন JavaScript এলো। মূল নীতি: অন্য origin-এ পাঠানো যায়, কিন্তু উত্তর পড়া যায় না। Form দিয়ে অন্য সাইটে POST তখন থেকেই চলে। CSRF জন্ম নেয় এই পাঠানোর ক্ষমতা থেকে, আর cookie-র নিজে থেকে যাওয়ার অভ্যাস থেকে।

CORS, যা পরে W3C-তে আর এখন Fetch মানে লেখা, সেই “পড়া”-র দেয়ালে নিয়ন্ত্রিত দরজা। তাই CORS বন্ধ রাখলেও সাধারণ form POST যায়, যেমন আমাদের ল্যাবে গেছে। অর্থাৎ CORS ঠিক থাকলেও CSRF হতে পারে। CORS CSRF-এর প্রতিরক্ষা নয়।

উল্টো দিকটিও আছে। Preflight লাগে এমন request, যেমন application/json-এর POST, ভুল origin থেকে আসতে পারে না। অনেকে তাই ভাবেন JSON API-তে CSRF নেই। ধারণাটি অনেকটা ঠিক, কিন্তু ভঙ্গুর। Server যদি অন্য content type-ও পড়ে, বা CORS যদি origin: true আর credentials: true দিয়ে সব origin ফিরিয়ে দেয়, যেমন আমাদের “আগে”-তে, তাহলে অন্য site token-এর route থেকে token পড়েই নিতে পারে। তখন CORS-এর ভুল CSRF token-কেও অকেজো করে।

গুলিয়ে যাওয়ার কারণ, দুটিতেই “cross-site” শব্দ আছে, আর দুটিই browser-এ দেখা যায়। তাই একটি সহজ পরীক্ষা মনে রাখুন। CSRF-এর প্রশ্ন: “কেউ আমার ব্যবহারকারীর হয়ে কিছু করাতে পারে?” CORS-এর প্রশ্ন: “অন্য সাইটের JavaScript আমার উত্তর পড়তে পারে?” Web-এর নিরাপত্তা মডেল পর্বে এই ভাগ বিস্তারে আছে।

মূল উপলব্ধি: প্রতিটি ব্যবস্থা ঠিক কোন প্রশ্নের উত্তর দেয়, তা জানুন। ভুল প্রশ্নে সঠিক ব্যবস্থা বসালে সুরক্ষার অনুভূতি পাওয়া যায়, সুরক্ষা নয়।

প্র ০৪SSRF-এ blocklist-এর চেয়ে allowlist কেন বেশি জোর পায়? বাস্তব ঘটনা কী শেখায়?

২০১৯ সালের Capital One তথ্য ফাঁস এই আলোচনায় বহুল উল্লেখিত। প্রকাশিত মার্কিন আদালতের নথি আর বিশ্লেষণ অনুযায়ী, ভুল কনফিগার করা একটি firewall-এর মধ্য দিয়ে cloud-এর metadata সেবায় পৌঁছে সাময়িক credential পাওয়া যায়, আর বিপুল গ্রাহকের তথ্য ফাঁস হয়। সংখ্যাটি প্রায় ১০ কোটি মানুষ বলে জানানো হয়েছিল। এরপর AWS ২০১৯-এর শেষে IMDSv2 আনে, যেখানে আগে একটি session token নিতে হয়। অর্থাৎ platform নিজেও এই পথ শক্ত করতে নেমেছিল।

Blocklist-এর দুর্বলতা হলো, তাকে সব খারাপ জিনিস আগেই জানতে হয়। 127.0.0.1, localhost, 2130706433, IPv6-এর রূপ, ভেতরের নেটওয়ার্কের নাম, redirect, DNS-এর বদল। আমাদের চালানো ব্লকেই দেখলেন, একটি সংখ্যা URL parser-এর হাতে 127.0.0.1 হয়ে গেল। String মিলিয়ে কেউ এটি ধরতে পারত না। তালিকা যত বড়ই হোক, পরিবেশ বদলালে সেটি পুরোনো হয়।

Allowlist প্রশ্ন উল্টে দেয়: “কী কী বৈধ?” বাজারঘরের ছবির উৎস যদি কয়েকটি পরিচিত host হয়, তালিকা ছোট আর পরীক্ষাযোগ্য। Saltzer আর Schroeder ১৯৭৫ সালে fail-safe defaults-এর যে নীতি দেন, এটি তারই রূপ: অনুমতি দেওয়ার ভিত্তিতে সিদ্ধান্ত, বাদ দেওয়ার ভিত্তিতে নয়।

তবে সব ক্ষেত্রে allowlist সম্ভব নয়। ব্যবহারকারীর দেওয়া যেকোনো সাইটের preview বা webhook-এর মতো ফিচারে host আগে জানা যায় না। তখন ঠিকানার blocklist (connect-এর সময়), আলাদা নেটওয়ার্কে বসানো ছোট একটি fetch সেবা, firewall, আকার আর সময়ের সীমা, এই স্তরগুলোর ওপর নির্ভর করতে হয়। প্রতিটি স্তর আলাদাভাবে ব্যর্থ হতে পারে, তাই অনেকগুলো।

মূল উপলব্ধি: নিরাপত্তায় “কী খারাপ” তালিকা কখনো শেষ হয় না, কিন্তু “কী বৈধ” তালিকা ছোট রাখা যায়। আগে ফিচারের পরিধি ছোট করুন, তারপর দেয়াল বসান।

প্র ০৫১৬/১৬ পাস। তাহলে কি বাজারঘর নিরাপদ? Checklist-এর সীমা কোথায়?

না। ১৬/১৬ মানে শুধু এই ১৬টি প্রশ্নের উত্তর এই মুহূর্তে সবুজ। Edsger Dijkstra-র বিখ্যাত কথা, test bug-এর উপস্থিতি দেখাতে পারে, অনুপস্থিতি নয়। নিরাপত্তায় কথাটি আরও খাটে, কারণ এখানে অপর পক্ষ ঠিক সেই জায়গাটিই খোঁজেন, যা আপনি ভাবেননি।

Checklist-এর শক্তি কম নয়। Atul Gawande তাঁর “The Checklist Manifesto” বইয়ে অস্ত্রোপচার আর বিমান চালনার উদাহরণে দেখান, জানা কাজ ভুলে যাওয়া ঠেকাতে তালিকা কতটা কার্যকর। আমাদের script-ও তাই করে: কেউ helmet সরালে পরের run-এ লাল। জানা ভুল ফিরে আসা ঠেকানোই এর আসল মূল্য।

সীমা তিন জায়গায়। প্রথমত, ব্যবসার নিয়ম। “একজন ক্রেতা একই কুপন দুবার ব্যবহার করতে পারবেন কি না”, এমন প্রশ্নের জন্য কোনো header নেই। A06 Insecure Design ঠিক এই কথাই বলে। দ্বিতীয়ত, অনুমতি। আমাদের একটি সারি শুধু /api/me দেখে। অন্য ৮০টি route? Broken access control সবচেয়ে ওপরে থাকার একটি কারণ, স্বয়ংক্রিয় টুল বোঝে না কে কোন ডেটার মালিক। তৃতীয়ত, সময়। আজকের সবুজ কাল বদলাতে পারে: নতুন dependency, নতুন ফিচার, ভুল config.

একটি ভালো অভ্যাস হলো Goodhart-এর সতর্কতা মনে রাখা: সংখ্যা লক্ষ্য হয়ে গেলে মাপ হিসেবে তার মূল্য কমে। কেউ শুধু ১৬/১৬ করার জন্য সারি মুছে দিলেও সবুজ থাকবে। তাই সারি বদলের PR-ও review করুন। বড় ঝুঁকির অ্যাপে অনুমতি নিয়ে বাইরের নিরপেক্ষ পরীক্ষা (penetration test) আর স্পষ্ট vulnerability disclosure নীতিও দরকার।

মূল উপলব্ধি: Checklist হলো মেঝে, ছাদ নয়। এটি জানা ভুল ফেরা আটকায়, আর threat model, review ও মানুষের বিচার ধরে যা তালিকায় নেই।

অনুশীলন

হিসাব, কমান্ড, সমালোচনা আর আউটপুট অনুমান, চারটিই আছে।

  1. Rate limit-এর হিসাব: একটি login route-এ প্রতি ঠিকানায় ১০ মিনিটে ৫টি চেষ্টা, আর ৪ অঙ্কের PIN. (ক) এক ঠিকানা থেকে দিনে সর্বোচ্চ কতটি চেষ্টা? (খ) ৫০০টি ঠিকানা থাকলে? সব PIN দেখতে কত দিন? (গ) Account অনুযায়ী দিনে ২০টি চেষ্টার সীমা দিলে কত দিন?

    (ক) দিনে ১৪৪টি ১০ মিনিটের ভাগ, তাই 144 × 5 = ৭২০টি। (খ) 720 × 500 = ৩,৬০,০০০টি। ৪ অঙ্কের PIN-এ ১০,০০০টি সম্ভাবনা, তাই 10,000 ÷ 3,60,000 ≈ ০.০২৮ দিন, প্রায় ৪০ মিনিট। (গ) 10,000 ÷ 20 = ৫০০ দিন। ঠিকানার সীমা নয়, account-এর সীমাই আসল বাধা। তার সাথে PIN-এর মেয়াদ আর সন্দেহজনক ঘটনার alert রাখুন।

  2. নিজের build যাচাই (কমান্ড): নিজের Vite প্রকল্পে এমন দুটি কমান্ড লিখুন, একটি dist-এ example-secret খুঁজবে, আরেকটি production dependency-তে high বা critical থাকলে ব্যর্থ হবে। দুটির exit code কী অর্থ বহন করে?

    ! grep -rq 'example-secret' dist কিছু না পেলে 0 দেয় (! ফলাফল উল্টে দেয়), আর পেলে অশূন্য, তাই CI থামে। npm audit --omit=dev --audit-level=high ওই মাত্রার সমস্যা থাকলে 1 দেয়, না থাকলে 0. CI-তে অশূন্য exit code মানে ধাপটি ব্যর্থ। Secret-এর আসল মান CI log-এ লিখবেন না; নকল মান বা key-র নামের ধরন খুঁজুন।

  3. কোড সমালোচনা (নকশা): অন্তত পাঁচটি সমস্যা ধরুন: app.use(cors({ origin: true, credentials: true })); app.post('/api/avatar', async (req,res)=>{ const r = await fetch(req.body.url); res.send(await r.text()); }); app.get('/api/delete-account', auth, del); res.cookie('sid', id); function Bio({html}){ return <div dangerouslySetInnerHTML={{__html: html}}/>; }
    • origin: true + credentials: যেকোনো origin login করা ক্রেতার উত্তর পড়তে পারে। নির্দিষ্ট allowlist দিন।
    • fetch(req.body.url): SSRF. Allowlist, connect-এর সময় ঠিকানা যাচাই, redirect বন্ধ, timeout, আকারের সীমা।
    • res.send(r.text()): বাইরের উত্তর সরাসরি ফেরত, ভেতরের তথ্য ফাঁস আর XSS-এর পথ। Content-type যাচাই করুন, লেখা ফেরত দেবেন না।
    • GET-এ account মুছে ফেলা: GET কখনো কিছু বদলাবে না; Lax cookie top-level GET-এ যায়। DELETE বা POST আর CSRF সুরক্ষা দিন।
    • res.cookie('sid', id): HttpOnly, Secure, SameSite নেই। ডিফল্টে ২ মিনিটের ছাড়ও থাকে।
    • dangerouslySetInnerHTML: ব্যবহারকারীর bio-তে stored XSS. সাধারণ JSX বা DOMPurify. /api/avatar-এ CSRF token-ও নেই।
  4. আউটপুট অনুমান: Browser-এ shop.bazarghar.test-এ login করা, cookie sid, SameSite=Lax, কোনো CSRF token নেই। ক) blog.bazarghar.test-এর একটি form shop-এ POST করল। খ) other.example-এর একটি form shop-এ POST করল। গ) other.example-এর একটি সাধারণ link shop-এর একটি GET পাতায় নিল। কোন কোন ক্ষেত্রে cookie যাবে, আর Sec-Fetch-Site কী হবে?

    ক) Cookie যাবে, same-site: একই registrable domain, তাই Lax বাধা দেয় না। এখানেই token দরকার। খ) যাবে না, cross-site. গ) যাবে, cross-site: Lax top-level GET navigation-এ cookie পাঠায়। তাই GET-এ কিছু বদলানো বিপজ্জনক। ল্যাবের port-ভিত্তিক সারিগুলোতেও ঠিক প্রথম দুটি ফলই এসেছিল।

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

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

ক্রমএকটি চালু অ্যাপে CSP বসানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Report-Only header-এ policy চালু করা
  2. Report জমা করে পড়া
  3. নিজের কোড বা তৃতীয় পক্ষের script ঠিক করা
  4. কিছু দিন report শান্ত আছে কি না দেখা
  5. Enforce করা
  6. Report আর alert চালু রাখা

React 19-এ <p>{comment}</p> লেখা আছে, আর comment-এর মান <b>চাল</b>. Browser-এ কী দেখা যাবে?

  • গাঢ় অক্ষরে “চাল”
  • <b>চাল</b> অক্ষরগুলো হুবহু
  • React error দিয়ে থেমে যাবে
  • ফাঁকা অনুচ্ছেদ

Content-Security-Policy-Report-Only header-এ একটি inline event handler নিয়ম ভাঙলে কী হয়?

  • Handler আটকে যায়, report যায় না
  • পুরো পাতা বন্ধ হয়ে যায়
  • Handler চলে, আর violation জানানো হয়
  • কিছুই ঘটে না, কোনো খবরও আসে না

shop.bazarghar.test-এর session cookie SameSite=Lax. কোন উৎসের POST form-এ cookie চলে যায়?

  • blog.bazarghar.test
  • other.example
  • bazarghar-test.example
  • কোনো উৎস থেকেই নয়, এমনকি একই site থেকেও নয়

একটি API-তে অচেনা origin-এর request এলো। CORS নিয়মে সেই origin অনুমোদিত নয়। সাধারণ GET-এ কী ঘটে?

  • Server request পাওয়ার আগেই নেটওয়ার্ক থেকে বাদ পড়ে
  • Server সব সময় 403 ফেরত দেয়
  • পাতার JavaScript উত্তরটি পড়তে পারে
  • Server উত্তর দেয়, কিন্তু browser সেই উত্তর পাতাকে পড়তে দেয় না

“URL থেকে ছবি আনা” ফিচারে শুরুর URL যাচাই হয়েছে। তবু Node-এর fetch-এর ডিফল্টে SSRF থাকতে পারে কোন কারণে?

  • ডিফল্ট User-Agent header-এর জন্য
  • ডিফল্টে redirect অনুসরণ করে, পরের ঠিকানা যাচাই হয় না
  • Browser-এর cookie সাথে পাঠায়
  • HTTPS সমর্থন করে না

CI-তে dependency হুবহু lockfile অনুযায়ী বসাতে, আর অমিল থাকলে থেমে যেতে কোন কমান্ড ব্যবহার করা উচিত?

  • npm install
  • npm update
  • npm ci
  • npm audit

শব্দকোষ

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

মনে রাখুন

  1. JSX লেখা escape করে; dangerouslySetInnerHTML-এ DOMPurify, URL-এ allowlist, আর CSP দ্বিতীয় দেয়াল।
  2. CSP আগে Report-Only, পরে enforce; বার্তা থামাতে unsafe-inline নয়, কোড বদলান।
  3. SameSite স্তর, একমাত্র দেয়াল নয়। Cookie থাকলে token বা Fetch Metadata, আর SameSite নিজে লিখুন।
  4. SSRF-এ allowlist, connect-এর সময় ঠিকানা যাচাই, redirect বন্ধ, সীমা; secret client-এ নয়, grep-এ প্রমাণ দিন।
  5. Lockfile, npm ci, audit, cooldown; আর script-এর ১৬/১৬ মেঝে, threat model ধরে বাকিটা।

পরের পর্বে: Authentication আর নিরাপত্তার মডিউল শেষ। এবার Next.js. File দিয়েই route, layout আর navigation: App Router দিয়ে বাজারঘরের frontend-কে নতুন ঘরে তোলার শুরু।

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

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