এই পর্বে যা শিখবেন
- XSS কী, এর ক্ষতি কতটা, আর reflected, stored ও DOM ধরনের পার্থক্য
- কেন “context” না বুঝলে encoding ভুল হয়, আর প্রতিটি context-এর সঠিক নিয়ম
- Template engine,
innerHTML,textContent, sanitizer আর URL allowlist কখন কোনটি - CSP-র nonce,
strict-dynamic,default-srcআর report-only rollout - নিজের app পরীক্ষা, detection আর ঘটনা ঘটলে সাড়া দেওয়ার পথ
১ · XSS কী: ডেটা যখন পাতার কোড হয়ে যায়
একটি কাল্পনিক অনলাইন বাজারের কথা ভাবুন। পণ্যের পাতায় ক্রেতারা মন্তব্য লেখেন। মন্তব্য তো শুধু লেখা, তাই না? সমস্যা হলো, browser-এর কাছে HTML পাতাটি একটিই লম্বা লেখা। সেখানে কোনটি আপনার কোড আর কোনটি ক্রেতার ডেটা, তার আলাদা কোনো দাগ নেই। ডেটায় যদি এমন অক্ষর থাকে যা HTML-এ বিশেষ অর্থ রাখে, browser সেটিকেও কোড হিসেবে পড়তে পারে।
XSS (Cross-Site Scripting) হলো সেই ত্রুটি, যেখানে আক্রমণকারীর দেওয়া ডেটা ভুক্তভোগীর browser-এ আপনার সাইটের script হিসেবে চলে। লক্ষ করুন, আক্রমণকারীর script চলছে আপনার origin-এর ভেতরে। তাই browser-এর same-origin policy (পর্ব ৩৬) তাকে আটকায় না। Browser ভাবে, এটি আপনার সাইটেরই কোড।
অফিসের একজন পিয়নের কথা ভাবুন, যিনি খুব বাধ্য। তাঁর কাজ দর্শনার্থীদের চিরকুট ম্যানেজারের টেবিলে পৌঁছে দেওয়া। কিন্তু ম্যানেজার টেবিলে যা পান, সেটিকেই অফিসের আদেশ ধরে নেন। একজন অচেনা দর্শনার্থী চিরকুটে আদেশের ভাষায় কিছু লিখলে ম্যানেজার সেটিই মেনে নেবেন, কারণ চিরকুটটি এসেছে নিজের টেবিলে। সমাধান দর্শনার্থীকে থামানো নয়। সমাধান হলো চিরকুটটি একটি খামে ভরা, যার ওপর লেখা “এটি শুধু পড়ার জিনিস, আদেশ নয়।” Output encoding ঠিক সেই খাম।
ক্ষতি কতটা হতে পারে
একবার script চললে সে ব্যবহারকারীর নামে যা খুশি করার চেষ্টা করতে পারে, কারণ browser প্রতিটি request-এ তাঁর session নিয়েই যায়:
- ব্যবহারকারীর নামে কাজ: ঠিকানা বদল, অর্ডার, বার্তা পাঠানো, এমনকি admin হলে admin-এর কাজ।
- পাতার তথ্য পড়া: পাতায় যা দেখা যাচ্ছে তার সবই, যেমন নাম, ফোন, লেনদেন।
- ভুয়া ফর্ম দেখানো: আসল ঠিকানায় থেকেই ভুয়া login ফর্ম, তাই ঠিকানা দেখে ধরা যায় না।
- নিজে থেকে ছড়ানো: একটি account থেকে অন্য account-এ।
“Cross-site scripting” নামটি এসেছিল ২০০০ সালের দিকে, Microsoft-এর প্রকৌশলীদের কাছ থেকে; সেই ফেব্রুয়ারিতেই CERT/CC ক্ষতিকর HTML tag নিয়ে সতর্কবার্তা দেয়। ২০০৫ সালে MySpace-এ একটি stored XSS worm প্রকাশিত প্রতিবেদন অনুযায়ী প্রায় ২০ ঘণ্টায় ১০ লাখের বেশি প্রোফাইলে পৌঁছায়, আর সাইটটি সাময়িকভাবে বন্ধ রাখতে হয়। একই বছর Amit Klein “DOM Based XSS” ধারণাটি লেখেন। OWASP Top 10:2025-এ XSS আলাদা শিরোনাম নয়, এটি A05 Injection-এর ভেতরে পড়ে (পর্ব ৩৭)।
এই পর্বের সব উদাহরণ নিরীহ, আর সব ডেমো চলে খেলনা ডেটায়। অন্যের সাইটে “দেখি তো কাজ করে কি না” বলে কিছু পরীক্ষা করবেন না। লিখিত অনুমতি ছাড়া এটি অপরাধ হতে পারে। সীমারেখাটি বিস্তারিত আছে পর্ব ৬ · আইন আর নৈতিকতা-য়। এখানে উদ্দেশ্য একটিই: নিজের app কীভাবে রক্ষা করবেন।
২ · তিন ধরন: ডেটা কোন পথে পাতায় আসে
পার্থক্যটি হামলার কৌশলে নয়, ডেটার পথে। পথ জানলে বোঝা যায় ত্রুটি খুঁজতে হবে কোথায়।
| ধরন | ডেটার পথ | কে আক্রান্ত | ত্রুটি কোথায় খুঁজবেন |
|---|---|---|---|
| Reflected | Request-এর ডেটা সেই response-এই ফিরে আসে, যেমন “আপনি খুঁজেছেন: …” | যিনি তৈরি করা link খোলেন | Server-এর template |
| Stored | ডেটা database-এ জমে, পরে সবার পাতায় দেখা যায় | পাতাটি যিনিই খোলেন, admin-সহ | Server-এর template বা client render |
| DOM-based | পাতার নিজের JavaScript ডেটা পড়ে অনিরাপদভাবে DOM-এ বসায় | যিনি সেই পাতা খোলেন | Browser-এ চলা JavaScript |
Stored ধরনটি সাধারণত সবচেয়ে বিপজ্জনক, কারণ ভুক্তভোগীকে কোনো link খুলতে হয় না। একবার জমলে সেটি বারবার চলে।
শ্রেণিবিভাগ নিয়ে একটু মতভেদ আছে। OWASP-এর “Types of XSS” পাতা জানায়, এই তিনটি একে অপরকে ছাপিয়ে যায়; একটি ত্রুটি একই সাথে stored আর DOM-based হতে পারে। তাই ২০১২ সালের দিকে Server XSS আর Client XSS নাম দুটি প্রস্তাব করা হয়: ত্রুটিটি server-এর কোডে, না browser-এর কোডে। এই ভাগটি প্রতিকার ঠিক করতে বেশি কাজের। পরীক্ষায় তিন ধরনের নামই বেশি আসে, দুটোই জেনে রাখুন।
৩ · মূল কারণ: context না বুঝে ডেটা বসানো
সব ধরনের XSS-এর মূলে একই কথা: অবিশ্বস্ত ডেটা এমন জায়গায় বসেছে, যেখানে browser তাকে কোড হিসেবে পড়তে পারে, আর বসানোর আগে সেই জায়গার নিয়মে encode করা হয়নি। এই “জায়গা”-কেই বলে encoding context.
অনিরাপদ নকশাটি দেখতে এমন। এখানে <user-input> একটি নিরীহ চিহ্ন, আসল কোনো ডেটা নয়:
// অনিরাপদ নকশা: ডেটা জুড়ে HTML বানানো হচ্ছে
card.innerHTML = "<p>" + "<user-input>" + "</p>";
// নিরাপদ নকশা: ডেটা শুধু লেখা হিসেবে বসছে
const p = document.createElement("p");
p.textContent = "<user-input>";
card.append(p);প্রথম লাইনে কোড আর ডেটা একই string-এ মিশে গেছে, তাই ডেটা পাতার কাঠামো বদলে দিতে পারে। অংশ ৬-এ এর পার্থক্য চালিয়ে দেখবেন। পর্ব ৩৯ · Injection প্রতিরক্ষা-র কথাও মনে পড়তে পারে: SQL-এ parameterized query যেমন কাঠামো আর ডেটা আলাদা রাখে, এখানেও একই নীতি।
ডেটার পথ আর প্রতিটি ধাপের প্রতিরক্ষা
Data flow, trust boundary and defensive layers
ছবিতে একটি জিনিস খেয়াল করুন: Database-এ মূল ডেটা অক্ষত থাকে। Encoding হয় দেখানোর ঠিক আগে। কারণ জমা করার সময় আপনি জানেন না ডেটাটি পরে HTML-এ যাবে, PDF-এ, SMS-এ না mobile app-এ। নিচের ডায়াগ্রামে একটি মন্তব্যের পুরো যাত্রা দেখুন।
- ক্রেতালিখলেন
- Serverযাচাই
- Databaseমূল লেখা
- Templateencode
- Browserলেখা দেখায়
<b>hello</b> & "welcome"
length = 24 (সীমা 500, ঠিক আছে) type = text (ঠিক আছে) মান একই আছে, কিছু কাটা হয়নি
id text
17 <b>hello</b> & "welcome"
ছাঁচ: <p>{{ comment.text }}</p> ফল: <p><b>hello</b> & "welcome"</p>
p
└─ text: <b>hello</b> & "welcome"
b নামের কোনো element তৈরি হয়নি
<b>hello</b> & "welcome"
হুবহু যা লেখা হয়েছিল, মোটা হরফ নেই
- ধাপ ১ · লেখা হলো। ক্রেতা লিখলেন
<b>hello</b> & "welcome". এখানেbশুধু মোটা হরফের tag, নিরীহ। তবু এটি দেখায়, tag-এর মতো দেখতে ডেটা browser-এ কী করে। - ধাপ ২ · Server যাচাই করল। দৈর্ঘ্য ২৪ অক্ষর, সীমার ভেতরে। অক্ষরগুলো কেটে ফেলা হয়নি। কেউ গণিতের মন্তব্যে “a < b” লিখতেই পারেন।
- ধাপ ৩ · Database-এ জমা। Parameterized query দিয়ে মূল লেখাটি হুবহু জমা হলো, id 17. জমা করার সময় HTML encoding করা হয় না।
- ধাপ ৪ · Template encode করল। ডেটা বসছে HTML লেখার context-এ, তাই
<,>,&,"নিরীহ রূপ পেল। Encoding ঘটেছে দেখানোর ঠিক আগে। - ধাপ ৫ · Browser parse করল। Browser entity-গুলোকে অক্ষরে ফেরাল, কিন্তু সেগুলোকে tag ভাবল না। DOM-এ শুধু একটি text node, কোনো
belement নেই। - ধাপ ৬ · পাঠক দেখলেন। পর্দায় ঠিক সেই লেখা, যা ক্রেতা লিখেছিলেন। ডেটা নষ্ট হয়নি, আবার কোড হয়েও ওঠেনি। এটাই সফল output encoding.
৪ · প্রতিটি context-এর নিজের নিয়ম
Output encoding মানে বিশেষ অক্ষরকে নিরীহ রূপে লেখা। HTML-এ এই রূপকে বলে HTML entity. এটি Base64-এর মতো কোনো গোপন করার কাজ নয়; উদ্দেশ্য শুধু browser-কে বলা, “এটি লেখা”। OWASP-এর Cross Site Scripting Prevention Cheat Sheet-এ HTML লেখার জন্য পাঁচটি অক্ষরের নিয়ম আছে:
| অক্ষর | যে রূপে লেখা হয় | কেন বিপজ্জনক |
|---|---|---|
& | & | Entity-র শুরু। এটিই সবার আগে সামলাতে হয় |
< | < | Tag-এর শুরু |
> | > | Tag-এর শেষ |
" | " | Attribute-এর মান শেষ করে দিতে পারে |
' | ' | একক উদ্ধৃতির attribute শেষ করে দিতে পারে |
কিন্তু একই encoding সব জায়গায় চলে না। পোস্ট অফিসে দেশের ভেতরের ঠিকানা আর বিদেশের ঠিকানার নিয়ম আলাদা; ডেটার ক্ষেত্রেও জায়গা বদলালে নিয়ম বদলায়।
| Context | নিয়ম | মনে রাখুন |
|---|---|---|
| HTML লেখা | HTML entity encoding | পাঁচটি অক্ষরই |
| HTML attribute | Attribute encoder, মান সবসময় উদ্ধৃতির ভেতরে | উদ্ধৃতি ছাড়া attribute অনিরাপদ। URL যাচাইয়ের কাজ এটি করে না |
| URL-এর parameter | Percent-encoding, শুধু মানটুকু | পুরো URL নয়। href-এ বসলে আগে URL, পরে attribute encoding |
| JavaScript | সরাসরি না বসানোই ভালো | ডেটা data-* attribute বা JSON হিসেবে রেখে script থেকে পড়ুন |
| CSS | নির্দিষ্ট property, allowlist করা মান | ব্যবহারকারীর string সরাসরি নয় |
| Script block, HTML comment, tag বা attribute-এর নাম | অবিশ্বস্ত ডেটা একেবারেই নয় | এখানে নিরাপদ encoding নেই বলেই ধরুন |
নিচের খেলনা encoder একই নিরীহ লেখাকে তিন জায়গার নিয়মে encode করে। কোনো request যাচ্ছে না, শুধু string বদলাচ্ছে:
// খেলনা encoder: বাস্তবে framework-এর পরীক্ষিত encoder ব্যবহার করুন
const sample = `<b>hello</b> & "welcome" 'friend'`;
const MAP = { "&": "&", "<": "<", ">": ">",
'"': """, "'": "'" };
const encodeHtml = s => s.replace(/[&<>"']/g, c => MAP[c]);
function encodeFor(context, value) {
if (context === "html-text") return encodeHtml(value);
if (context === "attribute") return encodeHtml(value);
if (context === "url-param") return encodeURIComponent(value);
if (context === "href-attr") return encodeHtml(encodeURIComponent(value));
throw new Error("no safe encoder for context: " + context);
}
const t = encodeFor("html-text", sample);
console.log("html-text: <p>" + t + "</p>");
console.log('attribute: <input value="' + encodeFor("attribute", sample) + '">');
console.log("url-param: /search?q=" + encodeFor("url-param", sample));
console.log('href-attr: <a href="/search?q=' + encodeFor("href-attr", sample) + '">');
try {
encodeFor("script-block", sample);
} catch (e) {
console.log("REFUSED: " + e.message);
}প্রথম দুই লাইনে পাঁচটি অক্ষরই entity হয়েছে, যেমন <b>hello</b>, আর ' হয়েছে '. HTML লেখা আর attribute-এ এখানে একই encoder, তবে attribute-এ মান উদ্ধৃতির ভেতরে থাকায় তা নিরাপদ। তৃতীয় লাইনে %3Cb%3E, %20, %26 এসেছে। লক্ষ করুন, encodeURIComponent ' অক্ষরটি বদলায় না, লাইনের শেষে 'friend' অক্ষত। তাই href-এ বসানোর সময় চতুর্থ লাইনের মতো আগে URL, পরে attribute encoding লাগে; সেখানে হয়েছে 'friend'. শেষ লাইন REFUSED: no safe encoder for context: script-block: অজানা context-এ encoder না চালিয়ে থেমে যাওয়াই fail secure আচরণ।
আপনার ভাষায় তৈরি encoder
নিজে encoder লেখার দরকার নেই। ওপরের কোড শুধু বোঝার জন্য। কয়েকটি পরিচিত উদাহরণ, হুবহু নাম ব্যবহারের আগে নিজের version-এর নথি মিলিয়ে নিন:
| ভাষা | HTML-এর জন্য | URL মানের জন্য |
|---|---|---|
| Python | html.escape(s, quote=True) | urllib.parse.quote(s, safe="") |
| PHP | htmlspecialchars($s, ENT_QUOTES, 'UTF-8') | rawurlencode($s) |
| JavaScript | DOM-এ textContent | encodeURIComponent(s) |
| Java | OWASP Java Encoder-এর মতো library | URLEncoder, space-এর আচরণ দেখে |
৫ · Template engine: নিরাপদ পথটিই সহজ পথ
প্রতিটি জায়গায় হাতে encode করতে গেলে একদিন ভুল হবেই। তাই আধুনিক template engine আর framework auto-escaping করে: {{ name }} লিখলে encoding নিজে থেকেই হয়। React-এর JSX, Vue-র {{ }}, Django-র template আর Laravel Blade-এর {{ }}, সবগুলোতেই এটি স্বাভাবিক আচরণ।
কিন্তু প্রতিটি ব্যবস্থায় একটি “জরুরি দরজা” আছে, যা encoding বন্ধ করে দেয়। XSS-এর বড় অংশ এই দরজাগুলো দিয়েই আসে। আপনার code review-তে এই শব্দগুলো দেখলেই থামতে হবে:
| Framework | নিরাপদ | Encoding বন্ধ করে |
|---|---|---|
| React | {value} | dangerouslySetInnerHTML |
| Vue | {{ value }} | v-html |
| Jinja2, Django | {{ value }} | |safe |
| Angular | {{ value }} | bypassSecurityTrustHtml |
| Laravel Blade | {{ $value }} | {!! $value !!} |
Auto-escaping-এরও সীমা আছে। Jinja2 library-তে নিজে Environment বানালে autoescape সাধারণত নিজে থেকে চালু থাকে না, আলাদা করে চালু করতে হয়; Flask-এর মতো framework .html ফাইলে চালু করে দেয়। আর auto-escaping সাধারণত HTML-এর জন্য, URL-এর scheme যাচাই করে না। উদ্ধৃতি ছাড়া attribute বা script block-এর ভেতরে ডেটা বসালে এটি যথেষ্ট নয়।
৬ · Browser-এ: innerHTML, textContent, sanitizer আর link
DOM-based XSS-এর ক্ষেত্রে server কিছু করার সুযোগই পায় না, সিদ্ধান্ত নেয় পাতার JavaScript. কিছু API string-কে কোড হিসেবে পড়তে পারে। এগুলোকে বলে DOM sink. সবচেয়ে পরিচিত হলো innerHTML. আর তার নিরাপদ বিকল্প textContent, যা string-কে শুধু লেখা হিসেবে বসায়।
পার্থক্যটি একটি খেলনা DOM-এ দেখুন। এটি আসল browser parser নয়। শুধু <b> tag চেনে, আর কোনো কিছু চালায় না:
// খেলনা DOM: real browser parser নয়, শুধু ধারণার জন্য
function toyParse(html) {
const nodes = [];
let rest = html;
while (rest.length > 0) {
const m = rest.match(/^<b>(.*?)<\/b>/);
if (m) {
nodes.push({ type: "element", tag: "b", text: m[1] });
rest = rest.slice(m[0].length);
continue;
}
const next = rest.indexOf("<b>", 1);
const end = next === -1 ? rest.length : next;
nodes.push({ type: "text", text: rest.slice(0, end) });
rest = rest.slice(end);
}
return nodes;
}
const toyEl = {
children: [],
set innerHTML(v) { this.children = toyParse(v); },
set textContent(v) { this.children = [{ type: "text", text: v }]; },
};
function report(label) {
const elements = toyEl.children.filter(n => n.type === "element").length;
console.log(label, "nodes=" + toyEl.children.length, "elements=" + elements);
for (const n of toyEl.children) console.log(" ", n.type, JSON.stringify(n.text));
}
const comment = `<b>hello</b> & "welcome"`;
toyEl.innerHTML = comment; report("innerHTML-like :");
toyEl.textContent = comment; report("textContent-like:");প্রথম লাইনে nodes=2 elements=1: innerHTML-এর মতো পথে ডেটা থেকে একটি আসল b element তৈরি হয়েছে, অর্থাৎ ডেটা পাতার কাঠামো বদলে দিয়েছে। নিরীহ tag-এ শুধু হরফ মোটা হয়; কিন্তু একই পথ দিয়ে যেকোনো tag তৈরি হতে পারে, আর সেখানেই বিপদ। textContent-এর মতো পথে nodes=1 elements=0: পুরো string একটি text node, কাঠামো অক্ষত।
OWASP-এর পরামর্শ অনুযায়ী লেখা বসাতে textContent, createTextNode, insertAdjacentText ব্যবহার করুন, আর innerHTML, outerHTML, insertAdjacentHTML, document.write এড়িয়ে চলুন। String থেকে কোড চালানোর API, যেমন eval, encoding দিয়েও নিরাপদ হয় না। এগুলোতে অবিশ্বস্ত ডেটা যাবেই না। DOM-এর ভিত্তি ঝালিয়ে নিতে পারেন JavaScript কোর্সের DOM পর্বে।
ব্যবহারকারী যদি সত্যিই সাজানো লেখা দিতে চান
ব্লগ, পণ্যের বিবরণ বা email-এর editor-এ rich text লাগে। সেখানে encoding করলে মোটা হরফও লেখা হয়ে যাবে। তখন লাগে HTML sanitization: অনুমোদিত tag আর attribute রেখে বাকিটা সরানো। কাজটি শুনতে সহজ, আসলে খুব কঠিন, কারণ browser HTML পড়ে অনেক অপ্রত্যাশিতভাবে। তাই regex দিয়ে নিজে বানাবেন না। DOMPurify-র মতো নিয়মিত হালনাগাদ হওয়া library একটি পরিচিত উদাহরণ।
// উদাহরণ: DOMPurify-র মতো library, version-ভেদে option আলাদা হতে পারে
const clean = DOMPurify.sanitize("<user-input>", {
ALLOWED_TAGS: ["b", "i", "p", "ul", "ol", "li", "a"],
ALLOWED_ATTR: ["href"],
});
box.innerHTML = clean; // sanitize-এর পরে আর কিছু বদলানো যাবে না- Allowlist ছোট রাখুন: যা লাগে শুধু তা-ই।
- Sanitize করার পরে বদলাবেন না: OWASP সতর্ক করে, পরে string পাল্টালে আগের নিশ্চয়তা নষ্ট হতে পারে।
- Library হালনাগাদ রাখুন: Browser বদলায়, তাই sanitizer-এর ত্রুটিও সময়ে সময়ে পাওয়া যায়।
- Markdown হলে আরও সহজ: সাধারণত HTML পুরো বন্ধ রেখে শুধু Markdown-এর চিহ্ন অনুমোদন করা নিরাপদ।
- Browser-এর নিজস্ব পথ: MDN অনুযায়ী HTML Sanitizer API-র
setHTML()এখনো সব প্রধান browser-এ নেই, তাই আপাতত fallback রাখতে হয়।
Link-এর জন্য URL scheme allowlist
ধরুন প্রোফাইলে ব্যবহারকারী নিজের website-এর ঠিকানা দেন। আপনি সেটি encode করে href-এ বসালেন। লেখাটি নিরাপদ হলো, কিন্তু link-এর অর্থ নিরাপদ কি? Attribute encoding URL-এর URL scheme যাচাই করে না। কিছু scheme ক্লিক করলে সাইট না খুলে পাতার ভেতরেই কিছু চালাতে পারে। তাই এখানে encoding-এর পাশাপাশি allowlist লাগে: https: আর mailto: ঠিক আছে, বাকি সব বন্ধ।
Denylist দিয়ে “খারাপ” scheme আটকানোর চেষ্টা করবেন না; তালিকা কখনো পুরো হয় না। হাতে string মেলানোর বদলে আসল URL parser ব্যবহার করুন, যাতে browser যেভাবে পড়বে আপনিও সেভাবে পড়েন। নিচের কোডে blocked-scheme: একটি বানানো, অস্তিত্বহীন scheme:
// খেলনা link তালিকা: কোনো URL খোলা হচ্ছে না, শুধু parse হচ্ছে
const ALLOWED = new Set(["https:", "mailto:"]);
const BASE = "https://shop.example.com/";
function safeHref(raw) {
let url;
try {
url = new URL(raw, BASE);
} catch {
return { ok: false, why: "cannot parse" };
}
if (!ALLOWED.has(url.protocol)) {
return { ok: false, why: "scheme " + url.protocol + " not allowed" };
}
return { ok: true, href: url.href };
}
const links = [
"https://example.com/docs",
"/profile/7",
"mailto:help@example.com",
"http://example.com/old",
"blocked-scheme:do-something",
"https://",
];
for (const raw of links) {
const r = safeHref(raw);
console.log(r.ok ? "ALLOW" : "BLOCK", raw, "=>", r.ok ? r.href : r.why);
}আউটপুটে তিনটি ALLOW. আপেক্ষিক ঠিকানা /profile/7 পুরো হয়ে গেছে https://shop.example.com/profile/7, তাই নিজের সাইটের link-ও একই নিয়মে যায়। http://example.com/old এসেছে BLOCK, কারণ এখানে নীতি শুধু https:; এটি আপনার পছন্দের বিষয়। blocked-scheme:do-something পেয়েছে scheme blocked-scheme: not allowed. অসম্পূর্ণ https:// পেয়েছে cannot parse. লক্ষ করুন, কোডে “খারাপ” scheme-এর কোনো তালিকা নেই: তালিকার বাইরের সবই বন্ধ, অর্থাৎ default deny. ALLOW হওয়া URL তারপরও attribute encoding করে বসাতে হবে।
৭ · Content Security Policy: ভুল হলেও দ্বিতীয় দেয়াল
ধরা যাক সবকিছু ঠিক করেছেন, তবু লাখ লাইনের কোডে একটি ফাঁক থাকতেই পারে। CSP সেই দিনের জন্য। এটি server-এর পাঠানো একটি header, যা browser-কে বলে: “এই পাতায় কোন script চলবে।” তালিকার বাইরের script browser নিজেই চালাবে না।
একটি অফিসের অনুষ্ঠানের কথা ভাবুন। দারোয়ানের হাতে একটি পরিচয়পত্রের তালিকা না দিয়ে, সকালে একটি নতুন সংকেত দেওয়া হলো, যা শুধু আজকের জন্য। যাঁর কাছে আজকের সংকেত আছে তিনিই ঢুকবেন। কাল সংকেত আবার বদলাবে। Nonce ঠিক এই সংকেত।
যে policy-টি আজ সুপারিশ করা হয়
OWASP-এর CSP Cheat Sheet আর web.dev-এর “strict CSP” নির্দেশিকা nonce-ভিত্তিক এই ধরনের policy দিয়ে শুরু করতে বলে:
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none''nonce-…'
প্রতিটি response-এ নতুন CSP nonce. শুধু একই মান বহন করা script চলে। Inline script আর inline event attribute বন্ধ থাকে।
'strict-dynamic'
strict-dynamic থাকলে বিশ্বস্ত script যে script লোড করে, সেগুলোও চলে। তখন host-এর লম্বা তালিকা লাগে না, তা উপেক্ষিত হয়।
default-src
default-src হলো fetch directive-গুলোর fallback, যেমন img-src, style-src. আলাদা directive লিখলে সেটি আর উত্তরাধিকার নেয় না।
base-uri, object-src
base-uri, form-action, frame-ancestors fallback পায় না। আলাদা করে লিখতে হয়। object-src fallback পায়, তবু স্পষ্ট লেখা ভালো।
এবার একটি পাতা লোড হওয়ার সময় nonce কীভাবে কাজ করে দেখুন। সব মান বানানো, example-nonce-Q1 কোনো আসল nonce নয়।
- Browserrequest
- Servernonce বানায়
- CSP যাচাইমেলে?
- Reportলঙ্ঘনের খবর
GET /product/7 HTTP/1.1 Host: shop.example.com
nonce = base64(CSPRNG থেকে 16 byte)
= example-nonce-Q1 (বানানো মান)
Content-Security-Policy: script-src 'nonce-example-nonce-Q1' 'strict-dynamic'; object-src 'none'; base-uri 'none' <script nonce="example-nonce-Q1" src="/app.js"></script>
script nonce সিদ্ধান্ত
/app.js example-nonce-Q1 চলবে
script বিশ্বাসের উৎস সিদ্ধান্ত
/app.js nonce মিলেছে চলেছে
/chart.js app.js লোড করেছে চলবে
script nonce সিদ্ধান্ত
inline, ধরুন কোনো নেই বন্ধ
ফাঁক দিয়ে ঢুকেছে
report পাঠানো হলো: script-src লঙ্ঘন
আগের nonce example-nonce-Q1 আর কোনো কাজের নয় নতুন nonce example-nonce-Z9 (বানানো মান)
- ধাপ ১ · Request. Browser পণ্যের পাতা চাইল:
/product/7. এখনো কোনো nonce নেই। - ধাপ ২ · Nonce তৈরি। Server এই একটি response-এর জন্য নিরাপদ random generator থেকে ১৬ byte, মানে ১২৮ bit নিল। এখানে দেখানো
example-nonce-Q1বানানো মান। - ধাপ ৩ · দুই জায়গায় একই মান। Header-এ আর নিজের script tag-এ, template দিয়ে, একই nonce বসল। ব্যবহারকারীর ডেটা তো encode হয়েই বসেছে, তাই সেখানে কোনো nonce যায় না।
- ধাপ ৪ · app.js মিলল। Browser দেখল tag-এর
example-nonce-Q1আর header-এর মান হুবহু এক। তাই/app.jsচলবে। - ধাপ ৫ · strict-dynamic.
app.jsনিজে/chart.jsলোড করল। এই tag-এ nonce নেই, তবু বিশ্বস্ত script লোড করেছে বলে এটি চলবে। - ধাপ ৬ · অচেনা script বন্ধ। ধরুন কোনো encoding ভুলে একটি inline script পাতায় ঢুকে গেছে। তার nonce নেই, আর আক্রমণকারী আগে থেকে এই মানও জানে না। Browser থামাল আর report পাঠাল।
- ধাপ ৭ · পরের লোডে নতুন মান। পরের response-এ nonce হলো
example-nonce-Z9. আগেরexample-nonce-Q1আর চলে না। স্থির nonce তাই কোনো নিরাপত্তাই দেয় না।
Nonce-এর হিসাব
১৬ byte = ১৬ × ৮ = ১২৮ bit. Base64-এ প্রতি ৩ byte-এ ৪ অক্ষর, তাই ১৬ byte-এ ⌈১৬/৩⌉ × ৪ = ২৪ অক্ষর (শেষে দুটি = সহ)। নিচের কোড আপনার server-এর জন্য একটি উদাহরণ:
import base64, secrets
nonce = base64.b64encode(secrets.token_bytes(16)).decode()
headers["Content-Security-Policy"] = (
f"script-src 'nonce-{nonce}' 'strict-dynamic'; "
"object-src 'none'; base-uri 'none'"
)দুটি প্রচলিত ভুল। এক, পুরো HTML-এর সব script tag-এ middleware দিয়ে nonce বসানো। OWASP সতর্ক করে: তাহলে পাতায় ঢুকে পড়া script-ও বৈধ nonce পেয়ে যাবে। Nonce বসাতে হবে template-এ, নিজের script tag-এ। দুই, nonce-ওয়ালা পাতা CDN-এ cache করা; তাহলে সবাই একই nonce পাবেন। এমন পাতায় cache বন্ধ রাখুন বা hash-ভিত্তিক CSP ভাবুন।
নিজের policy পরীক্ষা করুন
নিচের খেলনা evaluator একটি policy পড়ে সাধারণ দুর্বলতা জানায়। এটি শেখার জন্য, বাস্তবে Google-এর CSP Evaluator-এর মতো সরঞ্জামও দেখতে পারেন:
// খেলনা evaluator: browser-এর পুরো নিয়ম নয়, শুধু ধারণার জন্য
function parse(header) {
const map = {};
for (const part of header.split(";")) {
const [name, ...values] = part.trim().split(/\s+/);
if (name) map[name] = values;
}
return map;
}
function evaluate(header) {
const p = parse(header);
const s = p["script-src"] || p["default-src"];
if (!s) return ["FAIL no script-src or default-src"];
const out = [];
const nonce = s.some(v => v.startsWith("'nonce-") || v.startsWith("'sha256-"));
if (s.includes("'unsafe-inline'") && !nonce) out.push("FAIL unsafe-inline with no nonce");
if (s.includes("'unsafe-eval'")) out.push("WARN unsafe-eval");
if (s.includes("*") && !nonce) out.push("FAIL wildcard source");
if (!nonce) out.push("WARN allowlist only, no nonce or hash");
if (!p["object-src"] && !p["default-src"]) out.push("WARN no object-src");
if (!p["base-uri"]) out.push("WARN no base-uri");
return out.length ? out : ["PASS"];
}
const policies = {
A: "img-src 'self'",
B: "default-src 'self'",
C: "script-src 'self' 'unsafe-inline' *; object-src 'none'",
D: "script-src 'nonce-example1' 'strict-dynamic'; object-src 'none'; base-uri 'none'",
};
for (const [k, v] of Object.entries(policies)) console.log(k, evaluate(v).join(" | "));Policy A পেয়েছে FAIL no script-src or default-src: ছবি সীমিত, কিন্তু script-এর কোনো নিয়ম নেই। B-তে WARN allowlist only, no nonce or hash | WARN no base-uri: ভালো শুরু, তবে default-src থেকে base-uri আসে না। C সবচেয়ে বিপজ্জনক: FAIL unsafe-inline with no nonce আর FAIL wildcard source, অর্থাৎ header আছে কিন্তু সুরক্ষা প্রায় নেই। D পেয়েছে PASS. তবে খেলনা PASS মানে শুধু এই কয়েকটি নিয়ম পার হয়েছে; nonce সত্যিই প্রতিবার নতুন কি না, পাতা দেখে বলতে হবে।
CSP যা থামায় না
- ত্রুটিটি সারায় না: OWASP-এর ভাষায় CSP প্রতিরক্ষার বাড়তি স্তর, প্রধান প্রতিরক্ষা নয়। Encoding-এর ভুল থেকেই যায়।
- Script ছাড়া HTML injection: ভুয়া লেখা বা বিভ্রান্তিকর link পাতায় ঢুকলে script-এর নিয়ম তা আটকায় না।
- DOM XSS-এর একটি অংশ: বিশ্বস্ত script নিজেই অনিরাপদ sink-এ ডেটা দিলে strict CSP সব ক্ষেত্রে থামাতে পারে না।
- অন্য শ্রেণির ত্রুটি: SQL injection, broken access control, CSRF, business logic ভুল।
- বিশ্বস্ত script নিজেই বদলে গেলে: nonce দিয়ে দেওয়া third-party script আপস হলে সেটিও চলবে। এর জন্য আলাদা করে ভাবতে হয় Subresource Integrity আর vendor ঝুঁকি নিয়ে।
Trusted Types সম্পর্কে একটি কথা: CSP-তে require-trusted-types-for 'script' দিলে innerHTML-এর মতো sink সাধারণ string নেয় না, শুধু আপনার নির্দিষ্ট policy-র দেওয়া object নেয়। ফলে DOM XSS-এর জায়গাগুলো কয়েকটি policy-তে সীমিত হয়, review সহজ হয়। MDN এটিকে Baseline 2026, “newly available” বলছে, তাই পুরোনো browser-এ না-ও চলতে পারে।
৮ · Report-only rollout: কিছু না ভেঙে চালু করা
পুরোনো সাইটে হঠাৎ কড়া CSP দিলে চেকআউট পাতা, chart, বিজ্ঞাপন সব বন্ধ হতে পারে। তাই আছে CSP Report-Only: Content-Security-Policy-Report-Only header কিছুই আটকায় না, শুধু জানায় কী আটকাত। মহড়ার মতো: দরজা খোলা, কিন্তু খাতায় লেখা হচ্ছে কে কে আটকে যেত। লক্ষ রাখুন, <meta> tag-এ report-only চলে না, এটি HTTP header হিসেবেই পাঠাতে হয়।
Reporting-Endpoints: csp-endpoint="https://shop.example.com/csp-reports"
Content-Security-Policy-Report-Only:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none'; base-uri 'none';
report-to csp-endpointপুরোনো browser-এর জন্য অনেকে report-uri-ও সাথে রাখেন। নিচে কাল্পনিক বাজারের ছয় সপ্তাহের rollout দেখুন। সংখ্যাগুলো বানানো, শুধু পদ্ধতি বোঝার জন্য।
1send Report-Only header2add nonce to own scripts3load vendors via app.js4filter extension noise5switch to enforcing header6review new reports weekly
| সপ্তাহ | |
| header | |
| নিজের script | |
| vendor script | |
| extension noise | |
| মোট |
- সপ্তাহ ১ · শুধু দেখা। Report-Only চালু, কিছুই আটকায় না। মোট ১,২৪০ report: নিজের script ৯০০, vendor ৩০০, browser extension-এর শব্দ ৪০.
- সপ্তাহ ২ · নিজের কোড ঠিক। Inline script বাইরের ফাইলে গেল বা nonce পেল, inline attribute বদলে
addEventListenerহলো। নিজের ৯০০ থেকে ০, মোট ৩৪০. - সপ্তাহ ৩ · Vendor ঠিক। Vendor script এখন বিশ্বস্ত
app.jsথেকে লোড হয়,strict-dynamicসেগুলোকে চালায়। Vendor ৩০০ থেকে ১২, মোট ৫২. - সপ্তাহ ৪ · শুধু শব্দ বাকি। Vendor ০। বাকি ৪০ report ব্যবহারকারীদের extension থেকে, আপনার সাইটের ত্রুটি নয়। এগুলো ছেঁকে রাখা হলো।
- সপ্তাহ ৫ · Enforce. Header বদলে
Content-Security-Policyহলো। এখন সত্যিই আটকাবে। মোট report ৪০, সবই সেই শব্দ, নতুন কিছু ভাঙেনি। - সপ্তাহ ৬ · নজর চলতে থাকে। নতুন একটি vendor widget থেকে ৪টি report এলো, মোট ৪৪. এগুলো অবহেলা নয়, যাচাই করতে হবে: নিজের নতুন কাজ, না অপ্রত্যাশিত script?
Report-এর ভেতরে থাকে কোন পাতা, কোন directive, কোন উৎস। নিচে একটি নমুনা, মানগুলো বানানো, আর ক্ষেত্রের নাম ব্যবহৃত পদ্ধতি অনুযায়ী (report-uri বা report-to) আলাদা হতে পারে:
{
"document-uri": "https://shop.example.com/product/7",
"effective-directive": "script-src-elem",
"blocked-uri": "https://cdn.example.net/widget.js",
"disposition": "report"
}Report endpoint নিজেও ইন্টারনেটের দিকে খোলা। তাই এতে rate limit দিন, আর report-এর লেখা dashboard-এ encode করে দেখান। Report-ও অবিশ্বস্ত ডেটা।
৯ · ক্ষতি সীমিত করা, নিজের app পরীক্ষা আর সাড়া
Session cookie-তে HttpOnly দিলে পাতার JavaScript সেটি পড়তে পারে না। ফলে XSS হলেও session ID চুরি করে অন্য যন্ত্র থেকে ব্যবহারের পথটি বন্ধ হয়। বিস্তারিত আছে পর্ব ১৭ · Session আর token-এ।
Set-Cookie: __Host-sid=example-token; Path=/; Secure; HttpOnly; SameSite=Laxকিন্তু সীমাটি স্পষ্ট করে বুঝুন। HttpOnly cookie-টি পড়া আটকায়, ব্যবহার নয়। পাতার ভেতরে চলা script request পাঠালে browser নিজেই cookie জুড়ে দেয়। তাই ব্যবহারকারী পাতা খোলা রাখা পর্যন্ত script তাঁর নামে কাজ করতে পারে। সংবেদনশীল কাজে, যেমন টাকা পাঠানো বা password বদলে, আবার পরিচয় যাচাই এই ফাঁক অনেকটা কমায়। Token localStorage-এ রাখলে এই সুরক্ষাটুকুও থাকে না, কারণ script সেটি পড়তে পারে।
| স্তর | কী করে | কী করে না |
|---|---|---|
| Input যাচাই | ফোন, তারিখ, id-র মতো ক্ষেত্রে আবর্জনা কমায় | মুক্ত লেখার XSS থামায় না |
| Output encoding | ডেটাকে কোড হতে দেয় না | URL scheme যাচাই করে না |
| CSP | Encoding ভুল হলেও script চলা আটকায় | ত্রুটি সারায় না, সব DOM XSS ধরে না |
| HttpOnly | Cookie পড়া আটকায় | পাতার ভেতর থেকে কাজ করা আটকায় না |
| WAF | কিছু পরিচিত নকশার সাময়িক ঢাল | OWASP XSS ঠেকাতে এর ওপর ভরসার সুপারিশ করে না; DOM XSS সে দেখেই না |
WAF নিয়ে আরও জানতে দেখুন CDN, WAF আর edge. পুরোনো X-XSS-Protection header-এর ওপরও ভরসা করবেন না; আধুনিক browser-এ সেই ফিল্টার আর নেই।
নিজের app পরীক্ষা
এখানে “নিজের app” কথাটি খুব গুরুত্বপূর্ণ। পরীক্ষা করুন শুধু নিজের development পরিবেশে, বা পর্ব ৬-এ বলা লিখিত অনুমতি নিয়ে।
Unit test: নিরীহ চিহ্ন দিয়ে
একটি নিরীহ <b> tag পাঠিয়ে দেখুন পাতায় সেটি encode হয়ে আসছে কি না। মোটা হরফ দেখা গেলে বুঝবেন encoding হচ্ছে না, আর কোনো ক্ষতিকর কিছুও চালাতে হলো না।
# উদাহরণ: নিজের app-এর test, client আপনার test framework-এর
def test_comment_is_encoded(client):
client.post("/comments", data={"text": "<b>marker-7</b>"})
page = client.get("/comments").text
assert "<b>marker-7</b>" in page
assert "<b>marker-7</b>" not in pageনিজের কোডে বিপজ্জনক sink খোঁজা
grep -rnE "innerHTML|insertAdjacentHTML|dangerouslySetInnerHTML|v-html|\|safe" src/প্রতিটি ফলাফল ভুল নয়, কিন্তু প্রতিটির পক্ষে একটি কারণ থাকা চাই। এই কাজটিই CI-তে SAST নিয়ম হয়ে চলতে পারে, যা Semgrep বা ESLint-এর মতো সরঞ্জামের নিরাপত্তা-নিয়মে পাওয়া যায়। আর নিজের staging পরিবেশে OWASP ZAP-এর মতো DAST সরঞ্জাম চালানো যায়, অনুমতিসহ। বিস্তারিত পর্ব ৪৯ · নিরাপত্তা পরীক্ষা-য়। Code review-তে “ডেটাটি কোথায় বসছে?” প্রশ্নটি একটি অভ্যাস করে নিন।
Detection
- CSP report: Enforce চালুর পরে হঠাৎ অচেনা report বেড়ে যাওয়া একটি সংকেত।
- অস্বাভাবিক account আচরণ: একসাথে অনেক account-এ একই বদল, যা আগের worm-ঘটনাগুলোর মতো ছড়ানোর লক্ষণ হতে পারে।
- Log-এ ডেটা দেখা: Admin panel-ও encode করে দেখাক; নিরাপত্তার log পড়ার পাতাও XSS-এর লক্ষ্য হতে পারে।
ঘটনা ঘটলে সাড়া
- থামান: আক্রান্ত feature সাময়িক বন্ধ করুন।
- মূল কারণ সারান: নির্দিষ্ট sink বা template ঠিক করুন, একই নকশা পুরো code-এ খুঁজুন।
- জমা ডেটা পরিষ্কার: Stored হলে আক্রান্ত সারিগুলো খুঁজে সংশোধন করুন।
- Session বাতিল: আক্রান্ত ব্যবহারকারীদের session শেষ করুন, প্রয়োজনে পরিচয় পুনরায় যাচাই।
- ক্ষতি মাপুন আর জানান: Log দেখে কারা, কী তথ্য। প্রযোজ্য আইন আর নীতি অনুযায়ী জানান।
- Regression test যোগ করুন: যাতে একই ভুল ফিরে না আসে।
আরও গভীরে: ২০১৬ সালে ACM CCS-এ Weichselbaum, Spagnuolo, Lekies আর Janc-এর “CSP Is Dead, Long Live CSP!” গবেষণাপত্রে দেখা যায়, host allowlist-নির্ভর CSP-র বড় অংশ XSS থেকে কার্যত সুরক্ষা দেয় না, আর সেখান থেকেই 'strict-dynamic' প্রস্তাবিত হয়। CSP Level 3 এখনো W3C Working Draft. আরেকটি গভীর বিষয় HTML parser-এর আচরণ: parse আর serialize-এর পরে HTML বদলে যেতে পারে, আর এজন্যই sanitize করার পরে string বদলানো নিষেধ। পুরোনো browser-এর জন্য strict policy-তে https: আর 'unsafe-inline' fallback রাখা যায়; nonce থাকলে আধুনিক browser সেগুলো উপেক্ষা করে।
পরীক্ষা ও ইন্টারভিউতে:
- Stored আর reflected-এর পার্থক্য? Stored জমা থাকে আর সবার কাছে যায়; reflected সেই একই response-এ ফেরে।
- Encoding input-এ, না output-এ? Output-এ, কারণ context তখনই জানা যায়।
- HttpOnly কি XSS থামায়? না। শুধু cookie পড়া আটকায়।
- Host allowlist-এর বদলে nonce কেন? Allowlist করা domain-এর সব কিছু বিশ্বস্ত হয়ে যায়; nonce প্রতিটি response-এ নির্দিষ্ট script-কে বিশ্বাস করে।
- Report-Only কী করে? কিছু আটকায় না, শুধু কী আটকাত তা জানায়।
১০ · AI-এর যুগে XSS প্রতিরক্ষা
AI খুব দ্রুত UI কোড লেখে, আর ঠিক সেই গতিতেই অনিরাপদ sink ঢুকিয়ে দিতে পারে। CSP-র error দেখে AI-কে “ঠিক করে দাও” বললে সে প্রায়ই নিরাপত্তা ঢিলে করেই error সরায়। তাই error চলে যাওয়া মানেই সমস্যা মিটে যাওয়া নয়।
| AI-এর সাধারণ ভুল | কেন ক্ষতিকর | যা করবেন |
|---|---|---|
HTML দেখাতে dangerouslySetInnerHTML বা |safe | Encoding বন্ধ হয়ে যায় | লেখা হলে সাধারণ binding, rich text হলে sanitizer |
CSP error সারাতে 'unsafe-inline' বা * | Policy প্রায় অকেজো হয় | কারণ খুঁজে nonce বা আলাদা ফাইল |
| Regex দিয়ে হাতে বানানো sanitizer | Browser-এর parser-এর সাথে মেলে না | পরীক্ষিত library |
পুরোনো পরামর্শ, যেমন X-XSS-Protection | আজ সুরক্ষা দেয় না | OWASP আর MDN মিলিয়ে দেখা |
| বানানো CVE নম্বর বা library-র মিথ্যা option | ভুল আত্মবিশ্বাস তৈরি হয় | মূল সূত্র আর নিজের test |
| “এই কোড নিরাপদ” বলে অতি-আত্মবিশ্বাস | Context না দেখেই রায় | নিরীহ marker test চালিয়ে নিজে যাচাই |
নিজের কোড review করাতে নিচের কাঠামো কাজে লাগে। পাঠানোর আগে secret, token আর ব্যক্তিগত ডেটা সরিয়ে নিন।
AI-এর পরিবর্তন গ্রহণ করার আগে
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Input-এই বিপজ্জনক অক্ষর কেটে ফেললে তো ঝামেলা শেষ। তাহলে সবাই output encoding-এর কথা বলে কেন?
Web-এর ইতিহাসে এই চেষ্টা হয়েছে, আর ব্যর্থও হয়েছে। PHP-র পুরোনো magic quotes সুবিধাটি সব ইনপুটে স্বয়ংক্রিয়ভাবে backslash বসাত, যাতে injection কমে। ফল হলো উল্টো: ডেটা নষ্ট হতো, developer ভুল নিরাপত্তাবোধে থাকতেন, আর সবশেষে PHP 5.4-এ এটি সরিয়ে দেওয়া হয়। ব্যর্থতার কারণটি মৌলিক।
- ইনপুটের সময় context অজানা: একই লেখা পরে HTML-এ যেতে পারে, URL-এ, CSV-তে বা SMS-এ। প্রতিটির বিপজ্জনক অক্ষর আলাদা। যে অক্ষর একটিতে নিরীহ, অন্যটিতে অর্থবহ।
- বৈধ ডেটা নষ্ট হয়: “O'Brien” নামে উদ্ধৃতি আছে, গণিতের মন্তব্যে “a < b” আছে, একটি প্রোগ্রামিং ফোরামে পুরো HTML আছে। অক্ষর কেটে ফেললে আপনি ব্যবহারকারীর কথাই বদলে দিলেন।
- ডেটা সব পথে আসে না: পুরোনো সারি, import করা ফাইল, অন্য সার্ভিসের API, এগুলো আপনার ফর্মের ফিল্টার পার হয় না। Output-এ encoding থাকলে উৎস যেটাই হোক সুরক্ষা থাকে।
আরেকটি সমস্যা হলো, ফিল্টার আর browser একই ভাষায় পড়ে না। Input ফিল্টার একটি নকশা খোঁজে, কিন্তু browser-এর parser অনেক বেশি উদার, আর সময়ের সাথে বদলায়ও। তাই “খারাপ জিনিস খুঁজে বাদ দেওয়া” পদ্ধতি সবসময় পিছিয়ে থাকে। Encoding উল্টো পথে চলে: খারাপ কী তা অনুমান না করে সব ডেটাকেই লেখা হিসেবে চিহ্নিত করে। এতে ভবিষ্যতের অজানা নকশাও একই নিয়মে নিরীহ থাকে।
তাহলে কি input যাচাই অর্থহীন? একদমই না। ফোন নম্বরে শুধু অঙ্ক, তারিখে শুধু তারিখ, দৈর্ঘ্যে সীমা, এগুলো আবর্জনা কমায় আর হামলার জায়গা ছোট করে। তবে সেটি স্তর ০; মুক্ত লেখার ক্ষেত্রে এটি XSS থামাতে পারে না।
মূল উপলব্ধি: নিরাপত্তার সিদ্ধান্ত সেখানেই নিতে হয়, যেখানে সিদ্ধান্তের জন্য দরকারি তথ্য আছে। Context জানা যায় শুধু output-এর মুহূর্তে।
প্র ০২আগের CSP-তে বিশ্বস্ত domain-এর তালিকা থাকত। সেই পদ্ধতি ছেড়ে nonce-এ আসতে হলো কেন?
CSP-র মূল ধারণা ছিল সহজ: কোন কোন domain থেকে script আসতে পারবে, তার তালিকা। Mozilla-র উদ্যোগে শুরু হওয়া এই ধারণা W3C-তে standard হয়ে ওঠে। কিন্তু বাস্তবে তালিকাগুলো দীর্ঘ হতে থাকল: analytics, বিজ্ঞাপন, CDN, chart library.
২০১৬ সালে Google-এর গবেষকদের “CSP Is Dead, Long Live CSP!” গবেষণাপত্র এই ধারণাকে বড় ধাক্কা দেয়। প্রায় ১৬ লাখ ৮০ হাজার host আর ২৬,০১১টি আলাদা policy বিশ্লেষণ করে তাঁরা জানান, ৯৪.৭২% আলাদা policy-তে সুরক্ষা এড়ানো সম্ভব ছিল, আর CSP ব্যবহার করা ৯৯.৩৪% host-এর policy XSS-এর বিরুদ্ধে কার্যত কোনো সুবিধা দিচ্ছিল না। Script-এর জন্য সবচেয়ে বেশি allowlist করা ১৫টি domain-এর ১৪টিতেই এমন অনিরাপদ endpoint ছিল, যা সুরক্ষাকে দুর্বল করে।
দুর্বলতাটি ধারণাগত। Domain-কে বিশ্বাস করা মানে সেই domain-এর সবকিছুকে বিশ্বাস করা, পুরোনো ফাইল আর ভুলে থাকা endpoint-সহ। দারোয়ানকে “ওই পাড়ার সবাইকে ঢুকতে দিন” বলার মতো।
Nonce প্রশ্নটিই বদলে দেয়: “এই script কোথা থেকে এসেছে?” নয়, “এই নির্দিষ্ট script tag কি আমার server এই response-এ অনুমোদন করেছে?” তবে বড় সাইটে প্রতিটি নির্ভরশীল script-এ nonce পৌঁছানো কঠিন, আর সেজন্যই 'strict-dynamic': বিশ্বাস এক জায়গা থেকে নিচে ছড়ায়। দামও আছে: nonce-এর জন্য প্রতিটি response-এ server-এর অংশগ্রহণ লাগে, তাই পুরো static পাতায় hash-ভিত্তিক CSP বিকল্প।
মূল উপলব্ধি: একটি নিয়ন্ত্রণ থাকা আর কার্যকর হওয়া আলাদা জিনিস। “আমাদের CSP আছে” যথেষ্ট নয়; পরিমাপ করে দেখতে হয় সেটি আসলে কী থামায়।
প্র ০৩HttpOnly cookie ব্যবহার করলে XSS কি অনেকটাই নিরীহ হয়ে যায়?
না, আর এই ভুল ধারণাটি বিপজ্জনক। HttpOnly এসেছিল ২০০২ সালের দিকে, Internet Explorer 6 SP1-এর সময়, session চুরি কমানোর জন্য। পরে সব প্রধান browser এটি গ্রহণ করে, আর এটি খুব কাজেরও। Session ID চুরি হলে আক্রমণকারী নিজের যন্ত্র থেকে ঘণ্টার পর ঘণ্টা ব্যবহার করতে পারে। HttpOnly এই পথটি বন্ধ করে।
কিন্তু XSS-এ cookie চুরি করা আবশ্যক নয়। Script তো ব্যবহারকারীর browser-এ, আপনার origin-এর ভেতরেই চলছে। সে সরাসরি request পাঠাতে পারে, browser cookie জুড়ে দেবে। ২০০৫ সালের MySpace worm এর ভালো উদাহরণ: প্রকাশিত বিবরণ অনুযায়ী এটি প্রোফাইল বদলে নিজে ছড়াচ্ছিল, ব্যবহারকারীর যন্ত্রের ক্ষতি করেনি। ছড়ানোর জন্য cookie চুরির দরকারই ছিল না।
HttpOnly থাকার পরও যা সম্ভব থেকে যায়:
- পাতায় দেখা সব তথ্য পড়া
- ব্যবহারকারীর নামে যেকোনো কাজের request পাঠানো
- CSRF token পাতায় থাকলে সেটিও পড়া
- ভুয়া ফর্ম দেখিয়ে password চাওয়া
আসল পার্থক্য সময় আর জায়গা। HttpOnly-র ফলে সুযোগ সীমিত হয় পাতা খোলা থাকার সময়ে, আর সেই browser-এর ভেতরে। এটি অর্থবহ লাভ, পুরো সমাধান নয়। সেজন্যই টাকা পাঠানোর মতো কাজে আবার পরিচয় যাচাই, ছোট session, আর MFA যুক্ত করা হয়। পাসকি বা localStorage token-এর আলোচনাতেও একই যুক্তি ফিরে আসে: XSS থাকলে পাতার ভেতরে সবকিছু ঝুঁকিতে।
মূল উপলব্ধি: ক্ষতি কমানোর নিয়ন্ত্রণকে কখনো প্রতিরোধের নিয়ন্ত্রণ ভাববেন না। প্রতিটি স্তর কোন নির্দিষ্ট ক্ষতি কমায়, সেটি বলতে পারাই নিরাপত্তা-চিন্তা।
প্র ০৪React কেন একটি property-র নাম রাখল dangerouslySetInnerHTML? একটি নাম কি নিরাপত্তা বদলাতে পারে?
এটি নিরাপত্তা-নকশার একটি ছোট কিন্তু গভীর শিক্ষা। Framework চাইলে এই সুবিধাটি বাদ দিতে পারত না, কারণ CMS-এর মতো জায়গায় আগে থেকে sanitize করা HTML সত্যিই দেখাতে হয়। আবার সহজ নামে রাখলে সবাই না ভেবেই ব্যবহার করত। তাই নকশাকারীরা অনিরাপদ পথটিকে ইচ্ছে করে লম্বা, অস্বস্তিকর আর চোখে পড়ার মতো বানিয়েছেন। মান হিসেবেও সাধারণ string নয়, __html নামের ক্ষেত্রসহ একটি object দিতে হয়।
এর ফলে তিনটি বাস্তব লাভ:
- Review সহজ: একটি শব্দ খুঁজলেই সব ঝুঁকির জায়গা পাওয়া যায়। সাধারণ
innerHTML-এর ক্ষেত্রেও আজকাল linter একই কাজ করে। - ডিফল্ট নিরাপদ: অলস বা তাড়াহুড়ো করা developer-ও নিরাপদ পথেই থাকেন, কারণ সেটিই সবচেয়ে কম লেখার পথ।
- দায় স্পষ্ট: যিনি এটি লিখলেন, তিনি জেনেই নিচ্ছেন যে ডেটার নিরাপত্তা এখন তাঁর হাতে।
Saltzer আর Schroeder ১৯৭৫ সালে যে “psychological acceptability” আর “fail-safe defaults” নীতির কথা বলেছিলেন, এটি তারই বাস্তব রূপ। Trusted Types একই চিন্তাকে আরও এগিয়ে নেয়: নাম দিয়ে সতর্ক করা নয়, ব্যবস্থা দিয়েই আটকে দেওয়া। Sink সাধারণ string নিতেই পারবে না, আর ঝুঁকিপূর্ণ কোড কয়েকটি নামকরা policy-তে জমা হবে।
তবু সীমা আছে। AI-এর লেখা কোডে এই শব্দগুলো “কাজ করিয়ে দেওয়ার” জন্য সহজেই চলে আসে, আর AI নামের সতর্কতায় ভয় পায় না। তাই নকশার সতর্কতার পাশে CI-তে স্বয়ংক্রিয় নিয়মও লাগে।
মূল উপলব্ধি: মানুষের ওপর সতর্ক থাকার দায় চাপানোর চেয়ে নিরাপদ পথকে সবচেয়ে সহজ পথ বানানো অনেক বেশি কার্যকর।
প্র ০৫নিজের কোড নিখুঁত হলেও পাতায় অন্য কারও script চললে কী হয়? Third-party script-এর ঝুঁকি কে সামলাবে?
একটি বাস্তব পাতায় analytics, chat widget, বিজ্ঞাপন, পেমেন্ট, A/B পরীক্ষার script থাকে। মনে রাখুন, আপনার পাতায় চলা যেকোনো script আপনার origin-এর সব ক্ষমতা পায়। অর্থাৎ অন্যের হাতে থাকা একটি ফাইল কার্যত আপনার সাইটের অংশ।
বড় একটি প্রকাশিত উদাহরণ ২০১৮ সালের British Airways ঘটনা। UK-র Information Commissioner's Office (ICO)-র ২০২০ সালের সিদ্ধান্ত অনুযায়ী, আক্রমণকারীরা ভেতরে ঢুকে পেমেন্ট পাতার JavaScript বদলে দেয়, আর গ্রাহকদের কার্ডের তথ্য বাইরে যায়। প্রতিষ্ঠানটিকে ২ কোটি পাউন্ড জরিমানা করা হয়। এটি ঠিক encoding-এর XSS নয়, কারণ বদলে গিয়েছিল একটি script ফাইল। কিন্তু ব্যবহারকারীর browser-এর দিক থেকে ফল একই: অনাকাঙ্ক্ষিত script পাতার ভেতরে চলছে। ICO-র মূল্যায়নে দুর্বলতার কথা এসেছে প্রবেশ নিয়ন্ত্রণ, MFA আর নিরাপত্তা পরীক্ষায়, অর্থাৎ শিকলের অনেক জায়গায়।
প্রতিরক্ষার স্তরগুলো:
- সংখ্যা কমানো: প্রতিটি script-এর ব্যবসায়িক কারণ থাকা চাই। সংবেদনশীল পাতায়, যেমন চেকআউটে, third-party যত কম তত ভালো।
- SRI: নির্দিষ্ট version-এর script-এর hash মেলানো। ফাইল বদলালে browser চালায় না। তবে যে vendor প্রতিনিয়ত ফাইল বদলায়, সেখানে এটি চলে না।
- নিজের সার্ভারে রাখা: অনুলিপি রেখে নিজের release প্রক্রিয়ায় হালনাগাদ করা।
- পরিবর্তন নজরে রাখা: CSP report আর script-এর hash পর্যবেক্ষণ।
- আলাদা origin-এ বিচ্ছিন্ন করা: Sandboxed iframe-এ widget রাখা।
CSP-র connect-src ডেটা কোথায় যেতে পারে তার ওপরও কিছু সীমা দিতে পারে, তবে এটি একা সম্পূর্ণ সুরক্ষা নয়। Dependency আর vendor-এর বিষয় বিস্তারিত আসবে পর্ব ৪৮ আর পর্ব ৭১-এ।
মূল উপলব্ধি: আপনার পাতায় যা চলে, দায় আপনার। “এটি vendor-এর কোড” বলে ব্যবহারকারীর ক্ষতি মুছে যায় না।
অনুশীলন
সব কাজ নিজের কোডে বা খেলনা ডেটায়। অন্যের সাইটে নয়।
-
Nonce-এর হিসাব: (ক) ১৬ byte nonce-এ কত bit, আর padding-সহ base64-এ কত অক্ষর? (খ) সেকেন্ডে ১০৯টি অনুমান ধরলে ১২৮ bit-এর পুরো মান-জগৎ পার হতে কত বছর? (গ) ৪ byte nonce হলে কত সেকেন্ড? (১ বছর = ৩.১৫৬ × ১০৭ সেকেন্ড ধরুন)
(ক) ১৬ × ৮ = ১২৮ bit. Base64: ⌈১৬/৩⌉ × ৪ = ৬ × ৪ = ২৪ অক্ষর.
(খ) ২১২৮ ≈ ৩.৪০ × ১০৩৮. ÷ ১০৯ = ৩.৪০ × ১০২৯ সেকেন্ড। ÷ ৩.১৫৬ × ১০৭ ≈ ১.০৮ × ১০২২ বছর.
(গ) ২৩২ = ৪,২৯৪,৯৬৭,২৯৬. ÷ ১০৯ ≈ ৪.৩ সেকেন্ড. তবে nonce প্রতিটি response-এ বদলায়, তাই একটি response-এর অনুমান পরেরটিতে কাজে লাগে না। তবু ছোট বা অনুমানযোগ্য nonce কখনোই ব্যবহার করবেন না।
-
Code review করুন: একজন সহকর্মীর pull request-এ আছে:
box.innerHTML = "Hi " + "<user-input>";আর headerscript-src 'self' 'unsafe-inline' *. তিনি লিখেছেন, “CSP আছে, তাই সমস্যা নেই।” আপনার মন্তব্য কী হবে, আর কী বদলাতে বলবেন?- কোড: ডেটা HTML হিসেবে parse হচ্ছে। লেখা দেখানোই উদ্দেশ্য, তাই
textContentব্যবহার করতে হবে। HTML সত্যিই দরকার হলে sanitizer. - CSP:
'unsafe-inline'আর*মিলে policy প্রায় কিছুই আটকায় না। “CSP আছে” কথাটি ভুল নিরাপত্তাবোধ। - প্রস্তাব: nonce-ভিত্তিক strict policy,
object-src 'none',base-uri 'none', আগে report-only. - নীতিগত কথা: CSP দ্বিতীয় স্তর। এটি থাকলেও প্রথম স্তরের ভুল গ্রহণযোগ্য নয়।
- Test: নিরীহ
<b>marker দিয়ে regression test যোগ করতে হবে।
- কোড: ডেটা HTML হিসেবে parse হচ্ছে। লেখা দেখানোই উদ্দেশ্য, তাই
-
আউটপুট অনুমান করুন: নিচের কোডে ভুল ক্রমে encode করা হয়েছে। কী ছাপা হবে? পাতায় এই লেখা বসালে পাঠক কী দেখবেন? ভুলটি কোথায়?
const s = "<b>"; console.log(s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&"));আউটপুট:
&lt;b&gt;. প্রথম দুই ধাপে পাওয়া<b>-এর&শেষ ধাপে আবার encode হয়েছে, মানে দুবার encoding.পাতায় পাঠক দেখবেন
<b>, অর্থাৎ অদ্ভুত লেখা। ক্ষতিকর নয়, কিন্তু ভুল। ঠিক করার দুই পথ:&সবার আগে করা, অথবা এক regex-এ একবারে সব অক্ষর বদলানো, যেমন ওপরেরencodeHtml. তবে এই ধরনের ভুলের জন্যই নিজে না লিখে পরীক্ষিত encoder ব্যবহার করা ভালো। -
নিজের কোডে খুঁজুন: নিজের একটি project-এ অংশ ৯-এর
grepকমান্ডটি চালান। ধরুন ফল এলো: (ক)price.innerHTML = fixedIcon, যেখানে মানটি কোডেই লেখা ধ্রুবক, (খ)bio.innerHTML = user.bio, (গ){{ post.body|safe }}, যেখানে body ব্যবহারকারীর লেখা rich text. কোনটি কীভাবে সামলাবেন?(ক) কম ঝুঁকি, কারণ ডেটা অবিশ্বস্ত নয়। তবু সম্ভব হলে DOM API ব্যবহার করুন, যাতে নিয়মে ব্যতিক্রম কমে।
(খ) জরুরি। Bio সাধারণ লেখা হলে
textContent. এটি stored XSS-এর ক্লাসিক জায়গা।(গ) Rich text দরকার, তাই encoding চলবে না। ছোট allowlist দিয়ে sanitizer চালিয়ে, তারপর দেখান। Link-এ URL scheme allowlist যোগ করুন। মন্তব্যে লিখে রাখুন কেন
|safeএখানে গ্রহণযোগ্য।প্রতিটি সংশোধনের সাথে একটি নিরীহ marker test যোগ করুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমপুরোনো একটি সাইটে CSP চালুর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- পাতার সব script আর inline কোডের তালিকা করা
- নিজের script-এ nonce দেওয়া, inline কোড সরানো
- Report-Only header পাঠানো
- Report পড়ে সমস্যা ঠিক করা, শব্দ ছেঁকে ফেলা
- Enforce করা header চালু করা
- নিয়মিত report পর্যবেক্ষণ চালিয়ে যাওয়া
একটি মন্তব্যের ঘরে দেওয়া লেখা database-এ জমা হয়ে পরে পাতাটি যিনিই খোলেন তাঁর browser-এ script হিসেবে চলে। এটি কোন ধরনের XSS?
- Reflected XSS
- Stored XSS
- শুধু DOM-based XSS
- CSRF
HTML-এর জন্য output encoding কখন করা সবচেয়ে সঠিক?
- ফর্ম থেকে ডেটা পাওয়ার সাথে সাথে
- Database-এ জমা করার আগে
- ডেটা পাতায় বসানোর ঠিক আগে, সেই context-এর নিয়মে
- Browser পাতা দেখানোর পরে
JavaScript-এ একটি মন্তব্য শুধু লেখা হিসেবে দেখাতে হবে, যেখানে মন্তব্যে <b>hello</b> থাকতে পারে। কোনটি নিরাপদ পছন্দ?
- element.textContent = comment
- element.innerHTML = comment
- element.insertAdjacentHTML("beforeend", comment)
- document.write(comment)
একটি CSP header: default-src 'self'. কোন directive-এর ক্ষেত্রে এই মান fallback হিসেবে কাজ করে না?
- img-src
- script-src
- style-src
- base-uri
Session cookie-তে HttpOnly দেওয়া আছে, তবু পাতায় XSS ঘটল। কোন বক্তব্যটি সঠিক?
- HttpOnly-র কারণে script চলবেই না
- Script cookie পড়তে পারবে না, কিন্তু ব্যবহারকারীর নামে request পাঠাতে পারবে
- Browser আর কোনো request-এ cookie পাঠাবে না
- পাতার সব তথ্য নিরাপদ থাকবে
Content-Security-Policy-Report-Only header পাঠালে কী ঘটে?
- লঙ্ঘনকারী script আটকে যায়, কোনো report যায় না
- Browser header-টি পুরোপুরি উপেক্ষা করে
- কিছুই আটকায় না, শুধু কী আটকাত তার report পাঠায়
- নিজে থেকেই পাতার XSS ত্রুটি সারিয়ে দেয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- XSS মানে ডেটা কোড হয়ে যাওয়া; মূল কারণ context না বুঝে ডেটা বসানো।
- প্রতিরোধের মূল output encoding: দেখানোর ঠিক আগে, সেই context-এর নিয়মে।
- Auto-escaping চালু রাখুন, লেখায়
textContent, rich text-এ sanitizer, link-এ scheme allowlist. - CSP দ্বিতীয় স্তর: nonce,
strict-dynamic, আগে report-only, পরে enforce. এটি ত্রুটি সারায় না। - HttpOnly ক্ষতি কমায়, থামায় না। পরীক্ষা শুধু নিজের app-এ বা লিখিত অনুমতিতে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪১ · CSRF আর cookie সুরক্ষা — SameSite, token আর clickjackingপরবর্তী পর্বঅন্য সাইট থেকে আসা অনাকাঙ্ক্ষিত request ঠেকানো।
- সাইবার নিরাপত্তা · কোর্স হোম৮০টি পর্বপুরো কোর্সের রোডম্যাপ।
- পর্ব ১৭ · Session আর tokenআগের পর্বCookie flag, HttpOnly আর নিরাপদ logout.
- JavaScript · Web securityপাশের ট্র্যাকDeveloper-এর চোখে same-origin policy, CORS, XSS আর CSP.
- Computer Networks · HTTPভিত্তিHeader আর response কীভাবে কাজ করে।
- Cloud & DevOps · DevSecOpsগভীরে যানSAST আর DAST-কে pipeline-এ বসানো।