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

Web-এর নিরাপত্তা মডেল — same-origin policy, CORS আর browser-এর সীমা

The Web Security Model — Same-Origin Policy, CORS and the Limits of the Browser

Taught in Bangla, this lesson explains the web's trust layers, origin versus site with URL pairs worked by hand, what same-origin policy stops and does not, cookie and storage boundaries, CORS simple and preflight requests, credentials and common misconfigurations, iframe sandbox, postMessage, third-party script risk, and a risk map of the module's next lessons, with toy origin and CORS simulators.

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

  • Web-এর বিশ্বাসের স্তর: browser, server, network আর third-party script
  • Origin আর site-এর পার্থক্য, আর URL জোড়া দেখে হাতে হিসাব করা
  • Same-origin policy ঠিক কী থামায়, আর কী থামায় না
  • Cookie আর browser storage-এর সীমা কেন আলাদা
  • CORS-এর simple আর preflight request, credentials, আর সাধারণ ভুল configuration
  • postMessage, iframe sandbox আর বাইরের script-এর ঝুঁকি সামলানো
  • এই মডিউলের ঝুঁকির মানচিত্র: পর্ব ৩৭ থেকে ৪৪ কোনটি কোথায় পড়ে

সীমারেখা: এই পর্ব রক্ষকের জন্য। সব উদাহরণ খেলনা ডেটায় চলে, কোনো আসল request যায় না। নিজের নয় এমন সাইটের CORS বা অন্য কিছু পরীক্ষা করতে লিখিত অনুমতি লাগে। অনুমতি ছাড়া প্রবেশ অপরাধ হতে পারে। পর্ব ৬-এর নিয়ম এখানেও প্রযোজ্য।

১ · Web-এ কে কাকে বিশ্বাস করে

একটি অদ্ভুত কথা ভাবুন। আপনি একটি অপরিচিত ওয়েবসাইট খুললেন। মুহূর্তের মধ্যে সেই সাইটের লেখা কোড আপনার কম্পিউটারে চলতে শুরু করল। আপনি কিছু install করেননি, কোনো অনুমতিও দেননি। অথচ পাশের tab-এ আপনার ব্যাংকের পাতা খোলা। তবু সাধারণত কিছু হয় না। কেন হয় না, সেই উত্তরই এই পর্বের বিষয়।

Web-এর নিরাপত্তা মডেল হলো কিছু নিয়ম, যা ঠিক করে কোন পক্ষ কতটা ক্ষমতা পাবে। পক্ষ মূলত চারটি। প্রত্যেকের ওপর বিশ্বাস আলাদা।

স্তরকার নিয়ন্ত্রণেআপনি কী ধরে নেবেন
Browserব্যবহারকারীরব্যবহারকারী, বা তাঁর যন্ত্রে চলা যেকোনো প্রোগ্রাম, সবকিছু বদলাতে পারে। সেখানকার কোনো যাচাই চূড়ান্ত নয়।
Networkঅন্য অনেকেরপথে যে কেউ দেখতে বা বদলাতে চাইতে পারে। HTTPS ছাড়া কিছুই নিরাপদ নয়।
Serverআপনারবিশ্বাসের একমাত্র জায়গা। প্রতিটি অনুমতি এখানেই যাচাই হবে।
Third-party scriptবাইরের প্রতিষ্ঠানেরআপনার পাতায় ঢুকে সেটি আপনার মতোই ক্ষমতা পায়। তার মালিক বদলালে ঝুঁকিও বদলায়।

Web-এর trust boundary: কে কোন পাশে

Browser, network, server and third-party scripts

Web-এর বিশ্বাসের স্তর আর trust boundary Browser: ব্যবহারকারীর এলাকা shop.example.com-এর পাতা আপনার script বাইরের script একই ক্ষমতা অন্য origin এর পাতা পড়তে মানা Network: HTTPS দিয়ে সুরক্ষা trust boundary Server: বিশ্বাসের জায়গা ইনপুট যাচাই authentication authorization Database
ছবি ৩৬.১ — দুটি সীমা আছে। Origin-এর দেয়াল browser-এর ভেতরের পাতাগুলোকে আলাদা রাখে। Trust boundary browser থেকে server-কে আলাদা করে। বাইরের script প্রথম দেয়ালের ভেতরেই বসে থাকে।

Client থেকে আসা সবকিছুই অবিশ্বস্ত

Form-এ required লেখা, JavaScript-এ দাম হিসাব করা, বোতাম লুকিয়ে রাখা: এসবই ব্যবহারকারীর যন্ত্রে ঘটে। ব্যবহারকারী developer tools খুলে সেগুলো বদলাতে পারেন। Browser ছাড়াই সরাসরি request পাঠাতে পারেন। তাই client-side validation ভালো অভিজ্ঞতার জন্য দরকারি, কিন্তু নিরাপত্তা হিসেবে এর মূল্য শূন্য।

একটি মিষ্টির দোকানের কথা ভাবুন। ক্রেতা নিজের হাতে লেখা ফর্দ দিলেন: “এক কেজি রসগোল্লা, দাম ৫০ টাকা”। দোকানদার ফর্দের দাম মেনে নিলে দোকান চলবে না। তিনি পণ্যটুকু পড়বেন, দাম নেবেন নিজের তালিকা থেকে। Server-ও তাই করবে। Client বলবে কী চায়। দাম, অনুমতি আর পরিচয়ের সিদ্ধান্ত নেবে server নিজের তথ্য দেখে।

এই নিয়মের মূলে আছে Saltzer ও Schroeder (১৯৭৫)-এর complete mediation: প্রতিটি প্রবেশে অনুমতি যাচাই (পর্ব ৩)। Client-এর দেওয়া দাম, role, user ID, hidden field, cookie-র মান, এমনকি Origin header: সবকিছু server-এর কাছে শুধু দাবি, প্রমাণ নয়।

মূল ধারণা

Browser-এর নিয়মগুলো ব্যবহারকারীকে বাঁচায়, আপনার server-কে নয়। এক সাইট যাতে আরেক সাইটের ডেটা চুরি করতে না পারে, browser সেটি দেখে। কিন্তু যিনি browser ব্যবহারই করছেন না, তাঁর ওপর এসব নিয়ম খাটে না। তাই server-এর সুরক্ষা server-কেই করতে হয়।

২ · Origin আর site: ঠিকানার নিয়ম

Browser-কে তো জানতে হবে কোন পাতা কার। সেই পরিচয়ই origin: scheme, host আর port, এই তিনটির জোড়া (RFC 6454, ২০১১)। তিনটি হুবহু মিললে দুটি URL same-origin. একটি অক্ষরও না মিললে cross-origin. Path আর query গণনায় আসে না।

https + shop.example.com + 443 = একটি origin

একটি বহুতল বাজারের কথা ভাবুন। Scheme হলো কোন গেট দিয়ে ঢোকা, host হলো কোন ভবন, port হলো ভবনের কোন দোকান। তিনটি একই হলে একই দোকান, দোকানের ভেতরের তাক (path) যা-ই হোক। Port আলাদা হলে একই ভবনের পাশের দোকান, কিন্তু অন্য মালিক ধরে নিতে হবে।

এর চেয়ে ঢিলা একটি সীমা আছে: site. Site মানে scheme আর registrable domain. shop.example.com আর api.example.com দুটি আলাদা origin, কিন্তু একই site, কারণ দুটিই example.com-এর অধীন। Port-এর এখানে ভূমিকা নেই।

“Registrable domain” কে ঠিক করে? Public Suffix List নামের একটি তালিকা। সেখানে আছে .com, .com.bd এর মতো সবার জন্য খোলা শেষাংশ। তাই a.com.bd আর b.com.bd একই site নয়। অনেক hosting সেবাও নিজেদের domain তালিকায় তোলে, যাতে দুই গ্রাহকের সাইট আলাদা site হয়।

নিচের খেলনা ফাংশন হাতের হিসাবটিই করে। সাতটি URL জোড়া মিলিয়ে দেখুন।

// Origin আর site তুলনা। খেলনা তথ্য আর খেলনা suffix তালিকা, কোনো request যায় না।
const DEFAULT_PORT = { "http:": "80", "https:": "443" };
const SUFFIXES = ["com", "org", "hosting.example"];

function originOf(u) {
  const x = new URL(u);
  return [x.protocol, x.hostname, x.port || DEFAULT_PORT[x.protocol]];
}
function sameOrigin(a, b) {
  const q = originOf(b);
  return originOf(a).every((part, i) => part === q[i]);
}
function siteOf(u) {
  const [scheme, host] = originOf(u);
  const labels = host.split(".");
  for (let i = 0; i < labels.length; i++) {          // লম্বাতম suffix আগে
    if (SUFFIXES.includes(labels.slice(i).join(".")))
      return scheme + "//" + labels.slice(Math.max(i - 1, 0)).join(".");
  }
  return scheme + "//" + host;
}
const A = "https://shop.example.com/cart";
const pairs = [
  [A, "https://shop.example.com/orders?id=7"],
  [A, "https://shop.example.com:443/"],
  [A, "http://shop.example.com/cart"],
  [A, "https://shop.example.com:8443/"],
  [A, "https://api.example.com/v1"],
  [A, "https://example.org/"],
  ["https://ali.hosting.example/", "https://rina.hosting.example/"],
];
for (const [a, b] of pairs) {
  console.log(sameOrigin(a, b) ? "same-origin " : "cross-origin",
    siteOf(a) === siteOf(b) ? "same-site " : "cross-site", b);
}
same-origin  same-site  https://shop.example.com/orders?id=7
same-origin  same-site  https://shop.example.com:443/
cross-origin cross-site http://shop.example.com/cart
cross-origin same-site  https://shop.example.com:8443/
cross-origin same-site  https://api.example.com/v1
cross-origin cross-site https://example.org/
cross-origin cross-site https://rina.hosting.example/

প্রথম দুটিতে শুধু path বদলেছে, আর :443 হলো https-এর স্বাভাবিক port. তৃতীয় সারিতে http আর https আলাদা, তাই origin আর site দুটোই আলাদা। এটি আধুনিক “schemeful” same-site হিসাব। চতুর্থ আর পঞ্চম সারিতে origin আলাদা, কিন্তু site এক। শেষ সারিটি খেয়াল করুন: hosting.example suffix তালিকায় আছে, তাই পাশাপাশি দুই গ্রাহক আলাদা site. আসল browser পুরো Public Suffix List ব্যবহার করে।

একই site হওয়া মানে একই বিশ্বাস নয়। blog.example.com কোনো পুরোনো plugin চালালে সেটি দখল হওয়ার ঝুঁকি shop.example.com-এর ঘাড়েও পড়তে পারে, বিশেষত cookie আর SameSite-এর হিসাবে। তাই ব্যবহারকারীর আপলোড করা জিনিস বা অবিশ্বস্ত কনটেন্ট আলাদা domain-এ রাখা নিরাপদ অভ্যাস।

৩ · Same-origin policy: পড়া আটকায়, পাঠানো নয়

Same-origin policy (SOP) হলো browser-এর মূল দেয়াল। এর কথা একটিই: এক origin-এর script অন্য origin-এর ডেটা পড়তে পারবে না। ১৯৯৫ সালে Netscape Navigator 2-এর সময়ে JavaScript-এর সঙ্গে সঙ্গেই এই ধারণা আসে, কারণ পাতায় কোড চললে পাশের পাতা পড়া ঠেকানোর দরকার পড়েছিল।

নবীনরা প্রায়ই ভাবেন, SOP cross-origin request-ই আটকে দেয়। তা নয়। নিচের ধাপগুলো দেখুন: request কোথায় যায়, আর আটকায় কোথায়।

  1. অন্য সাইটের পাতাother.exampletab ২
  2. BrowserOrigin যোগ করেদ্বাররক্ষী
  3. Servershop.example.comউত্তর দেয়
  4. পাতা ডেটা পায় নাerrorপড়া বন্ধ
অবস্থাদুটি tab খোলা
tab ১: https://shop.example.com   (login করা)
tab ২: https://other.example      (অপরিচিত সাইট)
tab ২-এর script চায় /account-এর তথ্য পড়তে
RequestBrowser পাঠাচ্ছে
GET /account HTTP/1.1
Host: shop.example.com
Origin: https://other.example
ServerRequest পৌঁছে গেছে
server handler চলল
request server-এ পৌঁছেছে, SOP তা থামায়নি
ResponseBrowser-এর হাতে
HTTP/1.1 200 OK
Content-Type: application/json
(Access-Control-Allow-Origin নেই)

{"name":"demo-user","balance":"demo"}
Browser-এর সিদ্ধান্তপাতার script যা দেখে
page origin : https://other.example
data origin : https://shop.example.com
CORS অনুমতি : নেই
ফল: script পায় শুধু একটি error, body নয়
শিক্ষারক্ষকের হিসাব
পড়া     : আটকেছে
পাঠানো   : আটকায়নি
কাজ বদল করা request: server-এ কাজ হয়ে যেতে পারে
প্রতিরোধ: SameSite, CSRF token (পর্ব ৪১)
  1. শুরুর অবস্থা। আপনি এক tab-এ shop.example.com-এ login করা। অন্য tab-এ একটি অপরিচিত সাইট খুললেন। তার script চায় আপনার account পাতার তথ্য।
  2. Browser request পাঠায়। Browser নিজে থেকে Origin header বসায়, যাতে server জানে কোন পাতা থেকে এসেছে। Cookie যাবে কি না, তা request-এর credentials mode আর cookie-র SameSite ঠিক করে।
  3. Request পৌঁছে গেল। সবচেয়ে গুরুত্বপূর্ণ ধাপ। SOP এখানে কিছু থামায়নি। Server-এর কোড চলল।
  4. Response ফিরল browser-এ। Response-এ Access-Control-Allow-Origin নেই। অর্থাৎ server অন্য origin-কে পড়ার অনুমতি দেয়নি।
  5. Browser পাতাকে ডেটা দিল না। দুই origin আলাদা, অনুমতিও নেই। তাই script শুধু একটি error পায়। ডেটা browser পর্যন্ত এসেছিল, কিন্তু পাতার হাতে যায়নি।
  6. শিক্ষা। পড়া বন্ধ হয়েছে, পাঠানো নয়। তাই যে request কোনো কিছু বদলায়, শুধু SOP তাকে বাঁচায় না। এই ফাঁকটির নাম CSRF, যা পর্ব ৪১-এ বিস্তারিত আসবে।
কাজCross-origin-একেন
fetch-এর response পড়াবন্ধ, যদি CORS অনুমতি না থাকেSOP-এর মূল কাজ
অন্য origin-এর iframe-এর DOM পড়াবন্ধপাতার ভেতরের তথ্য গোপন থাকে
localStorage পড়াবন্ধStorage প্রতিটি origin-এর আলাদা
Form জমা বা link-এ পাঠানোচলেWeb-এর শুরু থেকেই এভাবে চলে
<img>, <script>, <link>, <iframe> বসানোচলেবসানো যায়, ভেতরের bytes পড়া সাধারণত যায় না
বাইরের <script> চালানোচলে, আপনার পাতার ক্ষমতায়Script-এর origin নয়, পাতার origin গোনা হয়

SOP যা ঠেকায় না: XSS (আক্রমণকারীর কোড আপনার origin-এর ভেতরেই চলে, পর্ব ৪০), CSRF (পাঠানো আটকায় না, পর্ব ৪১), আর server-এর দুর্বলতা (browser ছাড়াই request আসে, পর্ব ৩৮, ৩৯ ও ৪২)।

৪ · Cookie আর storage: দুই রকম সীমা

অনেকে ধরে নেন, cookie-ও origin মেনে চলে। চলে না। Cookie এসেছিল origin-এর ধারণার আগে, তাই তার নিয়ম আলাদা (পর্ব ১৭-এ flag-গুলো পড়েছেন)।

বিষয়Origin-ভিত্তিক (localStorage)Cookie
Portআলাদা port, আলাদা জায়গাPort দেখে না
Schemehttp আর https আলাদানিজে থেকে আলাদা নয়। Secure দিলে শুধু https-এ যায়।
Hostহুবহু একই hostDomain দিলে সব subdomain-এ যায়। না দিলে শুধু ওই host-এ।
Pathদেখে নাPath আছে, কিন্তু এটি নিরাপত্তার সীমা নয়
পাঠানোScript নিজে পড়লে তবেইBrowser নিজে থেকে request-এ জুড়ে দেয়

শেষ সারিটিই আসল ঝুঁকি। Cookie হলো ambient authority: request-এর সঙ্গে নিজে থেকেই পরিচয় চলে যায়, কে পাঠাল তা না দেখেই। তাই Domain=example.com দেওয়া cookie blog.example.com-এর দুর্বল পাতাতেও পৌঁছায়। এজন্য session cookie-তে __Host- নাম আর Domain ছাড়া রাখাই ভালো শুরু। SameSite ঠিক করে, অন্য site থেকে শুরু হওয়া request-এ cookie যাবে কি না।

Browser storage-এর বিভাজন

localStorage, sessionStorage আর IndexedDB প্রতিটি origin-এর জন্য আলাদা (JavaScript কোর্সের Web Storage পর্ব)। এক origin-এর script অন্য origin-এর storage দেখতে পায় না। তবে একই origin-এর সব script, বাইরের script-সহ, সবটা দেখতে পায়। তাই long-lived token সেখানে রাখা ঝুঁকিপূর্ণ।

আধুনিক browser আরও একটি স্তর যোগ করছে: storage partitioning. একই বাইরের widget দুটি ভিন্ন সাইটে বসলে তার storage আলাদা হয়, যাতে সে দুই সাইটের মধ্যে ব্যবহারকারীকে চিনতে না পারে। Browser ভেদে এর আচরণ আলাদা, তাই নির্দিষ্ট আচরণের ওপর নির্ভর করার আগে নিজের লক্ষ্য browser-এ যাচাই করে নেওয়া ভালো।

৫ · CORS: server-এর দেওয়া নিয়ন্ত্রিত ছাড়

SOP-এর সমস্যাও আছে। আপনার পাতা shop.example.com-এ, API api.example.com-এ। দুটি আলাদা origin, তাই SOP পড়তে দেবে না। অথচ দুটোই আপনার। এখানেই CORS (Cross-Origin Resource Sharing). এটি server-কে বলতে দেয়: “এই origin আমার response পড়তে পারে।” এখন এটি WHATWG-এর Fetch Standard-এর অংশ।

অফিসের রেজিস্টার খাতা অন্য বিভাগের কেউ দেখতে পারেন না, এটি SOP. এখন হিসাব বিভাগের কর্মকর্তা লিখে দিলেন: “মানবসম্পদ বিভাগের রহিমা এই খাতা দেখতে পারবেন।” দারোয়ান এই চিঠি দেখে তাঁকে যেতে দেন। এটি CORS. চিঠিটি যিনি লেখেন তিনি server, যিনি যাচাই করেন তিনি browser. চিঠিতে “যে-ই আসুক” লিখে দিলে পুরো নিয়মই অর্থহীন।

Simple request আর preflight

পুরোনো web-ও cross-origin form পাঠাতে পারত। তাই সেই ধরনের request-এ নতুন কোনো ঝুঁকি নেই, এগুলো সরাসরি যায়। এদের বলে simple request: method GET, HEAD বা POST, শুধু safelisted header, আর Content-Type শুধু form-ধরনের তিনটির একটি (application/x-www-form-urlencoded, multipart/form-data, text/plain).

বাকি সবকিছু, যেমন PUT, DELETE, JSON body বা নিজের header, পুরোনো server আশা করত না। তাই browser আগে একটি অনুমতি চায়: preflight request. এটি OPTIONS method-এর ছোট একটি প্রশ্ন, যাতে কোনো body বা cookie যায় না। নিচে আপনার নিজের সাইটের একটি request ধাপে ধাপে দেখুন।

  1. পাতাshop.example.comPUT চায়
  2. PreflightOPTIONSঅনুমতি চাওয়া
  3. APIapi.example.comনীতি জানায়
  4. আসল requestPUTএবার যায়
পাতা যা চায়Browser শ্রেণি ঠিক করে
method       : PUT                simple নয়
Content-Type : application/json   simple নয়
credentials  : include
সিদ্ধান্ত     : আগে preflight লাগবে
Preflight requestCookie বা body নেই
OPTIONS /cart/7 HTTP/1.1
Host: api.example.com
Origin: https://shop.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type
Preflight responseServer-এর নীতি
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://shop.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600
Vary: Origin
Browser-এর হিসাবপ্রতিটি শর্ত মিলছে?
origin হুবহু মিলেছে      : হ্যাঁ
PUT অনুমোদিত            : হ্যাঁ
content-type অনুমোদিত   : হ্যাঁ
credentials অনুমোদিত    : হ্যাঁ
ফল ৬০০ সেকেন্ড মনে রাখবে (browser-এর সীমা সাপেক্ষে)
আসল requestএবার cookie-সহ
PUT /cart/7 HTTP/1.1
Host: api.example.com
Origin: https://shop.example.com
Content-Type: application/json
Cookie: __Host-sid=example-token

{"qty":2}
আসল responseআবার যাচাই
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://shop.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
পাতা এবার response পড়তে পারে
  1. Browser শ্রেণি ঠিক করে। PUT আর JSON, দুটোই simple-এর বাইরে। তাই আসল request ধরে রেখে browser আগে অনুমতি চাইবে।
  2. Preflight যায়। Browser জিজ্ঞেস করে: এই origin থেকে PUT আর content-type header পাঠানো যাবে? এই প্রশ্নে cookie যায় না।
  3. Server নীতি জানায়। নিজের allowlist-এ origin পেয়ে server সেটিই ফিরিয়ে দেয়, সঙ্গে method, header, credentials আর Max-Age: 600. Vary: Origin বলে দেয়, cache যেন origin অনুযায়ী আলাদা উত্তর রাখে।
  4. Browser মিলিয়ে দেখে। সব শর্ত মিলেছে। ফলটি ৬০০ সেকেন্ড মনে রাখা হবে, যদিও browser নিজের ছোট সীমা বসাতে পারে। একটি শর্ত না মিললে আসল request যেতই না।
  5. আসল request যায়। এবার cookie আর body সহ PUT গেল। Server এখনও নিজের authentication আর authorization যাচাই করবে, কারণ CORS সেই কাজ করে না।
  6. Response-এও একই যাচাই। Preflight পাস করলেও আসল response-এ এই header দরকার। থাকলে তবেই পাতা response পড়তে পারে।

Credentials-এর নিয়ম

Cookie-সহ request-এর ক্ষেত্রে দুটি কঠিন নিয়ম। প্রথমত, পাতাকে নিজে credentials: "include" বলতে হয়। দ্বিতীয়ত, server-কে Access-Control-Allow-Credentials: true দিতে হয়, আর Access-Control-Allow-Origin-এ হুবহু origin লিখতে হয়। এখানে * চলে না। Browser তখন response পাতাকে দেয় না। এই নিয়ম ইচ্ছাকৃত: “সবাই পড়ুক” আর “ব্যবহারকারীর পরিচয়সহ পড়ুক” একসঙ্গে থাকা বিপজ্জনক।

একটি হিসাব: ধরি একটি পাতা ঘণ্টায় একই URL-এ ১,০০০টি PUT পাঠায়, সারা ঘণ্টা জুড়ে ছড়িয়ে। Cache না থাকলে বাড়তি preflight ১,০০০টি। Max-Age: 600 হলে প্রতি ৬০০ সেকেন্ডে একটি, অর্থাৎ ৩৬০০ ÷ ৬০০ = ৬টি। এখানে ধরে নেওয়া হয়েছে browser ১০ মিনিট মনে রাখে।

৬ · CORS-এর সাধারণ ভুল configuration

CORS error দেখে অনেকে তাড়াহুড়ো করে এমন কিছু লেখেন, যা error সরিয়ে দেয়, কিন্তু দেয়ালও সরিয়ে দেয়। সবচেয়ে পরিচিত ভুল origin reflection: request-এর Origin যা-ই হোক, সেটিই ফিরিয়ে দেওয়া, সঙ্গে credentials-ও। তখন যেকোনো সাইটের পাতা লগইন করা ব্যবহারকারীর পরিচয়ে আপনার API পড়তে পারে।

নিচের খেলনা simulator তিনটি নীতি পাশাপাশি দেখায়। কোনো আসল request নেই। decide ফাংশনটি browser-এর সিদ্ধান্তের একটি সরল রূপ।

// খেলনা CORS সিদ্ধান্ত: real request নেই, শুধু ধারণার জন্য
const OK_LIST = ["https://shop.example.com"];
const METHODS = ["GET", "POST", "PUT"];
const policies = {
  reflect:   o => ({ acao: o, creds: true, methods: METHODS }),        // ভুল
  endsWith:  o => o.endsWith("example.com")                            // ভুল
    ? { acao: o, creds: true, methods: METHODS } : {},
  allowlist: o => OK_LIST.includes(o)                                  // ঠিক
    ? { acao: o, creds: true, methods: METHODS } : {},
};
const isSimple = r => ["GET", "HEAD", "POST"].includes(r.method) && !r.json;

function decide(req, policy) {
  const h = policy(req.origin);
  const match = h.acao === req.origin && (!req.creds || h.creds === true);
  if (!isSimple(req) && !(match && h.methods.includes(req.method))) {
    return "preflight failed: real request NOT sent";
  }
  const how = isSimple(req) ? "no preflight" : "preflight ok";
  return how + ": server ran, page " + (match ? "CAN read" : "cannot read");
}
const origins = ["https://shop.example.com", "https://evil.example",
                 "https://evilexample.com", "null"];
for (const [name, policy] of Object.entries(policies)) {
  console.log("--- policy:", name);
  for (const origin of origins) {
    console.log(origin.padEnd(26), decide({ method: "GET", creds: true, origin }, policy));
  }
}
console.log("--- policy: allowlist, other request types");
const evil = "https://evil.example";
const put = { method: "PUT", json: true, creds: true, origin: evil };
const form = { method: "POST", creds: true, origin: evil };
console.log("PUT + JSON ", decide(put, policies.allowlist));
console.log("simple POST", decide(form, policies.allowlist));
--- policy: reflect
https://shop.example.com   no preflight: server ran, page CAN read
https://evil.example       no preflight: server ran, page CAN read
https://evilexample.com    no preflight: server ran, page CAN read
null                       no preflight: server ran, page CAN read
--- policy: endsWith
https://shop.example.com   no preflight: server ran, page CAN read
https://evil.example       no preflight: server ran, page cannot read
https://evilexample.com    no preflight: server ran, page CAN read
null                       no preflight: server ran, page cannot read
--- policy: allowlist
https://shop.example.com   no preflight: server ran, page CAN read
https://evil.example       no preflight: server ran, page cannot read
https://evilexample.com    no preflight: server ran, page cannot read
null                       no preflight: server ran, page cannot read
--- policy: allowlist, other request types
PUT + JSON  preflight failed: real request NOT sent
simple POST no preflight: server ran, page cannot read

reflect-এ চারটি origin-ই পড়তে পারে, মানে কোনো সুরক্ষাই নেই। endsWith দেখতে ঠিক লাগে, কিন্তু evilexample.com আলাদা একটি domain, যার শেষে একই অক্ষরগুলো আছে। Allowlist-এ শুধু নির্দিষ্ট origin হুবহু মেলে। শেষ দুই সারি খেয়াল করুন: preflight লাগা PUT server-এ পৌঁছায়ইনি। কিন্তু simple POST-এর বেলায় server-এর কোড চলেছে, শুধু পাতা উত্তর পড়তে পারেনি। তাই allowlist থাকলেও CSRF প্রতিরক্ষা আলাদাভাবে লাগে।

ভুলকেন বিপজ্জনকসমাধান
Origin reflection, সঙ্গে credentialsযেকোনো সাইট পরিচয়সহ পড়তে পারেহুবহু মিলের allowlist
endsWith, includes, অসাবধান regexমিল-করা নামের অন্য domain পাস করেURL parse করে পূর্ণ origin মেলানো
Origin: null অনুমোদনSandboxed iframe-এর মতো জায়গা থেকেও null আসে, তাই এটি কোনো পরিচয় নয়null কখনো allowlist-এ নয়
সব subdomain বিশ্বাসএকটি দুর্বল subdomain পুরো API-র দরজা হয়দরকারি host-গুলো আলাদা করে লেখা
Vary: Origin ছাড়া dynamic উত্তরCache এক origin-এর উত্তর অন্যকে দিতে পারেVary: Origin রাখা
CORS-কে authentication ভাবাBrowser ছাড়া request-এ CORS খাটে নাServer-এ পরিচয় আর অনুমতি যাচাই

নিজের server-এ allowlist-এর একটি উদাহরণ নিচে। মানগুলো কাল্পনিক। Nginx-এ খালি মানের header পাঠানো হয় না, তাই তালিকার বাইরের origin কোনো CORS header পায় না।

# http block-এ: হুবহু মিললে তবেই মান বসে
map $http_origin $cors_origin {
    default                     "";
    "https://shop.example.com"  "https://shop.example.com";
}
map $cors_origin $cors_creds {
    default  "true";
    ""       "";
}

# server বা location block-এ
add_header Access-Control-Allow-Origin      $cors_origin always;
add_header Access-Control-Allow-Credentials $cors_creds always;
add_header Vary                             "Origin" always;

Detection: নিজের API-র response log বা automated test-এ দেখুন, অচেনা Origin পাঠালে Access-Control-Allow-Origin-এ সেটি ফেরত আসে কি না। অনুশীলন ৩-এ একটি নিরাপদ পরীক্ষা আছে।

৭ · Iframe, sandbox আর postMessage

Iframe হলো এক পাতার ভেতরে আরেকটি পাতা। Payment form, ভিডিও বা map প্রায়ই এভাবে বসে। ভিন্ন origin হলে SOP দুই পক্ষকে আলাদা রাখে। ঠিক এই কারণেই সংবেদনশীল অংশ iframe-এ রাখা একটি ভালো নকশা: card নম্বর থাকে payment সেবার origin-এ, দোকানের script সেখানে পৌঁছাতে পারে না।

আরও শক্ত করে আটকাতে চাইলে sandbox attribute. এটি দিলে iframe-এর সব ক্ষমতা বন্ধ থাকে: script, form, popup, top-level navigation. তার ওপর iframe পায় একটি opaque origin, যা অন্য কোনো origin-এর সঙ্গে মেলে না এমন নামহীন পরিচয়। তারপর যা দরকার, শুধু সেটুকু খুলে দেওয়া হয়। এটিই least privilege.

<!-- script চলবে, কিন্তু পাতাটি opaque origin-এ থাকবে -->
<iframe src="https://widgets.example.com/preview"
        sandbox="allow-scripts"
        title="Preview"></iframe>

একই origin-এর কনটেন্টে allow-scripts আর allow-same-origin একসঙ্গে দেবেন না। HTML standard নিজেই সতর্ক করে: তখন ভেতরের script নিজের sandbox সরিয়ে ফেলতে পারে। অবিশ্বস্ত HTML আলাদা origin থেকে পরিবেশন করাই নিরাপদ।

postMessage: দুই origin-এর চিঠি

দুই origin-কে কথা বলতে হলে postMessage ব্যবহার হয়। Barth, Jackson ও Mitchell-এর ২০০৮ সালের গবেষণা এই ধরনের frame-যোগাযোগের নিরাপত্তা নিয়ে কাজ করেছিল। এখানে দায়িত্ব দুই পক্ষের:

  • পাঠানোর সময়: দ্বিতীয় argument-এ নির্দিষ্ট target origin. "*" দিলে ভুল জায়গাতেও বার্তা পৌঁছাতে পারে।
  • পাওয়ার সময়: event.origin হুবহু মিলিয়ে দেখা, তারপর ডেটার গঠন যাচাই। বার্তার ভেতরের কিছুকে কোড বা HTML হিসেবে চালানো যাবে না।
// খেলনা message; সত্যিকারের window বা payment নেই
const PAY = "https://pay.example.com";
const TYPES = ["paid", "cancelled"];

function noCheck(ev) { return ev.data.type; }           // ভুল

function sloppy(ev) {                                   // ভুল
  return ev.origin.includes("pay.example.com") ? ev.data.type : "ignored";
}
function strict(ev) {                                   // ঠিক
  if (ev.origin !== PAY) return "ignored (origin)";
  const d = ev.data;
  if (typeof d !== "object" || !TYPES.includes(d.type)) return "ignored (shape)";
  if (!/^ord-[0-9]+$/.test(d.orderId)) return "ignored (shape)";
  return d.type + " -> ask server to confirm " + d.orderId;
}
const messages = [
  { origin: PAY, data: { type: "paid", orderId: "ord-1001" } },
  { origin: "https://evil.example", data: { type: "paid", orderId: "ord-1001" } },
  { origin: "https://pay.example.com.evil.example",
    data: { type: "paid", orderId: "ord-1001" } },
  { origin: "null", data: { type: "paid", orderId: "ord-1001" } },
  { origin: PAY, data: { type: "refund-all", orderId: "ord-1001" } },
  { origin: PAY, data: { type: "paid", orderId: "<user-input>" } },
];
for (const ev of messages) {
  console.log("from", ev.origin, "type", ev.data.type);
  console.log("  noCheck:", noCheck(ev));
  console.log("  sloppy :", sloppy(ev));
  console.log("  strict :", strict(ev));
}
from https://pay.example.com type paid
  noCheck: paid
  sloppy : paid
  strict : paid -> ask server to confirm ord-1001
from https://evil.example type paid
  noCheck: paid
  sloppy : ignored
  strict : ignored (origin)
from https://pay.example.com.evil.example type paid
  noCheck: paid
  sloppy : paid
  strict : ignored (origin)
from null type paid
  noCheck: paid
  sloppy : ignored
  strict : ignored (origin)
from https://pay.example.com type refund-all
  noCheck: refund-all
  sloppy : refund-all
  strict : ignored (shape)
from https://pay.example.com type paid
  noCheck: paid
  sloppy : paid
  strict : ignored (shape)

noCheck সবকিছু মেনে নেয়। sloppy-র ফাঁক তৃতীয় বার্তায়: pay.example.com.evil.example নামের ভেতরে বিশ্বস্ত নামটি আছে, কিন্তু আসল domain অন্য। strict প্রথমে পূর্ণ origin মেলায়, তারপর গঠন। শেষ দুই বার্তা বিশ্বস্ত origin থেকে এলেও ফেরত যায়, কারণ type আর order ID নিয়মের বাইরে। প্রথম সারির ফলটিও লক্ষ করুন: strict handler নিজে “paid” মেনে নেয় না, server-কে যাচাই করতে বলে। Browser-এর বার্তা শেষ কথা নয়।

৮ · Third-party script: পাতার পুরো ক্ষমতা

Analytics, chat widget, বিজ্ঞাপন, font loader: অনেক পাতায় বাইরের কোড চলে। Third-party script সম্পর্কে একটি কথা মনে রাখলেই হয়: <script src> দিয়ে আনা কোড নিজের origin-এ নয়, আপনার পাতার origin-এ চলে। SOP তাকে আটকায় না, কারণ browser-এর চোখে সে এখন আপনারই।

দোকানের পেছনের ঘরের চাবি আপনি একজন ঠিকাদারকে দিলেন, ক্যাশবাক্সের পাশের তাকটি মেরামত করতে। চাবিটি আলাদা করে “শুধু তাক” খোলে না, পুরো ঘর খোলে। আর ঠিকাদারের প্রতিষ্ঠান বিক্রি হয়ে গেলে একই চাবি নতুন কারও হাতে। Third-party script ঠিক এমন।

পাতাshop.example.com
widget.jscdn.widgets.example থেকে
DOM আর formনাম, ঠিকানা, card ঘর
localStorageও HttpOnly-হীন cookie
HttpOnly cookieপড়তে পারে না
Same-origin APIব্যবহারকারীর পরিচয়ে
বাইরে পাঠানোCSP দিয়ে সীমিত
প্রতিরক্ষাকম, যাচাই, আলাদা
  1. Script পাতায় এলো। ফাইলটি অন্য domain থেকে এলেও চলছে shop.example.com-এর পরিচয়ে। এই মুহূর্তে SOP-এর দেয়াল তার জন্য নেই।
  2. পাতার সবকিছু দেখতে পায়। DOM পড়া যায়, form-এ যা লেখা হচ্ছে তাও। পাতার লেখা বদলে দেওয়াও যায়, যেমন “Pay” বোতামের লক্ষ্য।
  3. Storage-ও খোলা। localStorage আর script-পাঠযোগ্য cookie তার হাতে। শুধু HttpOnly cookie সে পড়তে পারে না। এই একটি কারণেই HttpOnly এত দরকারি।
  4. পরিচয় ধার করে request পাঠায়। HttpOnly cookie পড়া না গেলেও সেটি নিজে থেকে request-এ যায়। ফলে script ব্যবহারকারীর হয়ে same-origin API ডাকতে পারে। HttpOnly চুরি ঠেকায়, ব্যবহার ঠেকায় না।
  5. তথ্য বাইরে পাঠানোর চেষ্টা। SOP পাঠানো আটকায় না, তাই ডেটা বাইরে যেতে পারে। Content Security Policy-র connect-src এই পথ অনেকটা ছোট করে, যদিও সব পথ বন্ধ করে না।
  6. প্রতিরক্ষার তিন স্তর। Script কমানো। যা থাকে তা যাচাই করা (SRI, নিজের server-এ রাখা, আপডেট review). সংবেদনশীল অংশ আলাদা করা (payment আলাদা origin-এ, widget sandboxed iframe-এ)।

বাস্তব শিক্ষা

২০২৪ সালে Sansec-এর প্রকাশিত প্রতিবেদন অনুযায়ী, polyfill.io নামের বহুল ব্যবহৃত script সেবার domain নতুন মালিকের হাতে যাওয়ার পরে সেখান থেকে ক্ষতিকর কোড পরিবেশিত হচ্ছিল। আক্রান্ত সাইটের সংখ্যা নিয়ে অনুমান ভিন্ন, প্রায়ই এক লাখের বেশি বলা হয়েছে। লক্ষণীয় হলো, সাইটগুলো নিজের কোডে কিছুই বদলায়নি। ২০১৮ সালে British Airways-এর ঘটনায় UK ICO-র প্রকাশিত প্রতিবেদন অনুযায়ী, সাইটের একটি JavaScript ফাইল বদলে দিয়ে গ্রাহকের card-এর তথ্য বাইরে পাঠানো হয়। সেখানে ফাইল পরিবর্তন শনাক্ত করার নজরদারি থাকলে ঘটনা আগে ধরা পড়তে পারত।

SRI একটি নির্দিষ্ট ফাইলের hash মিলিয়ে দেখে। ফাইল বদলালে browser সেটি চালায় না। নিচের hash মানটি কাল্পনিক। নিজের ফাইলের আসল hash বসাতে হবে।

<script src="https://cdn.widgets.example/widget-2.4.1.js"
        integrity="sha384-example-hash-value"
        crossorigin="anonymous"></script>

SRI-র সীমাও আছে: version বাঁধা ফাইলে এটি চলে, কিন্তু নিজে থেকে বদলাতে থাকা script-এ চলে না। Script-এর গণিতও সহজ। ধরি প্রতিটি বাইরের script বছরে দখল হওয়ার সম্ভাবনা ১% (অনুমিত সংখ্যা, মাপা তথ্য নয়), আর ঘটনাগুলো স্বাধীন। ২০টি script-এ অন্তত একটি দখল হওয়ার সম্ভাবনা ১ − ০.৯৯২০ ≈ ১৮.২%. ৫টিতে ≈ ৪.৯%. বাস্তবের সংখ্যা ভিন্ন হবে, কিন্তু চেহারাটা একই: প্রতিটি নতুন script মোট ঝুঁকি বাড়ায়। Dependency-র বড় ছবি আসবে পর্ব ৪৮-এ।

৯ · Browser-এর দেয়াল, secure default আর এই মডিউলের মানচিত্র

Process আর site isolation

Origin-এর দেয়াল প্রথমে ছিল শুধু একটি software নিয়ম, একই process-এর ভেতরে। ২০১৮ সালে Spectre দেখাল, CPU-র speculative execution-এর পার্শ্বপ্রতিক্রিয়া দিয়ে একই process-এর মেমরি অনুমান করা যেতে পারে। তাই Chromium site isolation চালু করে: ভিন্ন site-এর পাতা ভিন্ন OS process-এ চলে (Reis, Moshchuk ও Oskov, USENIX Security ২০১৯)। Firefox-ও পরে একই ধরনের ব্যবস্থা আনে। এখানে মনে রাখার কথা, সীমাটি site-ভিত্তিক, origin-ভিত্তিক নয়। আবার browser-এর ভেতরের দুর্বলতা হলে সব নিয়ম ভেঙে পড়তে পারে, তাই browser আপডেট রাখা নিজেই একটি প্রতিরক্ষা (পর্ব ২৯)।

HTTPS হলো ভিত্তি

Origin-এর পুরো মডেল দাঁড়িয়ে আছে একটি ধারণার ওপর: যে পাতা https://shop.example.com থেকে এসেছে, সেটি সত্যিই সেখান থেকে এসেছে। HTTPS ছাড়া পথের কেউ পাতার ভেতরে নিজের script ঢোকাতে পারে, আর সেটি আপনার origin-এর পুরো ক্ষমতা পায়। তাই বিধি সহজ: সব পাতা HTTPS-এ, সঙ্গে HSTS (পর্ব ১২)।

Secure default-এর দিকে

পুরোনো web-এর default ছিল “সবকিছু চলে”। নতুন ধারা উল্টো: ডিফল্টে বন্ধ, দরকার হলে খোলা।

SameSite

Cookie অন্য site-এর request-এ যাবে কি না। Chromium ২০২০ থেকে SameSite না লেখা cookie-কে Lax ধরে। অন্য browser-এ আচরণ ভিন্ন হতে পারে, তাই নিজে লিখে দেওয়াই ভালো।

পর্ব ৪১

Secure context

Secure context মানে HTTPS বা localhost-এর মতো নির্ভরযোগ্য জায়গা। Service worker-এর মতো শক্তিশালী API শুধু এখানেই মেলে।

W3C Secure Contexts

Permissions Policy

Permissions Policy header বলে দেয়, পাতা আর তার iframe কোন browser ক্ষমতা ব্যবহার করতে পারবে, যেমন camera বা geolocation.

পর্ব ৪৪
Set-Cookie: __Host-sid=example-token; Secure; HttpOnly; SameSite=Lax; Path=/
Strict-Transport-Security: max-age=31536000; includeSubDomains
Permissions-Policy: camera=(), microphone=(), geolocation=(self)

এই মডিউলের ঝুঁকির মানচিত্র

এই মডেল বুঝলে পরের পর্বগুলো কোথায় বসে, তা পরিষ্কার হয়। প্রতিটি ঝুঁকি কোনো না কোনো সীমার ব্যর্থতা।

পর্ববিষয়কোন সীমা বা ধারণা
৩৭OWASP Top 10পুরো ঝুঁকির তালিকা, অগ্রাধিকার সহ
৩৮Broken access controlServer-এ অনুমতি যাচাই না হওয়া, client-কে বিশ্বাস করা
৩৯Injectionডেটা আর কোডের সীমা, server-এর ভেতরে
৪০XSS আর CSPআক্রমণকারীর কোড আপনার origin-এর ভেতরে চলা, SOP অকেজো
৪১CSRF, cookie, clickjackingপাঠানো আটকায় না, cookie নিজে থেকে যায়
৪২SSRF আর file handlingServer-কে দিয়ে ভেতরের জিনিসে পৌঁছানো, browser-এর নিয়ম এখানে নেই
৪৩API নিরাপত্তাClient ছাড়াই সরাসরি API, CORS-এর সুরক্ষা অপ্রাসঙ্গিক
৪৪Web hardeningSecurity header, WAF, dependency আর নিরাপদ error

আরও গভীরে: Origin-এর আনুষ্ঠানিক সংজ্ঞা RFC 6454 (Barth, ২০১১), আর CORS-এর আচরণ এখন WHATWG Fetch Standard-এ লেখা। Spectre-পরবর্তী web-এ Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy আর Cross-Origin-Resource-Policy নামের header-গুলো পাতাকে আলাদা browsing context আর “crossOriginIsolated” অবস্থায় নিতে সাহায্য করে। পুরোনো document.domain শিথিলতা origin-এর সীমা নষ্ট করত, তাই browser-গুলো এটি ধীরে ধীরে সরিয়ে দিচ্ছে। PCI DSS v4.0 card পরিশোধের পাতার script-এর তালিকা, অনুমোদন আর পরিবর্তন শনাক্ত করতে বলে।

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

  • Origin কী? Scheme, host আর port-এর জোড়া। Path গণনায় আসে না।
  • SOP কি request পাঠানো থামায়? না। মূলত response পড়া থামায়। তাই CSRF আলাদা সমস্যা।
  • Preflight কখন হয়? Simple request-এর শর্তের বাইরে হলে, যেমন PUT, JSON বা নিজের header.
  • Credentials-সহ * চলে? না। হুবহু origin আর Allow-Credentials: true লাগে।
  • CORS কি API রক্ষা করে? না। এটি browser-এর ছাড়ের নিয়ম। API রক্ষা করে server-এর authentication আর authorization.

১০ · AI-এর যুগে web-এর নিরাপত্তা মডেল

CORS error নিয়ে AI-কে জিজ্ঞেস করলে একটি সাধারণ উত্তর আসে: Access-Control-Allow-Origin: * বসান, বা origin ফিরিয়ে দিন। এতে error যায়, ঝুঁকি আসে। AI লক্ষ্য হিসেবে “error সরানো” ধরে নেয়, “কে পড়তে পারবে” নয়। যাচাই আপনার হাতে রাখতে হবে।

AI প্রায়ই যা করেঝুঁকিযেভাবে যাচাই করবেন
Error সরাতে broad CORSযেকোনো সাইট পড়তে পারেঅচেনা Origin দিয়ে নিজের server-এ পরীক্ষা
postMessage handler-এ origin যাচাই নেই, বা "*"ভুয়া বার্তা মেনে নেওয়াকোড পড়ে হুবহু তুলনা খোঁজা
Client-এ দাম বা role যাচাইসহজেই বদলানো যায়Server-এর কোডে একই যাচাই আছে কি না দেখা
Token localStorage-এ রাখাযেকোনো script পড়তে পারেনকশা review, পর্ব ১৭-এর সিদ্ধান্ত মেলানো
নেই এমন header বা standard বানিয়ে বলা, বা অস্তিত্বহীন CVEভুল আত্মবিশ্বাসMDN, WHATWG, RFC-র মূল লেখা পড়া
গোপন key prompt-এ চলে যাওয়াফাঁস হওয়ার ঝুঁকিআসল secret বা ব্যক্তিগত তথ্য কখনো paste না করা

নিজের প্রজেক্টের CORS আর frame নিরাপত্তা review করতে নিচের prompt কাজে দেবে। গোপন মান আগে সরিয়ে নিন।

তুমি একজন application security reviewer. আমার নিজের প্রজেক্টের CORS middleware আর postMessage handler নিচে দিলাম। গোপন মানগুলো example-token দিয়ে বদলে দিয়েছি। [এখানে কোড পেস্ট করুন] ১. Origin কীভাবে যাচাই হচ্ছে? হুবহু মিল, নাকি অসাবধান string তুলনা? ২. Credentials চালু থাকলে কোন কোন origin ব্যবহারকারীর ডেটা পড়তে পারবে? ৩. Vary: Origin আছে কি? null origin কীভাবে সামলানো হচ্ছে? ৪. কোনো নিরাপত্তা সিদ্ধান্ত শুধু client-এ হচ্ছে কি? ৫. প্রতিটি পরামর্শের জন্য WHATWG, MDN বা RFC-র উৎসের নাম দাও। নিশ্চিত না হলে সেটি বলো। শুধু error সরাতে কোনো যাচাই বন্ধ করতে বলো না।

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

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

প্র ০১Same-origin policy পাঠানো না থামিয়ে শুধু পড়া থামায় কেন? শুরু থেকেই দুটো থামালে কি web নিরাপদ হতো না?

উত্তরটি ইতিহাসে। ১৯৯৫ সালে JavaScript আসার সময় web ইতিমধ্যেই cross-origin ছিল। এক সাইটের পাতায় অন্য সাইটের ছবি বসত, link অন্য সাইটে যেত, form অন্য server-এ জমা পড়ত। এগুলোই ছিল web-এর সৌন্দর্য, মানে যেকোনো পাতা যেকোনো পাতার সঙ্গে জুড়তে পারে। JavaScript-এর আগে এসবে ঝুঁকি কম ছিল, কারণ পাতা ফেরত আসা উত্তর পড়তে পারত না।

JavaScript যোগ হতেই নতুন ক্ষমতা এলো: কোড পাতার ভেতরের জিনিস পড়তে পারে। তাই নতুন যে ক্ষমতার ঝুঁকি, Netscape শুধু সেটুকুতেই সীমা দিল। পুরোনো আচরণ বদলালে অগণিত চালু সাইট ভেঙে যেত। Web-এর সবচেয়ে কঠিন নিয়ম এটাই: ভেঙে ফেলা যাবে না।

এর দাম হলো CSRF. পাঠানো খোলা থাকায় অন্য সাইট থেকে আপনার cookie-সহ request যেতে পারে। সে উত্তর পড়তে পারে না, কিন্তু পাঠানোই যদি টাকা সরানোর মতো কাজ করে, তবে পড়ার দরকার নেই। অনেক বছর ধরে সাইটগুলোকে CSRF token দিয়ে এই ফাঁক নিজেরা ঢাকতে হয়েছে। পরে SameSite cookie এসে সমস্যাটি অনেকটা browser-এর স্তরে নিয়ে গেছে।

CORS-এর simple request-এর নকশাতেও এই ইতিহাস দেখা যায়। যা পুরোনো form পারত, তাতে preflight লাগে না, কারণ server তা আগে থেকেই পেত। Preflight শুধু নতুন ক্ষমতার জন্য।

মূল উপলব্ধি: নিরাপত্তার নিয়ম শূন্য থেকে লেখা হয় না। সেগুলো চালু ব্যবস্থার ওপর বসানো হয়। আজকের web-এর অদ্ভুত নিয়মগুলোর বেশিরভাগই পুরোনো আচরণ রক্ষা করার দাম। তাই রক্ষককে নিয়ম মুখস্থ করলেই চলে না, ফাঁকটি কোথায় রয়ে গেছে তাও জানতে হয়।

প্র ০২“CORS চালু করলে API নিরাপদ হয়” — এই কথাটি কেন ভুল? CORS-এর আগে মানুষ কী করত?

কথাটি ভুল, কারণ CORS কোনো সুরক্ষা যোগ করে না। সুরক্ষা হলো SOP, আর CORS সেই সুরক্ষায় একটি নিয়ন্ত্রিত ছিদ্র। CORS header না থাকলে পাতা পড়তে পারে না। Header দিলে আরও বেশি পক্ষ পড়তে পারে। অর্থাৎ CORS যত যোগ করবেন, দেয়াল তত ঢিলা হবে।

আরও বড় কথা, নিয়মটি মানে browser. Server শুধু একটি header পাঠায়। curl, একটি script বা অন্য server সেটি উপেক্ষা করে। তাই CORS দিয়ে “শুধু আমার সাইট আমার API ব্যবহার করবে” বলা যায় না। Browser-এ চলা ব্যবহারকারীকে অন্য সাইট থেকে রক্ষা করাই এর লক্ষ্য। API-র সুরক্ষা আসে authentication, authorization আর rate limit থেকে (পর্ব ৪৩)।

CORS আসার আগে পাশ কাটানোর পথ ছিল। একটি জনপ্রিয় কৌশল ছিল JSONP: <script> ট্যাগ cross-origin বসানো যায়, তাই API ডেটাকে একটি function call হিসেবে ফেরত দিত। এর মানে অন্য সাইটের কোড নিজের পাতায় চালানোর সম্পূর্ণ বিশ্বাস। Flash-এর crossdomain.xml ছিল আরেকটি ব্যবস্থা, আর সেখানেও সব domain খুলে দেওয়া ছিল পরিচিত ভুল। CORS এলো এসবের একটি মানসম্মত বিকল্প হিসেবে। W3C ২০১৪ সালে একে Recommendation করে, পরে এর আচরণ WHATWG Fetch Standard-এ চলে যায়।

Developer-দের মধ্যে “CORS error” বিরক্তির উৎস, তাই সবচেয়ে সহজ পথটি প্রায়ই বেছে নেওয়া হয়। Error-টি আসলে বলছে: browser আপনার ব্যবহারকারীকে রক্ষা করছে।

মূল উপলব্ধি: CORS header একটি অনুমতিপত্র, তালা নয়। প্রতিটি নতুন origin যোগ করার সময় নিজেকে জিজ্ঞেস করুন: এই origin-এর ওপর আমি ব্যবহারকারীর ডেটা ছেড়ে দিতে রাজি কি?

প্র ০৩Spectre কেন browser-এর নকশা বদলে দিল? Software-এর একটি নিয়ম কেন যথেষ্ট ছিল না?

২০১৮ সালের শুরুতে প্রকাশিত Spectre আর Meltdown কোনো সাধারণ bug ছিল না। সেগুলো CPU-র নকশার ফাঁক। আধুনিক CPU গতির জন্য আগাম অনুমান করে কাজ চালায়, ভুল হলে ফল ফেলে দেয়। কিন্তু cache-এ সেই কাজের ছাপ থেকে যায়, আর সময় মেপে সেই ছাপ অনুমান করা যেতে পারে।

Browser-এর জন্য এর অর্থ ভয়াবহ। তখন একটি process-এর ভেতরে অনেক সাইটের পাতা থাকতে পারত। SOP ছিল code-এর একটি নিয়ম: “এই মেমরি পড়া যাবে না।” কিন্তু Spectre বলল, মেমরি একই address space-এ থাকলে সেই নিয়ম hardware-এর স্তরে পাশ কাটানো যেতে পারে। অর্থাৎ যাচাইয়ের কোড ঠিক থাকলেও তথ্য বেরিয়ে যেতে পারে।

সাময়িক ব্যবস্থা হিসেবে browser-গুলো সূক্ষ্ম timer-এর নিখুঁততা কমায় আর SharedArrayBuffer বন্ধ রাখে। স্থায়ী ব্যবস্থা ছিল গঠনগত। Chromium ২০১৮ সালে desktop-এ site isolation ব্যাপকভাবে চালু করে, যাতে ভিন্ন site-এর ডেটা ভিন্ন OS process-এ থাকে। OS-এর process-সীমা hardware-সমর্থিত, তাই একটি রক্ষার স্তর এখানে ফেরত এলো (Operating Systems কোর্সের নিরাপত্তা পর্ব)। Chromium-এর দল জানিয়েছিল, এতে উল্লেখযোগ্য মেমরি খরচ বাড়ে। এটি নিরাপত্তার জন্য দেওয়া একটি সচেতন দাম।

পরে Cross-Origin-Opener-Policy আর Cross-Origin-Embedder-Policy এলো, যাতে সাইট নিজে থেকে কঠোর আলাদা অবস্থায় যেতে পারে, আর তখন SharedArrayBuffer-এর মতো ক্ষমতা ফিরে পায়।

মূল উপলব্ধি: প্রতিটি সীমা তার নিচের স্তরের ওপর দাঁড়িয়ে। নিচের স্তর ভাঙলে ওপরের নিয়ম অর্থ হারায়। ভালো নকশায় তাই একাধিক স্তর থাকে, যা defense in depth-এর কথা। শুধু “আমার কোড ঠিক আছে” বলা যথেষ্ট নয়।

প্র ০৪একটি অনলাইন দোকানে analytics, chat আর বিজ্ঞাপনের অনেক বাইরের script আছে। সবই বাদ দেওয়া বাস্তবসম্মত না হলে নিরাপত্তা দল কী করবে?

ব্যবসার দল এই script-গুলো কারণ ছাড়া আনে না। তাই “সব বাদ” বললে কেউ শুনবে না। কাজটি শুরু হয় ঝুঁকি মাপা দিয়ে, threat modeling-এর মতো।

প্রথম ধাপ তালিকা। কোন পাতায় কোন script, কে মালিক, কেন আছে, কে অনুমোদন দিয়েছে। অনেক প্রতিষ্ঠান এই তালিকা বানাতে গিয়েই এমন script পায়, যার কথা কেউ জানে না। আর tag manager থাকলে নতুন script বিনা code review-তেই ঢুকে পড়তে পারে, তাই সেটিও নিয়ন্ত্রণে আনতে হয়।

দ্বিতীয় ধাপ পাতা ভাগ করা। Checkout আর login পাতায় প্রায় কোনো বাইরের script থাকবে না। Card-এর ঘর payment সেবার iframe-এ থাকবে, যাতে অন্য script সেখানে না পৌঁছায়। PCI DSS v4.0 এই কারণেই card পরিশোধের পাতার script-এর হিসাব রাখতে আর পরিবর্তন ধরতে বলে।

তৃতীয় ধাপ সীমা দেওয়া। CSP দিয়ে কোন domain থেকে script আসবে আর কোথায় ডেটা যাবে তা ঠিক করা। Version বাঁধা ফাইলে SRI. সম্ভব হলে ফাইলটি নিজের server থেকে পরিবেশন করা, আর আপডেট review করা। Widget-কে sandboxed iframe-এ রাখা। Session cookie-তে HttpOnly.

চতুর্থ ধাপ নজরদারি আর সাড়া। Script ফাইলের hash বদলালে সতর্কতা, CSP report পর্যবেক্ষণ। Script বন্ধ করার দ্রুত পথ থাকা উচিত। ২০২৪ সালের polyfill.io ঘটনায় যাঁদের হাতে তালিকা ছিল, তাঁরা দ্রুত বুঝতে পেরেছিলেন কোথায় হাত দিতে হবে। Vendor-এর দায় চুক্তিতে রাখা পর্ব ৭১-এর বিষয়।

মূল উপলব্ধি: নিরাপত্তা মানে সব “না” বলা নয়। কোথায় কতটা ক্ষমতা যাচ্ছে তা জানা, আর সবচেয়ে সংবেদনশীল জায়গাকে সবচেয়ে কম খোলা রাখা, এটাই আসল কাজ।

প্র ০৫Cookie-র নিয়ম origin-এর সঙ্গে মেলে না কেন? “Ambient authority” কেন web-এর অনেক সমস্যার মূলে?

Cookie তৈরি হয়েছিল ১৯৯৪ সালে Netscape-এ, Lou Montulli-র হাতে, মূলত অনলাইন কেনাকাটার ঝুড়ি মনে রাখার জন্য। তখনও JavaScript আসেনি, same-origin policy-ও নয়। Cookie-র নকশায় host আর path ছিল, port বা scheme-এর কড়া হিসাব ছিল না, আর সব subdomain-এ ভাগ করার সুবিধা ছিল। পরে RFC 2109 (১৯৯৭) থেকে RFC 6265 (২০১১) পর্যন্ত নিয়মগুলো লেখা হয়, কিন্তু চালু আচরণ বদলানো কঠিন ছিল। তাই দুটি আলাদা সীমা আজও পাশাপাশি বাঁচে।

নকশার গভীর সমস্যাটি হলো, cookie নিজে থেকে যায়। Request কে শুরু করল, browser তা না দেখে গন্তব্য দেখে পরিচয় জুড়ে দেয়। একে বলে ambient authority: ক্ষমতা পরিবেশে ছড়িয়ে আছে, ডেকে আনতে হয় না। Norm Hardy ১৯৮৮ সালে যাকে “confused deputy” সমস্যা বলেছিলেন, cookie ঠিক সেটাই তৈরি করে। Browser হলো সেই বিভ্রান্ত প্রতিনিধি: অন্য সাইটের অনুরোধে আপনার ক্ষমতা ব্যবহার করে ফেলে।

CSRF, clickjacking আর cross-site tracking, তিনটিই এই একই শিকড় থেকে আসে। এর বিপরীত নকশা হলো পরিচয়কে স্পষ্টভাবে পাঠানো, যেমন JavaScript নিজে Authorization header বসানো। তখন CSRF-এর ঝুঁকি কমে, কিন্তু token-কে script-পাঠযোগ্য জায়গায় রাখতে হয়, আর XSS-এ তা চুরি হতে পারে। তাই এখানে কোনো বিনামূল্যের সমাধান নেই (পর্ব ১৭)।

SameSite cookie মূলত ambient authority-র ওপর একটি সীমা: অন্য site থেকে শুরু হলে পরিচয় যাবে না। আর __Host- prefix cookie-কে origin-এর মতো আচরণ করতে বাধ্য করে। অর্থাৎ পুরোনো নকশাকে না ভেঙে ধীরে ধীরে আধুনিক মডেলের দিকে আনা হচ্ছে।

মূল উপলব্ধি: “নিজে থেকে হয়” এমন সুবিধা প্রায়ই নিরাপত্তার ঋণ। যখনই কোনো ক্ষমতা স্বয়ংক্রিয়ভাবে যায়, জিজ্ঞেস করুন: কে এটি ঘটাতে পারে, আর সিস্টেম তা জানতে পারে কি?

অনুশীলন

হাতে হিসাব করুন, তারপর উত্তর মিলিয়ে দেখুন। পরীক্ষা শুধু নিজের সাইটে।

  1. হিসাব: পাতা https://app.example.com:8443/home. নিচের প্রতিটির সঙ্গে origin আর site তুলনা করুন। (ক) https://app.example.com:8443/api (খ) https://app.example.com/home (গ) https://cdn.example.com:8443/x.js (ঘ) http://app.example.com:8443/home. এরপর হিসাব করুন: ১%-এর অনুমিত ঝুঁকিতে ১০টি বাইরের script-এ অন্তত একটি দখল হওয়ার সম্ভাবনা কত?

    (ক) same-origin, same-site. শুধু path আলাদা। (খ) cross-origin, same-site. Port বদলে ৪৪৩ হয়েছে। (গ) cross-origin, same-site. Host আলাদা, registrable domain এক। (ঘ) cross-origin, cross-site. Scheme আলাদা।

    সম্ভাবনা: ১ − ০.৯৯১০ ≈ ০.০৯৫৬, অর্থাৎ প্রায় ৯.৬%. ধরে নেওয়া হয়েছে script-গুলোর ঘটনা স্বাধীন, আর ১% একটি অনুমান।

  2. আউটপুট অনুমান: পাতা https://shop.example.com একটি simple GET পাঠাল https://api.example.com/me-এ, credentials: "include" সহ। Server উত্তর দিল Access-Control-Allow-Origin: * আর Access-Control-Allow-Credentials: true. (ক) Server-এর কোড চলবে কি? (খ) পাতা body পড়তে পারবে কি? (গ) সমাধান কী?

    (ক) হ্যাঁ, simple GET-এ preflight নেই, তাই server-এর কোড চলবে। (খ) না। Credentials-সহ request-এ * গ্রহণযোগ্য নয়, তাই browser পাতাকে error দেয়। (গ) *-এর বদলে হুবহু https://shop.example.com, সঙ্গে Vary: Origin. মানটি allowlist থেকে আসবে, request থেকে অন্ধভাবে নয়।

  3. নিজের সাইটে পরীক্ষা: শুধু আপনার নিজের বা লিখিত অনুমতি পাওয়া staging API-তে নিচের কমান্ডটি চালান। https://test.example-কে একটি অচেনা origin ধরে নিন। Response-এর কোন header দেখবেন, আর কী দেখলে সমস্যা?
    curl -s -o /dev/null -D - \
      -H "Origin: https://test.example" \
      https://api.example.com/v1/ping

    দেখবেন Access-Control-Allow-Origin আর Access-Control-Allow-Credentials. Header দুটি না থাকলে ভালো। Access-Control-Allow-Origin: https://test.example ফিরে এলে সেটি reflection, আর সঙ্গে credentials true থাকলে এটি গুরুতর ত্রুটি। * থাকলে দেখুন endpoint-টি সত্যিই public ডেটা দেয় কি না। এই পরীক্ষা শুধু একটি header দেখে, কারও ডেটা নয়, তবু অনুমতি ছাড়া অন্যের সিস্টেমে এটি করবেন না।

  4. নকশা ও সমালোচনা: AI একটি middleware দিল, যেখানে API request-এর Origin header হুবহু Access-Control-Allow-Origin-এ ফিরিয়ে দেয়, আর Allow-Credentials: true বসায়। পাশে মন্তব্য: “preflight সমস্যা ঠিক হবে।” Code review-তে আপনি কী বলবেন? নতুন নকশা লিখুন।

    সমস্যা: এটি origin reflection. যেকোনো সাইটের পাতা লগইন করা ব্যবহারকারীর cookie-সহ API পড়তে পারে, ঠিক যেন SOP নেই। Error গেছে, দেয়ালও গেছে।

    নকশা: (১) Config-এ নির্দিষ্ট origin-এর তালিকা, environment অনুযায়ী আলাদা। (২) হুবহু মিললে তবেই সেই origin ফেরত, নইলে কোনো CORS header নয়। (৩) সবসময় Vary: Origin. (৪) null আর wildcard subdomain নয়। (৫) Allow-Methods আর Allow-Headers ছোট রাখা। (৬) Public, লগইন-বিহীন endpoint আলাদা করে * দিতে পারে, কিন্তু credentials ছাড়া। (৭) একটি automated test যে অচেনা origin কোনো CORS header পায় না। মনে রাখুন, session cookie SameSite=Lax হলেও reflection ঠিক করতে হবে।

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

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

ক্রমCookie-সহ একটি cross-origin PUT request-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Browser দেখে request-টি simple নয়
  2. Browser OPTIONS preflight পাঠায়, cookie ছাড়া
  3. Server অনুমোদিত origin, method আর header জানায়
  4. Browser cookie-সহ আসল PUT পাঠায়
  5. Server নিজে পরিচয় আর অনুমতি যাচাই করে কাজ করে
  6. Browser response-এর CORS header মিলিয়ে পাতাকে ডেটা দেয়

পাতার ঠিকানা https://shop.example.com/cart. নিচের কোনটি same-origin?

  • http://shop.example.com/cart
  • https://shop.example.com:443/orders
  • https://api.example.com/cart
  • https://shop.example.com:8443/cart

Same-origin policy সম্পর্কে কোন কথাটি সঠিক?

  • এটি সব cross-origin request server-এ পৌঁছানো বন্ধ করে
  • এটি XSS পুরোপুরি ঠেকায়
  • এটি মূলত অন্য origin-এর response বা DOM পড়া বন্ধ করে, পাঠানো নয়
  • এটি server-কে browser-এর বাইরের client থেকেও রক্ষা করে

একটি API request-এর Origin header যা-ই হোক, সেটিই Access-Control-Allow-Origin-এ ফিরিয়ে দেয়, সঙ্গে Access-Control-Allow-Credentials: true. এর ফল কী?

  • শুধু বিশ্বস্ত origin পড়তে পারবে
  • Browser সবসময় response আটকাবে
  • Database-এ query injection হবে
  • যেকোনো সাইটের পাতা ব্যবহারকারীর পরিচয়ে API-র উত্তর পড়তে পারে

পাতা https://shop.example.com অন্য domain থেকে <script src> দিয়ে একটি analytics script এনেছে। Script-টি কার ক্ষমতায় চলে?

  • পাতার origin-এর ক্ষমতায়, DOM আর localStorage-সহ
  • শুধু সেই analytics domain-এর ক্ষমতায়
  • নিজে থেকে sandbox করা অবস্থায়
  • CORS header ছাড়া এটি চলবেই না

একটি payment iframe https://pay.example.com থেকে বার্তা পাঠায়। গ্রহণকারী পাতার সবচেয়ে নিরাপদ যাচাই কোনটি?

  • event.origin-এ pay শব্দটি আছে কি না দেখা
  • event.origin হুবহু তুলনা, ডেটার গঠন যাচাই, আর server-এ ফলাফল নিশ্চিত করা
  • বার্তার ভেতরে থাকা sender নামটি বিশ্বাস করা
  • বার্তাটি eval দিয়ে চালিয়ে ফল দেখা

Cookie আর localStorage-এর সীমা নিয়ে কোন কথাটি সঠিক?

  • দুটিই port-এর ভিত্তিতে একইভাবে আলাদা হয়
  • Cookie সবসময় হুবহু একই origin-এ সীমিত
  • localStorage origin-ভিত্তিক, আর cookie-র সীমা host, Domain ও Path ধরে আলাদা নিয়মে চলে
  • localStorage-এর মান প্রতিটি request-এ নিজে থেকে যায়

শব্দকোষ

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

মনে রাখুন

  1. Client থেকে আসা সবকিছু অবিশ্বস্ত। নিরাপত্তার সিদ্ধান্ত হয় server-এ।
  2. Origin = scheme + host + port. Site আরও ঢিলা, আর cookie-র নিয়ম আলাদা।
  3. SOP পড়া আটকায়, পাঠানো নয়। তাই CSRF আলাদা সমস্যা।
  4. CORS একটি ছাড়, সুরক্ষা নয়। Allowlist হোক হুবহু মিলের, credentials-এর সঙ্গে * বা reflection নয়।
  5. বাইরের script পায় পাতার পুরো ক্ষমতা। কম রাখুন, যাচাই করুন, আলাদা করুন। postMessage-এ origin হুবহু মেলান।

পরের পর্বে: মডেলটি জানা হলো। এবার দেখব বাস্তবে web-এ কোন ঝুঁকিগুলো সবচেয়ে বেশি ঘটে, আর সেগুলো কীভাবে সাজানো হয়: OWASP Top 10.

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

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