পাঠ ২৬ · ৬০-এর মধ্যে · মডিউল ৫
Home / Courses / Cybersecurity & Ethical Hacking / ক্রস-সাইট স্ক্রিপ্টিং

ক্রস-সাইট স্ক্রিপ্টিং (XSS)

Cross-site scripting (XSS)
৯ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • XSS ঠিক কীভাবে কাজ করে — এবং কেন এটি "সার্ভারকে" নয়, অন্য ব্যবহারকারীদের লক্ষ্য করে
  • Reflected, Stored ও DOM-based XSS-এর মধ্যে পার্থক্য এবং কোনটি সবচেয়ে বিপজ্জনক ও কেন
  • Context-aware output encoding/escaping কী এবং কেন এটিই আসল সমাধান
  • Python-এর html.escape() দিয়ে একটি নিরাপদ কমেন্ট-রেন্ডারার লেখা — কোড সহ

১ · XSS আসলে কী — এবং এটি কাকে লক্ষ্য করে

Cross-Site Scripting (XSS)XSSঅবিশ্বস্ত ইউজার ইনপুট কোনো এস্কেপিং ছাড়াই পেজে HTML/JavaScript হিসেবে রেন্ডার হওয়ার ফলে তৈরি হওয়া দুর্বলতা — আক্রমণকারীর স্ক্রিপ্ট ভুক্তভোগীর ব্রাউজারে ভুক্তভোগীরই সেশন/অনুমতি নিয়ে চলে। অন্যান্য অনেক OWASP ক্যাটাগরি (যেমন L20-এর SQLi) সার্ভার বা ডেটাবেসকে লক্ষ্য করে। XSS সম্পূর্ণ ভিন্ন — এখানে চূড়ান্ত লক্ষ্য হলো অন্য ব্যবহারকারীর ব্রাউজার। যদি একটি ওয়েবসাইট ব্যবহারকারীর দেওয়া টেক্সট (যেমন একটি কমেন্ট) কোনো প্রকার নিরাপত্তা-প্রক্রিয়াকরণ ছাড়াই সরাসরি পেজে বসিয়ে দেয়, এবং সেই টেক্সটে <script>...</script>-এর মতো কোড থাকে, তাহলে সেই স্ক্রিপ্ট ঠিক সেই ওয়েবসাইটের নিজের প্রেক্ষাপটে প্রতিটি ভিজিটরের ব্রাউজারে চলে — যা দিয়ে সেশন কুকি চুরি করা বা ভুক্তভোগীর হয়ে অ্যাকশন নেওয়া সম্ভব হতে পারে।

২ · তিন প্রকার XSS

Reflected XSS
পেলোড থাকে রিকোয়েস্টেই (যেমন একটি সার্চ-কোয়েরি প্যারামিটার), এবং সার্ভার সেটিকে সাথে সাথে রেসপন্সে "প্রতিফলিত" করে দেখায়। ভুক্তভোগীকে একটি সাজানো লিংকে ক্লিক করাতে হয়।
Stored XSS
পেলোড সার্ভার-সাইডে সংরক্ষিত হয় (যেমন একটি কমেন্ট হিসেবে), এবং প্রতিটি ভিজিটরকে দেখানো হয়। কোনো বিশেষ লিংকের প্রয়োজন নেই — এই কারণেই এটি সবচেয়ে বিপজ্জনক।
DOM-based XSS
দুর্বলতাটি সম্পূর্ণ ক্লায়েন্ট-সাইড JavaScript-এই থাকে — সার্ভার কখনো পেলোড দেখেই না, ব্রাউজারের নিজের JS কোডই অনিরাপদভাবে ইউজার ইনপুট DOM-এ বসিয়ে দেয়।
কেন Stored XSS সবচেয়ে বিপজ্জনক

Reflected XSS-এ আক্রমণকারীকে ভুক্তভোগীকে একটি নির্দিষ্ট, সাজানো লিংকে ক্লিক করাতে হয় — একটি সামাজিক-প্রকৌশল (social engineering) ধাপ প্রয়োজন। কিন্তু Stored XSS একবার সার্ভারে সংরক্ষিত হয়ে গেলে, সেই পেজ দেখা প্রতিটি ব্যবহারকারীই স্বয়ংক্রিয়ভাবে আক্রান্ত হন — কোনো লিংকে ক্লিক করার দরকার নেই, শুধু পেজটি ভিজিট করাই যথেষ্ট।

৩ · প্রতিরক্ষা — Context-Aware Output Encoding

XSS-এর মূল কারণ ইনপুট নয় — মূল কারণ হলো আউটপুট রেন্ডার করার সময় এস্কেপ না করা। সঠিক ফিক্স হলো Context-Aware Output EncodingContext-Aware Output Encodingইউজার ইনপুট পেজে বসানোর ঠিক আগে, যে প্রেক্ষাপটে (HTML বডি, HTML অ্যাট্রিবিউট, JavaScript স্ট্রিং ইত্যাদি) বসানো হচ্ছে তার উপযোগী পদ্ধতিতে বিপজ্জনক ক্যারেক্টার এনকোড করে দেওয়া। — রেন্ডার করার ঠিক আগে ইউজার ইনপুটের <, >, &, ", ' ক্যারেক্টারগুলো তাদের নিরাপদ HTML-এনটিটি রূপে (যেমন &lt;) রূপান্তর করা। এর ফলে ব্রাউজার সেই টেক্সটকে কোড হিসেবে নয়, স্রেফ লেখা হিসেবে দেখায় — দেখতে অবিকল একই থাকে, কিন্তু আর এক্সিকিউট হয় না। Content-Security-Policy (CSP) হেডার একটি অতিরিক্ত defense-in-depth স্তর (ties to L23-এর মিসিং-হেডার আলোচনা) — এটি ব্রাউজারকে বলে দেয় কোন উৎস থেকে স্ক্রিপ্ট চালানো অনুমোদিত, তাই এস্কেপিং ফসকে গেলেও CSP অনেক XSS পেলোডকে ব্লক করতে পারে।

৪ · কোড ডেমো — ইনসিকিউর বনাম সিকিউর কমেন্ট-রেন্ডারার

নিচের কোড সেলটি সম্পূর্ণ স্যান্ডবক্সড — এটি একটি ভুয়া ইন-মেমরি "কমেন্ট" তালিকা নিয়ে কাজ করছে, কোনো বাস্তব ওয়েবসাইট, ব্রাউজার DOM বা নেটওয়ার্ক এখানে জড়িত নয়। ইনসিকিউর ফাংশনটি একটি <script> পেলোড ফেরত দেয় — কিন্তু এটি সবসময় একটি সাধারণ Python স্ট্রিং হিসেবেই থেকে যায়, কখনো প্রকৃত HTML/JS হিসেবে রেন্ডার বা এক্সিকিউট করা হচ্ছে না — আমরা শুধু print() দিয়ে দেখছি এটি বাস্তব ব্রাউজারে কী দেখাত।

Python
import html

# ভুয়া ইন-মেমরি "কমেন্ট" স্টোর -- বাস্তব কোনো ওয়েবসাইট, ডেটাবেস বা DOM নয়
comments = []

def post_comment(text):
    comments.append(text)

def render_comment_insecure(text):
    # ইনসিকিউর: raw স্ট্রিং যেমন এসেছে ঠিক তেমনই ফেরত দেওয়া হচ্ছে, কোনো এস্কেপিং নেই।
    # এই ফাংশনটি এখানে শুধুই একটি স্ট্রিং রিটার্ন করছে -- আমরা এটিকে কখনো প্রকৃত
    # HTML/DOM-এ বসাচ্ছি না বা কোনো JavaScript ইঞ্জিনে চালাচ্ছি না। এটি শুধু দেখায়
    # বাস্তব একটি ভালনারেবল সার্ভার কী স্ট্রিং পাঠাত।
    return text

def render_comment_secure(text):
    # সিকিউর: real Python stdlib html.escape() ব্যবহার করে বিপজ্জনক ক্যারেক্টার
    # (<, >, &, ", ') নিরাপদ HTML এনটিটিতে রূপান্তর করা হচ্ছে
    return html.escape(text)

# একটি toy আক্রমণ-পেলোড -- শুধুমাত্র একটি Python স্ট্রিং, কখনো এক্সিকিউট করা হয় না
attacker_payload = "সুন্দর পোস্ট! <script>steal_cookie()</script>"
post_comment(attacker_payload)

print("== ইনসিকিউর রেন্ডার (raw স্ট্রিং, শুধু প্রিন্ট -- কোথাও এক্সিকিউট হচ্ছে না) ==")
print(render_comment_insecure(comments[0]))
print("(বাস্তব ব্রাউজারে unescaped বসানো হলে <script> ট্যাগ সত্যিই চলত -- এখানে তা ঘটছে না)")

print()
print("== সিকিউর রেন্ডার (html.escape() প্রয়োগের পর) ==")
print(render_comment_secure(comments[0]))
print("(এখন <script> ট্যাগ নিষ্ক্রিয় টেক্সট -- ব্রাউজার একে কোড নয়, নিছক লেখা হিসেবে দেখাবে)")

    
লক্ষ্য করুন — ইনসিকিউর আউটপুটে <script>steal_cookie()</script> ঠিক যেমন লেখা হয়েছিল তেমনই আছে। এই পুরো ডেমোতে এটি শুধুমাত্র একটি Python স্ট্রিং হিসেবেই ছিল ও প্রিন্ট হয়েছে — কোনো বাস্তব ব্রাউজার, HTML পার্সার বা JavaScript ইঞ্জিন এই কোডে জড়িত নেই, তাই কোনো প্রকৃত স্ক্রিপ্ট কখনো চলেনি। এই বাক্যটি শুধু দেখাচ্ছে একটি প্রকৃত ভালনারেবল ওয়েব অ্যাপ্লিকেশনে এই একই স্ট্রিং ব্রাউজারে বসালে কী হতো।
সিকিউর ভার্সনের আউটপুটে < ও > ক্যারেক্টারগুলো তাদের HTML-এনটিটি রূপে (&lt;, &gt;) রূপান্তরিত হয়েছে। ব্রাউজার এই এনটিটিগুলো দেখে বুঝতে পারে এগুলো একটি ট্যাগ শুরু নয়, বরং আক্ষরিক < ক্যারেক্টার হিসেবে দেখানো উচিত — ফলে টেক্সট হুবহু একই দেখায় (ইউজার একই কমেন্ট পড়েন) কিন্তু কোনো কোড এক্সিকিউট হয় না।

৫ · কেন "ইনপুট ভ্যালিডেশন" একা যথেষ্ট নয়

একটি সাধারণ ভুল ধারণা হলো XSS ঠেকাতে শুধু ইনপুটে <script> শব্দটি ব্লক করলেই যথেষ্ট। বাস্তবে আক্রমণকারীরা অসংখ্য বিকল্প উপায়ে (ইভেন্ট হ্যান্ডলার অ্যাট্রিবিউট, বিভিন্ন এনকোডিং, ইত্যাদি) একই ফলাফল অর্জন করতে পারে — তাই "খারাপ শব্দ ব্লক করা" (blocklisting) কখনো নির্ভরযোগ্য সমাধান নয়। আসল সমাধান সবসময় আউটপুট এস্কেপিং — রেন্ডার করার মুহূর্তে, প্রতিটি প্রেক্ষাপটে সঠিকভাবে এনকোড করা, ইনপুট কী তা অনুমান করে ব্লক করার চেষ্টা নয়।

মূল কথা · Key takeaway

XSS মনে করিয়ে দেয় যে "ব্যবহারকারীর ইনপুট" শুধু ডেটাবেসের জন্য বিপজ্জনক নয় (SQLi, L20) — এটি অন্য ব্যবহারকারীদের ব্রাউজারের জন্যও সমানভাবে বিপজ্জনক হতে পারে। যেকোনো জায়গায় ইউজার-নিয়ন্ত্রিত ডেটা পেজে ফিরে আসছে (কমেন্ট, প্রোফাইল নাম, সার্চ রেজাল্ট) — সেখানেই রেন্ডার করার ঠিক আগে সঠিক এস্কেপিং প্রয়োগ করতে হবে।

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

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ XSS-এর "লক্ষ্য" কেন সার্ভার নয়, বরং অন্য ব্যবহারকারীর ব্রাউজার — এটি SQLi-এর মতো আক্রমণ থেকে কীভাবে ভিন্ন?

SQLi-তে (L20) আক্রমণকারীর পেলোড সার্ভার-সাইড ডেটাবেস ইঞ্জিনে এক্সিকিউট হয় — লক্ষ্য সার্ভারের নিজের ডেটা। XSS-এ পেলোড ভিন্ন জায়গায় এক্সিকিউট হয়: ভুক্তভোগীর ব্রাউজারে, ভুক্তভোগীর নিজের সেশন/লগইন প্রেক্ষাপটে। এই পার্থক্যের কারণেই প্রতিরক্ষার জায়গাও ভিন্ন — SQLi ঠেকাতে হয় ডেটাবেস কোয়েরি বানানোর সময়, আর XSS ঠেকাতে হয় পেজে আউটপুট রেন্ডার করার সময়।

প্র ০২ একটি সাইট যদি শুধু ইনপুট থেকে <script> শব্দটি সরিয়ে দেয় (কিন্তু আউটপুট এস্কেপ না করে), তাহলে এটি কেন এখনো XSS-ঝুঁকিতে থাকবে?

কারণ ব্রাউজারে JavaScript চালানোর অনেক উপায় আছে যা <script> শব্দটি ব্যবহারই করে না — যেমন একটি ইমেজ ট্যাগের onerror অ্যাট্রিবিউট (<img src=x onerror=...>) বা একটি লিংকের javascript: URL। "নির্দিষ্ট খারাপ শব্দ ব্লক করা" (blocklisting) একটি অসম্পূর্ণ তালিকার উপর নির্ভর করে, যেখানে সঠিক আউটপুট এস্কেপিং প্রেক্ষাপট অনুযায়ী সব বিপজ্জনক ক্যারেক্টার নিষ্ক্রিয় করে দেয় — কী ইনপুট এসেছে তা অনুমান করার প্রয়োজন হয় না।

প্র ০৩ DOM-based XSS-এ সার্ভার কখনো পেলোড দেখে না — তাহলে ডেভেলপাররা কীভাবে এটি ধরবেন বা প্রতিরোধ করবেন?

যেহেতু DOM-based XSS সম্পূর্ণ ক্লায়েন্ট-সাইড JavaScript-এ ঘটে (যেমন URL-এর একটি অংশ সরাসরি innerHTML-এ বসিয়ে দেওয়া), তাই সার্ভার-সাইড লগ পর্যালোচনা এটি ধরতে পারবে না। প্রতিরোধ করতে হবে ক্লায়েন্ট-সাইড কোড রিভিউ দিয়ে — যেকোনো জায়গায় ইউজার-নিয়ন্ত্রিত ডেটা (URL, cookie, localStorage) সরাসরি innerHTML-এ বসছে কি না তা খুঁজে বের করা এবং নিরাপদ API (যেমন textContent, যা কখনো HTML পার্স করে না) দিয়ে প্রতিস্থাপন করা, অথবা সেখানেও context-aware এস্কেপিং প্রয়োগ করা।

অনুশীলন

  1. চিন্তা করুন: একটি ওয়েবসাইটের "ইউজারনেম" ফিল্ড যদি প্রোফাইল পেজে unescaped দেখানো হয়, তাহলে এটি Reflected, Stored নাকি DOM-based XSS-এর ঝুঁকিতে পড়বে, এবং কেন?

    এটি সাধারণত Stored XSS — কারণ ইউজারনেম রেজিস্ট্রেশনের সময় সার্ভারে সংরক্ষিত হয়ে যায় এবং প্রোফাইল পেজ দেখা প্রতিটি ভিজিটরের কাছে (এমনকি ইউজার নিজে লগইন না থাকলেও) দেখানো হয় — কোনো বিশেষ লিংকে ক্লিক করার প্রয়োজন নেই। এটি স্পষ্ট করে কেন ইউজারনেমের মতো "সহজ, নিরীহ" মনে হওয়া ফিল্ডও যদি এস্কেপ না করা হয়, তা একটি গুরুতর Stored XSS দুর্বলতা হয়ে উঠতে পারে।

  2. পরীক্ষা করুন: উপরের কোড সেলে attacker_payload-এ একটি অতিরিক্ত & ক্যারেক্টার যোগ করে (যেমন "Tom & Jerry") Run চাপুন — সিকিউর ভার্সনে এটি কীভাবে এনকোড হয়?

    html.escape() & ক্যারেক্টারকে &amp;-এ রূপান্তর করবে — কারণ HTML-এ & নিজেই একটি বিশেষ ক্যারেক্টার (এনটিটির শুরু বোঝায়, যেমন &lt;)। যদি & নিজেই এস্কেপ করা না হতো, তাহলে পরবর্তী টেক্সট ব্রাউজার ভুলভাবে একটি এনটিটি হিসেবে পার্স করার চেষ্টা করতে পারত — তাই html.escape() সবসময় সবার আগে &-কেই এনকোড করে, তারপর অন্য ক্যারেক্টার।

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

পূর্ববর্তী পাঠ
A07: আইডেন্টিফিকেশন ও অথেন্টিকেশন ফেইলিওর