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

SSRF আর file handling — URL fetch, upload আর path-এর নিরাপত্তা

SSRF & File Handling — Safe URL Fetching, Uploads and Paths

Taught in Bangla, this lesson covers SSRF defense layer by layer: allowlists, IP checks, blocked redirects, egress firewalls, a separate fetch service and IMDSv2 for cloud metadata. It also shows path traversal prevention, safe file upload and download, deserialization and XXE limits, archive size limits, and how to detect these attempts in logs, with worked examples on toy data.

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

  • SSRF কেন ঘটে, আর cloud metadata সেবা কেন ঝুঁকি বাড়ায়
  • নিরাপদ URL fetch-এর প্রতিটি স্তর, কোন স্তর কী ঠেকায় আর কী ঠেকায় না
  • Path traversal ঠেকাতে path ঠিকভাবে যাচাই করা
  • নিরাপদ file upload ও download-এর পুরো পথ
  • Deserialization, XXE আর archive খোলার ঝুঁকি ও সীমা
  • Log থেকে এসব চেষ্টা ধরা, আর ধরা পড়লে কী করবেন

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

১ · SSRF — অফিসের পিয়নকে দিয়ে ভেতরের ফাইল আনানো

একটি বড় অফিসের কথা ভাবুন। গেটে দারোয়ান আছেন, বাইরের লোক ভেতরের ঘরে ঢুকতে পারেন না। কিন্তু অফিসের পিয়নকে সবাই চেনেন, তিনি সব ঘরে যেতে পারেন। এখন বাইরের একজন গেট থেকে পিয়নকে বললেন, “ভাই, হিসাব শাখার তিন নম্বর আলমারি থেকে ফাইলটা এনে দিন।” পিয়ন যদি যাচাই না করেন, দারোয়ান বৃথা। বাইরের লোক নিজে ঢোকেননি, ঢুকেছে তাঁর অনুরোধ।

SSRF (Server-Side Request Forgery) ঠিক এই ঘটনা। Web application অনেক সময় ব্যবহারকারীর দেওয়া ঠিকানা থেকে নিজে কিছু আনে: “ছবির লিংক দিন”, link preview, PDF বানানো, webhook পরীক্ষা, RSS পড়া। অনুরোধটি যায় server থেকে, তাই সেটি server-এর পরিচয় আর server-এর network অবস্থান নিয়ে যায়। Server যদি ঠিকানা যাচাই না করে, ব্যবহারকারী সেটিকে এমন জায়গায় পাঠাতে পারেন, যেখানে বাইরে থেকে পৌঁছানো যায় না। পর্ব ৩-এর ভাষায় এটি একটি confused deputy: বেশি অধিকারের প্রোগ্রাম অন্যের কথায় কাজ করে ফেলছে।

একই server, দুই দিকের পথ

The application sits on the trust boundary: outbound requests need their own policy

SSRF-এর trust boundary: বাইরের অনুরোধ app হয়ে ভেতরে পৌঁছানোর ঝুঁকি বাইরের ব্যবহারকারী URL দিলেন: <user-url> trust boundary Web app (server) নিজের পরিচয়ে অনুরোধ পাঠায় ভেতরের অঞ্চল Admin panel Database Cloud metadata Egress firewall অনুমোদিত partner লাল পথ: বন্ধ রাখুন · সবুজ পথ: শুধু যা দরকার
ছবি ৪২.১ — আমরা সাধারণত ভেতরে ঢোকার পথ পাহারা দিই। SSRF মনে করিয়ে দেয়, server থেকে বাইরে যাওয়ার পথেরও নীতি লাগে।

হামলাকারীর চোখে ধারণাটি সহজ: যা বাইরে থেকে দেখা যায় না, server দিয়ে তার সাড়া দেখা বা তাতে প্রভাব ফেলা। ফল সবসময় সরাসরি ফেরে না। কখনো উত্তর পাওয়া যায় না, শুধু সময় বা error-এর পার্থক্য থেকে কিছু বোঝা যায়। একে blind SSRF বলে। রক্ষকের চোখে প্রশ্নটি হলো: আমার server কোথায় কোথায় যেতে পারে, আর সেই তালিকা কে ঠিক করে?

লক্ষ্যসম্ভাব্য ক্ষতিমূল প্রতিরক্ষা
ভেতরের admin panelLogin ছাড়া চলা ভেতরের সেবার তথ্য বা কাজভেতরেও authentication, segmentation
Cloud metadataঅস্থায়ী credential ফাঁসIMDSv2-এর মতো ব্যবস্থা, role-এ কম অধিকার
ভেতরের network-এর ছবিকী কী সেবা আছে, তার আভাসEgress নিয়ম, একই ধরনের error
Server নিজেইবড় ফাইল বা ধীর সংযোগে সম্পদ শেষTimeout, আকারের সীমা, rate limit

OWASP Top 10-এর ২০২১ সংস্করণে SSRF ছিল আলাদা বিভাগ A10. ২০২৫ সংস্করণের পাতায় একে (CWE-918) A01 Broken Access Control-এর অধীনে দেখানো হয়েছে। বিভাগ বদলালেও ঝুঁকি একই। পুরো মানচিত্র আছে পর্ব ৩৭-এ।

২ · Cloud metadata — কেন ঝুঁকি হঠাৎ বেড়ে যায়, আর IMDSv2

Cloud-এ একটি virtual machine চালু হলে তার নিজের সম্পর্কে তথ্য জানতে হয়: সে কে, কোন অঞ্চলে, কোন role পেয়েছে। এজন্য থাকে cloud metadata service, যা machine-এর ভেতর থেকে একটি link-local ঠিকানায় পাওয়া যায়। অনেক সময় এটি অস্থায়ী workload identity-র credential-ও দেয়। কোডে secret না রাখার জন্য এটি খুব ভালো নকশা (Cloud IAM দেখুন)।

কিন্তু ভাবুন, ব্যাংকের লকার রুমের চাবির বাক্স কর্মীদের ঘরের ভেতরে খোলা রাখা, কারণ “বাইরের কেউ তো ঢুকতে পারে না।” এখন পিয়নকে দিয়ে যদি বাক্স থেকে চাবি আনানো যায়? SSRF ঠিক এটাই বদলে দেয়। সাধারণ ভেতরের তথ্যের বদলে এখানে ফাঁস হতে পারে cloud-এ কাজ করার অধিকার। ক্ষতির মাপ তখন নির্ধারণ করে সেই role-এর blast radius.

Capital One, ২০১৯ — প্রকাশিত প্রতিবেদনের স্তরে। প্রকাশিত প্রতিবেদন অনুযায়ী ১০ কোটিরও বেশি ক্রেডিট আবেদনের তথ্য চুরি হয়েছিল। Krebs on Security-সহ বিভিন্ন প্রতিবেদনে বলা হয়, একটি ভুলভাবে সাজানো web application firewall, SSRF-এর ধরনের সমস্যা আর প্রয়োজনের চেয়ে বেশি অধিকারের role মিলে ঘটনাটি ঘটায়। Amazon বলেছিল, কারণ ছিল firewall-এর ভুল configuration, তাদের মূল পরিকাঠামো নয়। ২০২০ সালের ৬ আগস্ট মার্কিন নিয়ন্ত্রক OCC ব্যাংকটিকে ৮ কোটি ডলার জরিমানার ঘোষণা দেয়, cloud-এ যাওয়ার আগে কার্যকর ঝুঁকি মূল্যায়ন না করার কারণে। শিক্ষা একটি ভুলে আটকে নেই: যেকোনো একটি স্তর ঠিক থাকলে ক্ষতি অনেক কম হতে পারত।

এরপর AWS ২০১৯ সালের নভেম্বরে IMDSv2 আনে। আগের ধরনে একটি সাধারণ অনুরোধেই তথ্য মিলত। নতুন ধরনে আগে একটি session শুরু করে token নিতে হয়, তারপর সেই token দেখিয়ে তথ্য পড়তে হয়। AWS-এর ব্যাখ্যা অনুযায়ী এর তিনটি বাধা আছে:

  • আলাদা ধরনের অনুরোধ: Session শুরু করতে PUT লাগে, যা বেশিরভাগ খোলা proxy বা firewall পার করে না, আর সাধারণ URL-আনা কোড সাধারণত পাঠায় না।
  • Proxy-র চিহ্ন থাকলে না: অনুরোধে X-Forwarded-For header থাকলে token দেওয়া হয় না।
  • Hop limit: Token-এর উত্তর ছোট TTL নিয়ে আসে, ডিফল্টে ১, ফলে সেটি machine ছেড়ে বেশি দূর যেতে পারে না।

নিচের কমান্ডটি আপনার নিজের AWS instance-এর জন্য একটি hardening উদাহরণ। এটি v1 ধরন বন্ধ করে। অন্য provider-এর ব্যবস্থা আলাদা, অনেকে বিশেষ header চায়, তাই নিজের provider-এর documentation দেখুন।

aws ec2 modify-instance-metadata-options \
  --instance-id i-0123456789abcdef0 \
  --http-tokens required \
  --http-put-response-hop-limit 1

IMDSv2 SSRF-এর সমাধান নয়, একটি স্তর মাত্র। এটি ভেতরের admin panel বা database রক্ষা করে না। আর application নিজেই যদি যেকোনো method ও header পাঠাতে দেয়, বাধা দুর্বল হয়। Role-এর অধিকার কম রাখাই ক্ষতির আসল সীমা। Container থাকলে hop limit-এর প্রভাবও আলাদাভাবে যাচাই করুন, ঠিক মান নির্ভর করে আপনার সাজানোর ওপর।

৩ · নিরাপদ URL fetch — পিয়নের জন্য নিয়মের খাতা

পিয়নকে নিয়ম দিতে হলে কী লিখবেন? “এই কয়টি ঘর ছাড়া কোথাও যাবেন না।” তালিকার বাইরে কিছু চাইলে না বলবেন। এটাই allowlist, যা default deny-এর প্রয়োগ। সম্ভব হলে এর চেয়েও ভালো পথ হলো ব্যবহারকারীর কাছ থেকে URL না নেওয়া। তিনি শুধু একটি পছন্দ বা id দেবেন, আর URL server-এ আগেই ঠিক করা থাকবে।

স্তরকী করেকী ঠেকায় না
URL না নেওয়াServer নিজেই URL বানায়বৈধ partner-এর নিজের ত্রুটি
AllowlistScheme, host, port নির্দিষ্টঅনুমোদিত host-এর DNS ভুল দিকে গেলে
Resolve-then-checkDNS-এ পাওয়া প্রতিটি IP যাচাই, সেই IP-তেই সংযোগPublic কিন্তু অনিচ্ছিত গন্তব্য
Range আটকানোPrivate, loopback, link-local বন্ধতালিকা অসম্পূর্ণ থাকলে
Redirect বন্ধঅন্য ঠিকানায় পাঠানো মানে নাযে সেবার redirect সত্যিই দরকার, সেখানে প্রতি ধাপে আবার যাচাই লাগে
Egress firewallNetwork স্তরে বাইরে যাওয়ার পথ সীমিতApplication-এর ভেতরের যুক্তি
আলাদা fetch serviceকম অধিকারের আলাদা জায়গায় আনাআনা ডেটার বিষয়বস্তু, সেটি আলাদা যাচাই চায়

আগে ঠিকানার শ্রেণি চেনা। String দিয়ে “10. দিয়ে শুরু কি না” দেখা নড়বড়ে, কারণ একই ঠিকানার নানা লেখার রূপ আছে। ঠিকানাকে সবসময় নির্ভরযোগ্য library দিয়ে parse করুন। নিচের কোড Python-এর standard library-র ipaddress দিয়ে সিদ্ধান্ত নেয়। কোনো সংযোগ হয় না, শুধু ঠিকানার শ্রেণি বিচার হয়। এখানে একটি documentation range-কে খেলনা partner হিসেবে ধরা হয়েছে।

import ipaddress

# Toy allowlist: pretend this documentation range is a partner's servers
PARTNER = ipaddress.ip_network("203.0.113.0/24")

def decide(text):
    try:
        addr = ipaddress.ip_address(text)
    except ValueError:
        return "BLOCK", "not a valid address"
    if addr.version == 4 and addr in PARTNER:
        return "ALLOW", "inside partner allowlist"
    if addr.is_loopback:
        return "BLOCK", "loopback: this machine"
    if addr.is_link_local:
        return "BLOCK", "link-local range"
    if addr.is_private:
        return "BLOCK", "private or reserved"
    return "BLOCK", "default deny: not allowlisted"

tests = ["203.0.113.10", "198.51.100.7", "10.0.0.5", "192.168.1.1",
         "127.0.0.1", "169.254.10.20", "100.64.0.1",
         "::1", "fe80::1", "fd00::1", "not-an-ip"]

print(f"{'address':15} {'decision':6} reason")
for t in tests:
    verdict, reason = decide(t)
    print(f"{t:15} {verdict:6} {reason}")

shared = ipaddress.ip_address("100.64.0.1")
print("100.64.0.1 is_private:", shared.is_private,
      "is_global:", shared.is_global)

আসল আউটপুটের কয়েকটি লাইন:

203.0.113.10    ALLOW  inside partner allowlist
169.254.10.20   BLOCK  link-local range
100.64.0.1      BLOCK  default deny: not allowlisted
::1             BLOCK  loopback: this machine
100.64.0.1 is_private: False is_global: False

শেষ লাইনটি খেয়াল করুন। 100.64.0.1 ঠিকানাটি is_private-এ False, আবার is_global-এও False. এটি carrier-grade NAT-এর ভাগ করা range. শুধু “private কি না” দেখলে এটি ফাঁক গলে যেত। এজন্য কোডের শেষ লাইনে default deny আছে: কোনো নিয়মে না মিললে উত্তর “না”। IPv6-এর জন্যও একই বিচার চলছে, যা অনেক হাতে লেখা তালিকায় বাদ পড়ে। 198.51.100.7 documentation range, Python একে বিশেষ ঠিকানা ধরে is_private-এ True দেয়, তাই সেটিও আটকেছে।

এবার পুরো পথ দেখুন। ধরুন “হাটবাজার” নামের একটি কাল্পনিক অনলাইন বাজারে বিক্রেতা পণ্যের ছবির লিংক দেন। প্রতিটি ধাপে দেখুন কোন প্রশ্নের উত্তর মিলল।

Fetch service-এর খাতা (খেলনা ডেটা)
ধাপ
দেখা হচ্ছে
সিদ্ধান্ত
  1. URL parse. লেখাটিকে নির্ভরযোগ্য URL parser দিয়ে অংশে ভাগ করা হলো: scheme, host, path. হাতে লেখা regex নয়। Parse না হলে সরাসরি না।
  2. Scheme ও port. শুধু https আর ৪৪৩ চলবে। ফাইল পড়ার বা অন্য protocol-এর scheme এখানেই বাদ।
  3. Host allowlist. নামটি হুবহু তালিকায় আছে। আংশিক মিল বা “শেষে partner.example আছে কি না” ধরনের ঢিলা তুলনা নয়।
  4. DNS resolve. নাম থেকে IP আনা হলো। নাম ঠিক হলেও IP কোথায়, সেটিই আসল প্রশ্ন।
  5. প্রতিটি IP যাচাই। একাধিক IP এলে সবগুলো পাস করতে হবে। একটিও ভেতরের হলে পুরো অনুরোধ বাতিল।
  6. যাচাই করা IP-তে সংযোগ। আবার DNS না করে এই IP-তেই সংযোগ হলো, কিন্তু TLS-এর নাম যাচাই থাকল। না হলে যাচাই আর সংযোগের মাঝে DNS উত্তর বদলাতে পারে। একে DNS rebinding-এর ঝুঁকি বলে, আর এটি TOCTOU-র একটি রূপ।
  7. Redirect এলো। Server বলল অন্য ঠিকানায় যান। HTTP client-এ redirect বন্ধ, তাই অনুসরণ হলো না। অনুমোদিত ঠিকানাও অন্যত্র পাঠাতে পারে, যা open redirect-এর সাথে মিলে বিপজ্জনক।
  8. Log আর ফল। সিদ্ধান্ত, কারণ, host আর IP log-এ গেল। ব্যবহারকারী পেলেন সাধারণ বার্তা “ছবিটি আনা গেল না”। ভেতরের কোনো বিবরণ বা response-এর বিষয়বস্তু ফেরত গেল না।

এবার একই নীতি একটি ছোট simulator-এ চালান। এখানে DNS আর web দুটোই সাধারণ JavaScript object, কোনো অনুরোধ এই পাতার বাইরে যায় না। off মানে redirect বন্ধ, recheck মানে প্রতি ধাপে সব যাচাই আবার।

// Toy fetch policy. DNS and the web are plain objects: nothing leaves this page.
const HOSTS = new Set(['img.partner.example', 'cdn.partner.example',
  'stale.partner.example', 'mixed.partner.example']);
const DNS = { 'img.partner.example': ['203.0.113.10'],
  'cdn.partner.example': ['203.0.113.20'], 'stale.partner.example': ['10.0.0.8'],
  'mixed.partner.example': ['203.0.113.30', '192.168.1.9'] };
const WEB = { 'https://cdn.partner.example/logo.png': 'https://img.partner.example/logo.png',
  'https://cdn.partner.example/old.png': 'https://intranet.corp.example/' };
const internal = ip => /^(10\.|127\.|192\.168\.|169\.254\.)/.test(ip);
function check(text) {
  const u = new URL(text);
  if (u.protocol !== 'https:') return 'scheme ' + u.protocol;
  if (u.port !== '') return 'port ' + u.port;
  if (!HOSTS.has(u.hostname)) return 'host not on allowlist';
  const bad = DNS[u.hostname].find(internal);
  return bad ? 'resolves to internal ' + bad : null;
}
function fetchToy(text, redirects) {
  let url = text;
  for (let hop = 0; hop < 3; hop++) {
    const why = check(url);
    if (why) return 'REFUSED hop ' + hop + ': ' + why;
    const next = WEB[url];
    if (!next) return 'OK, pinned to ' + DNS[new URL(url).hostname][0];
    if (redirects === 'off') return 'STOPPED: redirect not followed';
    url = next;
  }
  return 'REFUSED: too many redirects';
}
for (const u of ['https://img.partner.example/a.png', 'http://img.partner.example/a.png',
  'https://img.partner.example:8443/a.png', 'https://other.example/a.png',
  'https://stale.partner.example/a.png', 'https://mixed.partner.example/a.png'])
  console.log(u.replace('https://', '').padEnd(36), fetchToy(u, 'off'));
for (const u of Object.keys(WEB)) for (const mode of ['off', 'recheck'])
  console.log(u.slice(8).padEnd(28), mode.padEnd(8), fetchToy(u, mode));

আউটপুটের মূল লাইন:

img.partner.example/a.png            OK, pinned to 203.0.113.10
stale.partner.example/a.png          REFUSED hop 0: resolves to internal 10.0.0.8
mixed.partner.example/a.png          REFUSED hop 0: resolves to internal 192.168.1.9
cdn.partner.example/logo.png recheck  OK, pinned to 203.0.113.10
cdn.partner.example/old.png  recheck  REFUSED hop 1: host not on allowlist

stale host তালিকায় আছে, তবু আটকেছে, কারণ তার DNS এখন ভেতরের IP দেখায়। পুরোনো, ভুলে যাওয়া DNS রেকর্ডে এমন হতে পারে। mixed-এর দুটি IP-র একটি ভেতরের, তাই পুরোটাই বাতিল। recheck মোডে বৈধ redirect কাজ করেছে, কিন্তু দ্বিতীয় ধাপে ভেতরের host আটকে গেছে। সবচেয়ে সরল আর নিরাপদ পছন্দ তবু off. আসল কোডে IP-র বিচার regex দিয়ে নয়, আগের ব্লকের মতো library দিয়ে করুন। এই খেলনায় regex শুধু কোড ছোট রাখতে।

Application-এর নিয়ম ভুল হতে পারে, তাই network স্তরেও একই নীতি বসান। পর্ব ২২-এর মতো app server-এর বাইরে যাওয়ার পথ সীমিত করুন, দরকারে সব বাইরের অনুরোধ forward proxy দিয়ে পাঠান। ইচ্ছামতো URL নিতেই হলে (যেমন webhook বা link preview) allowlist সম্ভব নয়। তখন fetch service-কে আলাদা network-এ রাখুন, যেখান থেকে ভেতরের কিছুতেই পথ নেই, আর তাকে কোনো cloud role দেবেন না।

৪ · Path traversal — নির্ধারিত তাকের বাইরে হাত

গুদামের কর্মীকে বলা আছে, “গ্রাহক যে নম্বর বলবেন, ৫ নম্বর ঘরের সেই তাক থেকে জিনিস দিন।” কেউ যদি বলেন, “এই ঘর থেকে বেরিয়ে পাশের ঘরের তাক থেকে দিন”, আর কর্মী নিয়ম না বুঝে তাই করেন? Path traversal এই ভুল। Application ব্যবহারকারীর দেওয়া নাম ফোল্ডারের path-এর সাথে জুড়ে ফাইল খোলে। নামের ভেতরে “ওপরের ফোল্ডারে যাওয়া” অর্থের অংশ থাকলে OS সেটি মেনে নেয়। Path-এর গঠন বদলে যায়, ফোল্ডারের সীমা অর্থহীন হয়।

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

base = /srv/shop/files (খেলনা উদাহরণ)
ব্যবহারকারীর নাম
Path
তুলনা
ফল
  1. নাম এলো। ব্যবহারকারী download-এ একটি নাম পাঠালেন। লেখাটি দেখে সিদ্ধান্ত নেওয়া হচ্ছে না, শুধু গ্রহণ করা হলো।
  2. Base-এর সাথে জোড়া। base আর নাম জুড়ে একটি প্রার্থী path হলো। এই লেখাটি দেখতে base দিয়ে শুরু, কিন্তু ভেতরে ওপরে ওঠার অংশ আছে। তাই শুরুর লেখা মিলিয়ে কিছু প্রমাণ হয় না।
  3. Resolve. Symlink আর ওপরে ওঠার অংশ খুলে OS যে আসল জায়গা বুঝবে, সেটি বের হলো: /srv/shop/private/salary.csv.
  4. Base-এর সাথে তুলনা। তুলনা হলো path-এর অংশ ধরে, লেখার অক্ষর ধরে নয়। তৃতীয় অংশে files-এর জায়গায় private, তাই আসল path base-এর বাইরে।
  5. প্রত্যাখ্যান। কোনো ফাইল খোলা হয়নি। উত্তরে সাধারণ 404, log-এ পুরো ঘটনা। “এই ফাইল নেই” আর “অনুমতি নেই” আলাদা বললে তথ্য ফাঁস হয়।
  6. ভালো নকশা। ব্যবহারকারী শুধু একটি id দেন। Server database থেকে নিজের তৈরি নাম আনে, আর আগে মালিকানা যাচাই করে। ব্যবহারকারীর লেখা path-এ পৌঁছায়ই না, তাই এই পুরো শ্রেণির ঝুঁকি কমে যায়।

নিচের কোড block-এর নিজের বানানো অস্থায়ী ফোল্ডারে খেলনা ফাইল তৈরি করে। naive ঘরটি অনিরাপদ কোডের ধরন দেখায়, যা শুধু লেখা তুলনা করে। safe ঘরটি আসল path ধরে বিচার করে।

import os, tempfile
from pathlib import Path

def safe_path(base, name):
    """Return the real path if it stays inside base, else None."""
    target = (base / name).resolve()
    return target if target.is_relative_to(base) else None

def naive_ok(base, name):
    # Unsafe pattern: compares text only, never looks at the disk
    return os.path.normpath(base / name).startswith(str(base))

with tempfile.TemporaryDirectory() as tmp:
    root = Path(tmp).resolve()
    base = root / "public"
    (base / "reports").mkdir(parents=True)
    (root / "private").mkdir()
    (base / "reports" / "jan.txt").write_text("January sales")
    (root / "private" / "salary.txt").write_text("toy data")
    os.symlink(root / "private" / "salary.txt", base / "shortcut.txt")

    names = ["reports/jan.txt", "reports/../reports/jan.txt",
             "../private/salary.txt", "../public-old/a.txt",
             "shortcut.txt", "/srv/toy/other.txt"]
    print(f"{'requested name':28} {'naive':6} safe")
    for name in names:
        naive = "pass" if naive_ok(base, name) else "fail"
        real = safe_path(base, name)
        if real:
            print(f"{name:28} {naive:6} OPEN {real.relative_to(root)}")
        else:
            print(f"{name:28} {naive:6} REFUSED")

আসল আউটপুট:

requested name               naive  safe
reports/jan.txt              pass   OPEN public/reports/jan.txt
reports/../reports/jan.txt   pass   OPEN public/reports/jan.txt
../private/salary.txt        fail   REFUSED
../public-old/a.txt          pass   REFUSED
shortcut.txt                 pass   REFUSED
/srv/toy/other.txt           fail   REFUSED

প্রথম দুটি নাম একই ফাইলে পৌঁছায়, তাই খুলেছে। লেখা আলাদা হলেও ফল এক, এটাই canonicalization-এর সুবিধা। চতুর্থ সারিতে অনিরাপদ যাচাই pass দিয়েছে, কারণ public-old লেখাটি public দিয়ে শুরু। পঞ্চম সারিতে ফোল্ডারের ভেতরের shortcut আসলে বাইরে নির্দেশ করে, যা শুধু লেখা দেখে বোঝা যায় না। is_relative_to অংশ ধরে তুলনা করে, আর resolve() symlink খুলে দেখে, তাই দুটিই আটকেছে। Python 3.9 থেকে এই method আছে।

একটি সীমা মনে রাখুন: যাচাই আর ফাইল খোলার মাঝে অন্য process symlink বসাতে পারে। যে ফোল্ডারে ব্যবহারকারীর লেখার সুযোগ আছে, সেখানে এই TOCTOU ঝুঁকি বাস্তব। তাই upload ফোল্ডারে ব্যবহারকারীকে symlink বানাতে দেবেন না, সার্ভিসের অ্যাকাউন্টের অধিকার কম রাখুন, আর সম্ভব হলে container বা আলাদা volume-এ রাখুন। ফাইলের অধিকারের ধারণা দেখুন Operating Systems-এর protection পর্বে।

৫ · File upload — অচেনা পার্সেল ঘরে নেওয়ার আগে

পোস্ট অফিসে একটি পার্সেল এলো, ওপরে লেখা “বই”। আপনি কি শুধু লেখা দেখে সেটি লকার রুমে তুলবেন? ওজন দেখবেন, স্ক্যান করবেন, খুলবেন আলাদা জায়গায়। Unrestricted file upload মানে এই যাচাইগুলো না থাকা। Upload হলো ব্যবহারকারীর দেওয়া এমন ডেটা, যা disk-এ থাকে, পরে অন্য প্রোগ্রাম পড়ে, আর অন্য ব্যবহারকারীর browser খোলে। তাই ঝুঁকি কয়েক রকম:

ঝুঁকিকী হতে পারেপ্রতিরক্ষা
চালানো যায় এমন ফাইলWeb root-এ থাকলে server কোড হিসেবে চালাতে পারে, একে web shell বলেAllowlist, web root-এর বাইরে রাখা, upload ফোল্ডারে চালানো বন্ধ
Content-Type spoofingClient যা ঘোষণা করে, সেটি client-এরই হাতেMagic bytes আর আসল parse
বড় ফাইলDisk, memory, bandwidth শেষReverse proxy আর app, দুই জায়গায় সীমা
ছবি বা নথি parse-এর ত্রুটিLibrary-র bug ফাইল খোলার সময়ই কাজে লাগতে পারেPatch, আলাদা sandbox, pixel সীমা
Browser-এ চলা সক্রিয় বিষয়বস্তুHTML, SVG বা এমন ফাইল আপনার domain-এ খুললে XSSআলাদা domain, Content-Disposition, nosniff
Malware বিতরণআপনার সাইট অন্যকে ক্ষতিকর ফাইল দেয়Antivirus স্ক্যান, শুধু দরকারি ধরন
ফাইলের নামOverwrite, traversal, log বা HTML-এ ঝামেলাServer-এর বানানো এলোমেলো নাম

নিচের pipeline-এ প্রতিটি ধাপ আগের ধাপের ওপর ভরসা না করে নিজের কাজ করে। খেলনা ফাইলের নাম dokan-logo.png, আকার ১২০ KB.

খেলনা ফাইলের যাত্রা
ধাপ
ফাইল
জায়গা
ফল
  1. আকার। পুরো ফাইল পড়ার আগেই সীমা। Reverse proxy আর app দুই জায়গায় সীমা থাকলে বড় অনুরোধ শুরুতেই থামে।
  2. Extension. শুধু অনুমোদিত তালিকা। শেষের extension ছোট হাতের করে তুলনা। নিষিদ্ধ তালিকা নয়, কারণ নতুন ধরন বাদ পড়ে যায়।
  3. Magic bytes. ফাইলের শুরুর byte দেখে ধরন মিলিয়ে নেওয়া হলো। Client-এর ঘোষিত Content-Type শুধু তথ্য, প্রমাণ নয়। তবে magic bytes-ও শুধু শুরুটা দেখে, বাকি অংশ নিয়ে কিছু বলে না।
  4. পুনর্গঠন। ছবিকে decode করে নতুন PNG বানানো হলো, তাই লুকানো বাড়তি অংশ আর ব্যক্তিগত metadata বাদ গেল। নথির জন্য একই ধারণাকে CDR বলে। কাজটি sandbox-এ চলে, কারণ parser নিজেই ঝুঁকি।
  5. স্ক্যান। Antivirus জানা ক্ষতিকর জিনিস ধরে। নতুন কিছু ধরতে নাও পারে, তাই এটি একটি স্তর, প্রমাণ নয়। স্ক্যান শেষ না হওয়া পর্যন্ত ফাইল অন্য কেউ পাবেন না।
  6. নতুন নাম। Server CSPRNG দিয়ে এলোমেলো নাম বানাল, আর ফাইল গেল object storage-এ বা web root-এর বাইরে। ফলে নাম আন্দাজ, overwrite আর path traversal, তিনটিই কমে।
  7. রেকর্ড। Database-এ লেখা হলো: মালিক, মূল নাম, ধরন, আকার, hash, স্ক্যানের ফল। Download-এর অধিকার এখান থেকেই যাচাই হবে।
  8. পরিবেশন। আলাদা domain থেকে, Content-Disposition: attachment আর X-Content-Type-Options: nosniff দিয়ে। ফলে browser ফাইলটি মূল সাইটের পাতা হিসেবে চালায় না।

এবার প্রথম কয়েকটি ধাপ কোডে। নিচের ফাইলগুলো ছোট object, কিছু জমা বা খোলা হয় না। claims ঘরটি client-এর ঘোষণা, কোড সেটি ইচ্ছা করেই পড়ে না। এলোমেলো নাম এখানে seed দেওয়া PRNG থেকে, শুধু একই আউটপুট পেতে। আসল system-এ CSPRNG ব্যবহার করুন।

// Toy upload check: files are small objects, nothing is stored or opened.
function mulberry32(a) {
  return function () {
    a = a + 0x6D2B79F5 | 0;
    let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}
const rand = mulberry32(42);   // seeded only so the output repeats; use a CSPRNG in production
const MAX = 5 * 1024 * 1024;   // 5 MiB
const MAGIC = { png: [0x89, 0x50, 0x4E, 0x47], jpg: [0xFF, 0xD8, 0xFF], pdf: [0x25, 0x50, 0x44, 0x46] };

function serverName(ext) {
  let s = '';
  for (let i = 0; i < 4; i++) s += Math.floor(rand() * 2 ** 32).toString(16).padStart(8, '0');
  return s + '.' + ext;
}
function check(f) {
  if (f.size === 0 || f.size > MAX) return 'REJECT size ' + f.size;
  const ext = f.name.split('.').pop().toLowerCase();
  const sig = MAGIC[ext];
  if (!sig) return 'REJECT extension .' + ext;
  if (!sig.every((b, i) => f.head[i] === b)) return 'REJECT bytes are not .' + ext;
  return 'ACCEPT store as ' + serverName(ext);
}
const files = [
  { name: 'photo.png', claims: 'image/png', size: 120000, head: [0x89, 0x50, 0x4E, 0x47] },
  { name: 'CV.PDF', claims: 'application/pdf', size: 300000, head: [0x25, 0x50, 0x44, 0x46] },
  { name: 'tool.exe', claims: 'image/png', size: 90000, head: [0x4D, 0x5A, 0x90, 0x00] },
  { name: 'fake.jpg', claims: 'image/jpeg', size: 90000, head: [0x4D, 0x5A, 0x90, 0x00] },
  { name: 'big.png', claims: 'image/png', size: 9000000, head: [0x89, 0x50, 0x4E, 0x47] },
  { name: 'empty.jpg', claims: 'image/jpeg', size: 0, head: [] },
];
for (const f of files) console.log(f.name.padEnd(10), f.claims.padEnd(16), check(f));

আসল আউটপুট:

photo.png  image/png        ACCEPT store as 99e1ef7c72c32b8ada3b32c0ab73b0ad.png
CV.PDF     application/pdf  ACCEPT store as 2cc09a8a86cec4d345f245149fef4401.pdf
tool.exe   image/png        REJECT extension .exe
fake.jpg   image/jpeg       REJECT bytes are not .jpg
big.png    image/png        REJECT size 9000000
empty.jpg  image/jpeg       REJECT size 0

tool.exe নিজেকে image/png বলে দাবি করেছে, কিন্তু কোড সেই দাবি দেখেইনি। fake.jpg-এর extension ঠিক, ঘোষণাও ঠিক, কিন্তু byte মেলেনি। CV.PDF বড় হাতের হলেও পাস করেছে, আর সংরক্ষিত নামে মূল নামের কিছুই নেই। এই কোড শুধু প্রথম স্তর। পাস করা মানে “ক্ষতিকর নয়” নয়, মানে শুধু “প্রথম যাচাই পেরিয়েছে”।

পরিবেশনের দিকটিও ঠিক করতে হয়। নিচে nginx-এর একটি উদাহরণ, আপনার নিজের server-এর জন্য। internal থাকায় এই পথে কেউ সরাসরি ঢুকতে পারেন না, অধিকার যাচাইয়ের পর শুধু app এখানে পাঠাতে পারে। চালানোর আগে version-এর documentation মিলিয়ে নিন।

server {
    client_max_body_size 5m;

    location /protected-files/ {
        internal;
        alias /srv/shop/uploads/;
        default_type application/octet-stream;
        add_header Content-Disposition "attachment" always;
        add_header X-Content-Type-Options "nosniff" always;
    }
}

SVG ছবি হলেও XML, তাই ভেতরে script থাকতে পারে। HTML-এর মতো একে আপনার মূল domain থেকে পাতায় দেখাবেন না। Upload ফোল্ডারে script চালানোর ক্ষমতাও বন্ধ রাখুন। আর ফাইলের নাম header-এ বসালে আগে পরিষ্কার করুন, RFC 6266 দেখুন।

৬ · File download আর সরাসরি object reference

Upload নিরাপদ হলেও download দুর্বল হতে পারে। দুটি ভুল সবচেয়ে সাধারণ। একটি হলো unrestricted file download: একটি endpoint ফাইলের নাম নিয়ে যা চাওয়া হয় তাই দেয়, যা আগের অংশের path traversal-এর সাথেই বসে। অন্যটি হলো IDOR: /invoice/1001 দেখা গেলে /invoice/1002-ও খোলে, কারণ মালিকানা যাচাই হয় না। পর্ব ৩৮ এই বিষয়টি গভীরে দেখায়। মূল কথা, প্রতিটি অনুরোধে server-এ অধিকার যাচাই করতে হবে (পর্ব ১৯)।

এলোমেলো নাম কতটা কাজে লাগে, হিসাব করে দেখি। ধরুন ১০ লাখ ফাইল আছে, আর কেউ সেকেন্ডে ১,০০০ বার আন্দাজ করতে পারেন।

নামের ধরনসম্ভাব্য নামের সংখ্যাএক আন্দাজে মেলার সম্ভাবনাগড়ে একটি পেতে
ক্রমিক নম্বর, ১ থেকে ১০ লাখ১০⁶১ (প্রায় সব নম্বরেই ফাইল)প্রথম চেষ্টাতেই; সব দেখতে ১,০০০ সেকেন্ড, প্রায় ১৭ মিনিট
১২৮-bit এলোমেলো নাম২¹²⁸ ≈ ৩.৪ × ১০³⁸১০⁶ ÷ ২¹²⁸ ≈ ২.৯ × ১০⁻³³≈ ৩.৪ × ১০³² আন্দাজ, প্রায় ১.১ × ১০²² বছর

অঙ্কটি দেখায়, এলোমেলো নাম গণনা প্রায় অসম্ভব করে দেয়। কিন্তু এটি authorization নয়। লিংক email-এ, browser history-তে বা log-এ চলে গেলে যে পাবেন, তিনিই খুলতে পারবেন। তাই গোপন ফাইলে অধিকার যাচাই থাকবেই। Object storage ব্যবহার করলে সরাসরি public বানানোর বদলে অল্প সময়ের presigned URL দিন: app আগে অধিকার যাচাই করে, তারপর কয়েক মিনিটের জন্য সই করা লিংক দেয়। বিস্তারিত দেখুন System Design-এর object storage পর্বে।

৭ · Deserialization আর XXE — যখন ডেটা নির্দেশ দেয়

ধরুন আপনি একটি বন্ধ বাক্স পেলেন, আর ভেতরের চিরকুটে লেখা: “যে খুলবেন, তিনি সিন্দুক থেকে টাকা বের করে দিন।” চিরকুট মানার নিয়ম থাকলে সমস্যা। সাধারণ ডেটা শুধু বলে “কী আছে”। কিছু ফরম্যাট বলতে পারে “কী করতে হবে”। এই পার্থক্য থেকেই এই অংশের দুটি ঝুঁকি আসে।

অবিশ্বস্ত ডেটার deserialization

Serialization মানে object-কে byte-এ বদলানো, আর deserialization মানে উল্টো কাজ। Python-এর pickle, Java-র native serialization-এর মতো ফরম্যাট শুধু মান নয়, কোন ধরনের object বানাতে হবে সেটিও ফাইলে রাখে। অবিশ্বস্ত byte খোলার মানে তাই আসলে কোড চালানোর সিদ্ধান্ত অন্যের হাতে তুলে দেওয়া। Python-এর documentation নিজেই সতর্ক করে: pickle শুধু বিশ্বস্ত ডেটায়।

  • নিরাপদ ফরম্যাট বেছে নিন: বাইরের ডেটার জন্য JSON. এটি শুধু মান রাখে, ধরন বা কোড নয়। Python-এর উদাহরণ দেখুন CSV ও JSON পর্বে।
  • Parse-এর পর schema যাচাই: JSON নিরাপদ পথ, কিন্তু “যা এসেছে সব ঠিক” বলে না। Field, ধরন, সীমা যাচাই করুন (পর্ব ৪৩)।
  • না বদলানো গেলে: অখণ্ডতার জন্য HMAC দিয়ে সই যাচাই আগে (পর্ব ৯), অনুমোদিত class-এর allowlist, আর কম অধিকারে চালানো। এগুলো ঝুঁকি কমায়, শূন্য করে না।

XXE — XML-এর বাইরের entity

XML-এ DTD অংশে “entity” ঘোষণা করা যায়, যা নথির ভেতরে অন্য কিছু বসানোর সংক্ষিপ্ত নাম। কোনো কোনো XML parser এমন entity-কে বাইরের ফাইল বা ঠিকানা থেকেও আনতে পারে। অবিশ্বস্ত XML-এ এটি চালু থাকলে XXE ঘটে: parser নিজেই ফাইল পড়তে বা অনুরোধ পাঠাতে পারে, অর্থাৎ SSRF-এর মতো ফলও হতে পারে। DOCX, SVG আর অনেক পুরোনো API-র ভেতরেও XML থাকে, তাই ঝুঁকি শুধু “.xml” ফাইলে আটকে নেই।

প্রতিরক্ষা সরল: দরকার না থাকলে DTD আর external entity বন্ধ করুন। Java-র parser-এ OWASP XXE Prevention Cheat Sheet-এর এই ধরনের সেটিং দেখা যায়। উদাহরণটি শুধু নিরাপদ configuration:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);

ডিফল্ট আচরণ ভাষা আর version-ভেদে আলাদা। Python-এর documentation বলছে, Expat ডিফল্টে local ফাইল পড়ে না বা network সংযোগ করে না, তবে পুরোনো Expat-এ entity-র বিস্তার থেকে denial of service হতে পারে। তাই ব্যবহার করা parser আর তার version নিজে যাচাই করুন, অনুমান করবেন না।

৮ · Archive, আকার আর হারের সীমা

ছোট একটি পোঁটলা, খুললেই ঘর ভরে যায়। Decompression bomb এমন ফাইল। David Fifield-এর “A better zip bomb” (USENIX WOOT ২০১৯) ৪৬ MB-র একটি zip থেকে প্রায় ৪.৫ PB বিস্তার দেখিয়েছিল, অর্থাৎ অনুপাত প্রায় ৯.৮ কোটি গুণ। আবার archive-এর ভেতরের ফাইলের নাম নিজেও অবিশ্বস্ত ডেটা। সেই নাম যাচাই না করলে খোলা ফাইল ফোল্ডারের বাইরে পড়তে পারে। Snyk ২০১৮ সালে এই শ্রেণির নাম দেয় Zip Slip.

নিয়ম হলো: archive খোলা মানে একটি নিয়ন্ত্রিত প্রক্রিয়া চালানো, যার সব মাপে সীমা আছে।

সীমাউদাহরণ মান (আপনার প্রয়োজনমতো বদলান)কেন
মোট খোলা আকার৫০০ MBDisk শেষ হওয়া ঠেকায়
Compression ratio১০০ : ১অস্বাভাবিক বিস্তার ধরে
ফাইলের সংখ্যা১,০০০Inode আর সময় বাঁচায়
ভেতরে archive-এর স্তর০ বা ১বারবার খোলা ঠেকায়
প্রতিটি নামSafe path যাচাই, symlink নিষেধZip Slip ঠেকায়
সময় আর জায়গা৩০ সেকেন্ড, sandboxCrash সীমিত রাখে

একটি হিসাব। ২ MB-র একটি archive-এর header বলছে খুললে ৫,০০০ MB হবে। অনুপাত ৫,০০০ ÷ ২ = ২,৫০০ : ১, সীমার ২৫ গুণ, আর মোট আকার সীমার ১০ গুণ। এটি শুরুতেই বাতিল। কিন্তু header মিথ্যা বলতে পারে। তাই সংখ্যাটি শুধু প্রথম বিচার। আসল সুরক্ষা হলো খোলার সময় প্রকৃত byte গোনা, আর সীমা ছুঁলেই থামা। ২ MB-র জন্য ১০০ : ১ অনুপাতের সীমা মানে ২০০ MB-তে থেমে যাওয়া, যা ৫০০ MB-র আগেই আসে।

আকার আর হারের সীমা একসাথে। এক ফাইলের সীমাই যথেষ্ট নয়। ধরুন ১,০০০ জন ব্যবহারকারী, প্রত্যেকে দিনে ১০টি, প্রতিটি সর্বোচ্চ ২০ MB. সবচেয়ে খারাপ অবস্থায় ১,০০০ × ১০ × ২০ MB = ২,০০,০০০ MB, অর্থাৎ দিনে ২০০ GB. তাই ব্যবহারকারী-প্রতি upload quota, মোট storage-এর সীমা আর পর্ব ২৮-এর ধারণা মেনে অনুরোধের rate limit লাগে। URL fetch-এও একই কথা: timeout, সর্বোচ্চ response আকার, একসাথে কতগুলো fetch, সবকিছুর সীমা রাখুন। Rate limit-এর নকশা দেখুন System Design-এ।

আরও গভীরে: OWASP-এর SSRF Prevention আর File Upload Cheat Sheet এই পর্বের নিয়মগুলো বিস্তারে দেয়, আর CWE-918, CWE-22, CWE-434, CWE-502, CWE-611 এই ত্রুটিগুলোর মান্য নাম। ভাষার ডিফল্টও বদলায়: Python 3.14-এ tarfile-এর ডিফল্ট extraction filter হয়েছে data (নকশা PEP 706), যা বিপজ্জনক link আর গন্তব্যের বাইরের path আটকায়, যদিও documentation বলে এটি সব ঝুঁকি ঠেকায় না। URL parser-এর পার্থক্যও আলাদা গবেষণার বিষয়, তাই যাচাই আর ব্যবহারে একই parser রাখুন।

৯ · ধরা আর সাড়া — রক্ষকের চোখ কোথায় রাখবেন

প্রতিরোধ শতভাগ হয় না, তাই প্রতিটি নিয়ম একটি log-এর সূত্রও। Fetch service-এর প্রত্যাখ্যান, path-এর প্রত্যাখ্যান আর upload-এর প্রত্যাখ্যান তিনটিই নিরাপত্তার সংকেত। কী রাখবেন, কতদিন রাখবেন, তা পর্ব ৫৫-এ আছে। এখানে ব্যবহারিক তালিকা:

SSRF-এর সংকেত

App server থেকে ভেতরের বা link-local range-এ সংযোগ। একই ব্যবহারকারীর অনেক প্রত্যাখ্যাত fetch. Metadata সেবায় অস্বাভাবিক প্রক্রিয়ার ডাক। Workload credential-এর অচেনা জায়গা থেকে ব্যবহার।

Path আর download-এর সংকেত

একই session-এ বহু 404. Download-এ ক্রমিক id-র ধারা। অস্বাভাবিক পরিমাণে ফাইল নামানো।

Upload-এর সংকেত

Byte না মেলায় বারবার প্রত্যাখ্যান। Upload ফোল্ডারে চালানো যায় এমন নতুন ফাইল। Web process থেকে অচেনা child process (পর্ব ২৯)।

নিচে একটি Sigma-ধাঁচের শেখার উদাহরণ। decision আর reason আপনার নিজের application log-এর field ধরে নেওয়া হয়েছে, তাই নিজের log অনুযায়ী বদলান।

title: Fetch service refused an internal destination (teaching example)
status: experimental
logsource:
  product: hatbazar_fetch_service
  category: application
detection:
  selection:
    decision: refused
    reason|contains:
      - 'internal'
      - 'link-local'
  condition: selection
falsepositives:
  - Partner DNS record pointing to a private address by mistake
level: high

ধরা পড়লে: সেই feature সাময়িক বন্ধ করুন বা network স্তরে পথ আটকান। Cloud-এর credential নিয়ে সন্দেহ হলে দ্রুত rotate করুন আর সেই role-এর audit log দেখুন। সন্দেহজনক upload মুছে না ফেলে আলাদা করুন, যাতে প্রমাণ থাকে। ঘটনা সামলানোর পূর্ণ ধাপ পর্ব ৬১-তে। আর পরীক্ষা শুধু নিজের staging পরিবেশে বা লিখিত অনুমতিতে (পর্ব ৫০), কারণ আসল সেবায় অনুমতি ছাড়া এসব চেষ্টা বেআইনি হতে পারে।

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

  • SSRF কী? ব্যবহারকারীর দেওয়া গন্তব্যে server নিজের পরিচয়ে অনুরোধ পাঠানো, যা ভেতরের সম্পদে পৌঁছাতে পারে।
  • Allowlist আর denylist-এর কোনটি SSRF-এ ভালো? Allowlist, কারণ এটি default deny; denylist অসম্পূর্ণ হতে বাধ্য।
  • Path traversal ঠেকানোর মূল ধাপ? Canonicalize করে base-এর ভেতরে আছে কি না যাচাই, ভালো হয় server-এর বানানো নাম।
  • Upload-এ Content-Type কেন যথেষ্ট নয়? এটি client-এর ঘোষণা, তাই byte আর আসল parse দিয়ে যাচাই লাগে।
  • XXE ঠেকানোর উপায়? Parser-এ DTD আর external entity বন্ধ করা, অথবা JSON-এর মতো সরল ফরম্যাট।

১০ · AI-এর যুগে — URL আর ফাইলের কোডে AI-কে কতটা বিশ্বাস

AI খুব দ্রুত “ছবি upload বা URL থেকে আনা”-র কাজ চালানোর মতো কোড লিখে দেয়। কিন্তু কাজ চলা আর নিরাপদ হওয়া আলাদা। নিরাপত্তার শর্ত প্রম্পটে না থাকলে কোডেও সাধারণত থাকে না। সে অচেনা CVE নম্বর বানাতে পারে, TLS error সরাতে যাচাই বন্ধ করতে বলতে পারে, আর পুরোনো পরামর্শ আত্মবিশ্বাসের সাথে দিতে পারে।

AI যা লিখতে পারেকী ভুলযা চাইবেন
URL সরাসরি HTTP client-এ, redirect চালুSSRF-এর ক্লাসিক ধরনAllowlist, IP যাচাই, redirect বন্ধ
verify=False দিয়ে error সরানোTLS যাচাই নেইআসল certificate সমস্যা ঠিক করা
মূল ফাইলের নামেই disk-এ রাখাTraversal, overwriteএলোমেলো নাম, ফোল্ডারের বাইরে রাখা
Content-Type দেখে ধরন ঠিক করাClient-এর কথা বিশ্বাসByte আর আসল parse
Upload ফোল্ডারে chmod 777সবার লেখা ও চালানোর অধিকারসর্বনিম্ন অধিকার
দ্রুত কাজে pickle বা yaml.loadঅবিশ্বস্ত ডেটার ঝুঁকিJSON বা নিরাপদ loader

AI-কে দিয়ে নিজের কোড review করান। নিচের প্রম্পট defensive, আর এতে কোনো আসল secret বা URL দেবেন না:

তুমি একজন সতর্ক application security reviewer. নিচের কোড আমার নিজের প্রজেক্টের। আসল secret আর ঠিকানা বদলে দিয়েছি। [এখানে URL fetch বা file upload-এর কোড পেস্ট করুন] ১. SSRF, path traversal, file upload, deserialization আর XXE, এই পাঁচ দিক থেকে দুর্বলতা খোঁজো। ২. প্রতিটি সমস্যার জন্য লাইন উল্লেখ করে কারণ বলো আর নিরাপদ পরিবর্তন দাও। ৩. কোনো হামলার payload লিখবে না, শুধু রক্ষণাত্মক সমাধান ও test case-এর ধারণা দেবে। ৪. কোনো CVE বা standard উল্লেখ করলে উৎস দেবে। নিশ্চিত না হলে "নিশ্চিত নই" বলবে।

AI-এর কোড নিজের হাতে যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Capital One ২০১৯-এর প্রকাশিত প্রতিবেদন থেকে “একটি ভুল” নাকি “একসাথে অনেক ভুল”, কোন শিক্ষাটি নেবেন?

প্রথমে সীমাটি মানি: আমরা শুধু প্রকাশিত প্রতিবেদনের স্তরে কথা বলছি, আর সব বিবরণ সর্বজনীন নয়। Krebs on Security-র প্রতিবেদনে একটি ভুলভাবে সাজানো web application firewall, SSRF ধরনের সমস্যা আর প্রয়োজনের চেয়ে বেশি অধিকারের কথা আসে। Amazon বলেছিল তাদের মূল সেবা কারণ নয়, কারণ ছিল firewall-এর configuration. মার্কিন নিয়ন্ত্রক OCC ২০২০ সালে যে ৮ কোটি ডলার জরিমানা ঘোষণা করে, তার ভাষ্য আরও বড় ছবি দেখায়: বড় IT কাজ public cloud-এ নেওয়ার আগে কার্যকর ঝুঁকি মূল্যায়ন হয়নি, আর ফাঁক সময়মতো ঠিক হয়নি।

এখান থেকে তিনটি প্রকৌশল শিক্ষা আসে। এক, নিরাপত্তার যন্ত্র নিজেও একটি attack surface. Firewall বা proxy-কে যে অধিকার দিচ্ছেন, সেটিও least privilege মানতে হবে। দুই, শিকলের প্রতিটি সংযোগ একেকটি সুযোগ: কঠোর metadata ব্যবস্থা, role-এ কম অধিকার, egress নিয়ম, data access-এর ওপর alert, যেকোনো একটি ক্ষতি কমাতে পারত। তিন, প্রযুক্তিগত ভুলের পেছনে প্রক্রিয়ার ভুল থাকে: কে review করল, কে cloud-এর ঝুঁকি মূল্যায়ন করল, কে alert দেখল।

AWS-এর IMDSv2 এই ঘটনার কয়েক মাস পরে আসে, আর AWS নিজেই একে defense in depth বলে উপস্থাপন করে। একটি প্ল্যাটফর্ম পরিবর্তন অনেক গ্রাহকের ঝুঁকি কমায়, কারণ এটি নিরাপদ পথকে সহজ করে। তবে নতুন ব্যবস্থা ব্যবহার করতে হয়, পুরোনোটি বন্ধ করতে হয়, নইলে সুবিধা আসে না।

মূল উপলব্ধি: বড় ঘটনার পেছনে সাধারণত একটি জাদুকরী দুর্বলতা নয়, কয়েকটি সাধারণ ফাঁকের যোগফল থাকে। তাই প্রশ্ন হওয়া উচিত “এই স্তর ভেঙে গেলে পরের স্তর কী?”

প্র ০২Webhook বা link preview-তে যেকোনো URL নিতেই হয়, allowlist সম্ভব নয়। তাহলে নকশা কেমন হবে?

এটি SSRF-এর সবচেয়ে কঠিন বাস্তব অবস্থা। ব্যবসার দাবি হলো অচেনা গন্তব্যে যাওয়া, তাই “শুধু পরিচিত host” নিয়ম খাটে না। এখানে কৌশল বদলায়: গন্তব্য সীমিত করা যাচ্ছে না, তাই অনুরোধকারীর ক্ষমতা সীমিত করুন।

  • আলাদা সেবা: Fetch কাজ আলাদা process বা আলাদা network-এ, যেখান থেকে database, admin panel বা metadata সেবায় network পথই নেই। এটি কোডের যাচাই ভুল হলেও টিকে থাকে।
  • পরিচয়হীন: এই সেবায় কোনো cloud role বা ভেতরের credential নেই। চুরি করার কিছু না থাকলে ক্ষতিও কম।
  • Egress শুধু public-এ: Forward proxy-তে private, loopback আর link-local বন্ধ, প্রতিটি সংযোগের গন্তব্য IP সেখানেই যাচাই।
  • সীমা: Timeout, আকার, হার আর সম্মিলিত সংখ্যার সীমা।
  • উত্তর ফেরত না দেওয়া: Webhook-এ শুধু “সফল” বা “ব্যর্থ”, পুরো response নয়। ভেতরের কোনো বিবরণ ফাঁস হয় না।

Denylist এখানে সহায়ক স্তর, কিন্তু প্রধান ভরসা নয়। ঠিকানার নানা রূপ আর নতুন range থাকায় তালিকা অসম্পূর্ণ থাকতে পারে। IP-কে library দিয়ে parse করে IPv6-সহ যাচাই করাই সঠিক অভ্যাস। সমান গুরুত্বের কথা হলো ভেতরের সেবাগুলোকেও “যেহেতু ভেতরে, তাই নিরাপদ” না ভাবা। পর্ব ২৫-এর zero trust চিন্তা অনুযায়ী ভেতরের সেবাও নিজ দায়িত্বে authentication চায়। তাহলে কোনো অনুরোধ পৌঁছে গেলেও সে কিছু পাবে না। এই নকশার দাম হলো বাড়তি সেবা, proxy আর পরিচালনার খরচ, তবে ঝুঁকি সীমিত হয়।

মূল উপলব্ধি: নিয়ম যখন সম্পূর্ণ হতে পারে না, তখন নকশা এমন করুন যেন নিয়ম ভাঙলেও ক্ষতি ছোট থাকে। অধিকার আর network-এর সীমা কোডের if-এর চেয়ে শক্ত।

প্র ০৩JSON নিরাপদ, pickle বিপজ্জনক। তাহলে কি serialization ফরম্যাট বেছে নিলেই সমস্যা শেষ?

না, তবে সবচেয়ে বড় ঝুঁকি অনেকটা কমে। pickle বা Java-র native serialization-এর ফরম্যাট নকশা অনুযায়ী object-এর ধরন ফিরিয়ে আনে। অর্থাৎ খোলার সময়ই প্রোগ্রামের ভেতরের কোড চলতে পারে। ২০১৫ সালের “Marshalling Pickles” আলোচনা দেখায় বড় library-র সাধারণ class জোড়া দিয়েই বিপজ্জনক আচরণ তৈরি হতে পারে, আর তারপর Java জগতে এই শ্রেণি অনেক বেশি মনোযোগ পায়। Python-এর documentation স্পষ্ট বলে, pickle শুধু বিশ্বস্ত ডেটায়।

JSON শুধু মান রাখে: লেখা, সংখ্যা, তালিকা, map. এতে “এই class বানাতে হবে” বলার কিছু নেই, তাই parse করলে নিজে থেকে কোড চলে না। কিন্তু তিনটি ঝুঁকি থেকে যায়।

  • যুক্তির ত্রুটি: JSON-এর "role": "admin" field আপনি সরাসরি object-এ বসালে সেটি অন্য নামের সমস্যা। তাই schema দিয়ে যাচাই আর অনুমোদিত field-এর তালিকা লাগে।
  • সম্পদের ব্যবহার: খুব বড় বা গভীর কাঠামো memory আর CPU খরচ করে। আকার আর গভীরতার সীমা লাগে।
  • Library-র বিশেষ সুবিধা: কিছু library JSON-এর ওপর “type” বসিয়ে polymorphic deserialization দেয়। সেটি চালু করলে আবার পুরোনো ঝুঁকি ফেরে।

YAML-এও সাবধান থাকতে হয়। Library-ভেদে এমন loader আছে, যা ভাষার object বানাতে পারে। Python-এর PyYAML-এ অবিশ্বস্ত ডেটায় safe_load ব্যবহারের পরামর্শ এজন্যই। পুরোনো সিস্টেম একদিনে বদলানো না গেলে HMAC দিয়ে ডেটার অখণ্ডতা যাচাই, allowlist-ভিত্তিক class filter আর কম অধিকারে চালানো মধ্যবর্তী পথ।

মূল উপলব্ধি: ফরম্যাট বাছাই ঠিক করে ডেটা কতটা “ক্ষমতা” পাবে। সবচেয়ে কম ক্ষমতার ফরম্যাট বেছে নিন, তারপরও প্রতিটি মান যাচাই করুন।

প্র ০৪ছবি তো শুধু ছবি। তাহলে upload-করা ছবি নিয়ে এত সতর্কতা কেন?

ছবি মানে একটি জটিল ফরম্যাট, আর সেটি পড়ার জন্য বড় parser library কাজ করে। জটিল parser-এর bug-ই এখানে ঝুঁকি। ২০১৬ সালে ImageMagick-এর “ImageTragick” (CVE-2016-3714) দেখায় ছবি processing-এর ধাপ কতটা বিপজ্জনক হতে পারে। ২০২৩ সালে libwebp-এর heap buffer overflow (CVE-2023-4863) ধরা পড়ে, যা CISA-র তালিকায় সক্রিয়ভাবে ব্যবহৃত হিসেবে আসে। এই library অসংখ্য browser আর app-এ থাকায় patch ছড়াতে হয়েছিল বহু জায়গায়। পর্ব ৩২-এর মতো এখানেও আসল কাজ dependency-র হিসাব রাখা।

তাই ছবি নিলে প্রথম নিয়ম হলো ধরে নিন parser ভাঙতে পারে।

  • আলাদা করে চালান: ছবি processing আলাদা process বা container-এ, কম অধিকারে, network ছাড়া, সময় আর memory-র সীমা দিয়ে। Crash হলেও মূল app বাঁচে।
  • Pixel সীমা: ফাইল ছোট হলেও pixel বিশাল হতে পারে। তাই decode-এর আগে মাপ দেখুন। Pillow-র মতো library-তে এজন্য decompression bomb সতর্কতা আছে।
  • ফরম্যাট কমান: শুধু দরকারি দুই-তিনটি ধরন। প্রতিটি নতুন ফরম্যাট মানে নতুন parser.
  • পুনর্গঠন: নতুন করে encode করলে লুকানো অংশ আর ব্যক্তিগত metadata বাদ যায়। Location-এর তথ্য থাকা ছবি প্রকাশে গোপনীয়তাও রক্ষা হয়। তবে পুনর্গঠনের ধাপ নিজেই parser চালায়, তাই sandbox জরুরি।

SVG আলাদা গল্প। এটি pixel নয়, XML, তাই XXE আর script দুই ঝুঁকিই আসে। অনেক সাইট SVG-কে upload-এ নেয়ই না, বা নিয়ে কঠোরভাবে পরিষ্কার করে আলাদা domain থেকে দেখায়।

মূল উপলব্ধি: ফাইল শুধু ডেটা নয়, এটি parser-এর জন্য ইনপুট। যত জটিল parser, তত ছোট রাখুন তার অধিকার।

প্র ০৫Python 3.14-এ tarfile-এর ডিফল্ট বদলানো থেকে নিরাপদ ডিফল্ট সম্পর্কে কী শেখা যায়?

Archive-এর path ঝুঁকি নতুন কিছু নয়। Python-এর tarfile-এর এই সমস্যা বহু বছর আগে থেকে জানা ছিল (CVE-2007-4559), আর ২০১৮ সালে Snyk “Zip Slip” নামে নানা ভাষা ও library-তে একই শ্রেণি দেখায়। কারণ বোঝা সহজ: archive-এর ভেতরের নামকে অনেক প্রোগ্রামার ফাইলের নাম ভাবেন, অবিশ্বস্ত ইনপুট ভাবেন না। ডকুমেন্টে সতর্কবার্তা থাকলেও ব্যস্ত ডেভেলপার তা পড়েন না, আর অনেক উদাহরণ কোড সরাসরি extractall ডাকে।

PEP 706 extraction filter-এর নকশা আনে। Python documentation অনুযায়ী 3.14-এ ডিফল্ট হয়েছে data filter, যা বাইরে নির্দেশ করা link আর গন্তব্যের বাইরের path-এর মতো বিপজ্জনক জিনিস আটকায়। আগে ডিফল্ট আচরণ ছিল fully_trusted ধরনের। পরিবর্তনটি ধাপে ধাপে এসেছে, কারণ পুরোনো কোড ভাঙার ঝুঁকি ছিল। এটি secure by default-এর ক্লাসিক উদাহরণ: সঠিক কাজটি ডিফল্টে থাকলে মানুষকে মনে রাখতে হয় না।

দুটি সতর্কতা আছে। Documentation নিজেই বলে, এই filter সব অনিরাপদ আচরণ ঠেকায় না। Decompression bomb-এর মতো আকারের ঝুঁকি আপনাকেই সামলাতে হবে। Zip বা অন্য ভাষার library-র আচরণও আলাদা হতে পারে। আর পুরোনো Python-এ এই ডিফল্ট নেই, তাই server-এ কোন version চলে তা দেখুন।

আপনার নিজের API-তেও একই চিন্তা খাটে। Upload helper-এর ডিফল্ট কি attachment? File-পড়া function কি base ছাড়া চলে? Fetch client-এর ডিফল্টে redirect বন্ধ? নিরাপদ পথটি কম কোডে হলে দলের সবাই সেটিই ব্যবহার করবেন। পর্ব ৪৬ (Secure coding)-এর নিয়মগুলো এই চিন্তাকেই সাধারণ করে।

মূল উপলব্ধি: ডকুমেন্টের সতর্কবার্তা ঝুঁকি কমায় সামান্য, ডিফল্ট বদলানো কমায় অনেক। তবে ডিফল্ট বিশ্বাসের আগে যাচাই করুন, ঠিক কী আটকায় আর কী আটকায় না।

অনুশীলন

শুধু নিজের system বা খেলনা উদাহরণে করুন। আগে নিজে চেষ্টা করুন, তারপর উত্তর দেখুন।

  1. হিসাব: নীতি হলো মোট সীমা ৫০০ MB আর অনুপাতের সীমা ১০০ : ১. একটি ৩ MB archive খুলতে শুরু হলো। (ক) কত MB-তে থামবে? (খ) ২,০০০ ব্যবহারকারী দিনে ৫টি করে সর্বোচ্চ ৮ MB ফাইল পাঠালে সবচেয়ে খারাপ অবস্থায় ৩০ দিনে কত storage লাগবে?

    (ক) অনুপাতের সীমা ৩ × ১০০ = ৩০০ MB, যা ৫০০ MB-র চেয়ে কম। তাই ৩০০ MB প্রকৃত byte গোনা হলেই থামবে।

    (খ) ২,০০০ × ৫ × ৮ MB = ৮০,০০০ MB = দিনে ৮০ GB. ৩০ দিনে ৮০ × ৩০ = ২,৪০০ GB = ২.৪ TB. এই সংখ্যা quota ঠিক করতে আর storage-এর alert বসাতে কাজে লাগে।

  2. Code review: একটি কাল্পনিক দোকানের profile picture feature-এর কোড: r = http.get(user_url, follow_redirects=True, verify=False), তারপর open("/var/www/html/uploads/" + name, "wb"). name আসে ব্যবহারকারীর কাছ থেকে। কী কী ঝুঁকি, আর কীভাবে নতুন করে নকশা করবেন?
    • ঝুঁকি: URL যাচাই নেই আর redirect চালু, তাই SSRF. TLS যাচাই বন্ধ। নাম জোড়া দেওয়ায় path traversal আর overwrite. Web root-এ রাখায় চালানো যায় এমন ফাইলের ঝুঁকি। আকার বা ধরনের সীমা নেই।
    • নকশা: সম্ভব হলে URL-এর বদলে সরাসরি upload নিন। URL লাগলে আলাদা fetch service, allowlist বা public-only egress, IP যাচাই, redirect বন্ধ, TLS চালু, timeout আর আকারের সীমা।
    • ফাইল: Magic bytes, পুনর্গঠন, এলোমেলো নাম, object storage বা web root-এর বাইরে রাখা, আর পরিবেশনে আলাদা domain ও nosniff.
  3. আউটপুট অনুমান: Python 3.14-এ এই লাইনগুলোর আউটপুট কী? (নিচে ip মানে ipaddress.ip_address)
    print(ip("10.1.2.3").is_private)
    print(ip("169.254.1.1").is_link_local)
    print(ip("100.64.0.1").is_private)
    print(ip("203.0.113.9").is_global)

    আউটপুট যথাক্রমে True, True, False, False (নিজে চালিয়ে মেলানো)। প্রথমটি private, দ্বিতীয়টি link-local. তৃতীয়টি shared range, private নয়, global-ও নয়। চতুর্থটি documentation range, তাই global নয়। শিক্ষা: কোন property কী বোঝায় তা documentation দেখে নিশ্চিত হোন, আর শেষে default deny রাখুন।

  4. নিজের server-এর audit: শুধু নিজের server-এ, upload ফোল্ডারে (ধরুন /srv/shop/uploads) চালানোর অধিকারওয়ালা ফাইল আর script-এর মতো extension কীভাবে খুঁজবেন? কিছু পেলে কী করবেন?

    এই কমান্ড দুটি শুধু তালিকা দেখায়, কিছু বদলায় না:

    find /srv/shop/uploads -type f -perm /111 -print
    find /srv/shop/uploads -type f \( -name '*.php' -o -name '*.jsp' -o -name '*.html' \) -print

    ফাইল পেলে সাথে সাথে মুছবেন না। আগে ফাইলটি আলাদা করে hash আর সময় লিখে রাখুন, log-এ দেখুন কে কখন upload করল, প্রয়োজনে incident process চালু করুন। তারপর কারণ ঠিক করুন: allowlist, অধিকার, আর upload ফোল্ডারে script চালানো বন্ধ।

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

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

ক্রমএকটি নিরাপদ upload pipeline-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. আকারের সীমা যাচাই
  2. Extension allowlist আর magic bytes মেলানো
  3. Sandbox-এ পুনর্গঠন আর স্ক্যান
  4. এলোমেলো নাম দিয়ে web root-এর বাইরে রাখা
  5. অধিকার যাচাই করে attachment হিসেবে পরিবেশন

একটি app ব্যবহারকারীর দেওয়া URL থেকে ছবি আনে। SSRF ঝুঁকির মূল কারণ কোনটি?

  • ব্যবহারকারীর browser-এ অচেনা script চলে
  • Server নিজের পরিচয় ও network অবস্থান নিয়ে অযাচাই করা গন্তব্যে যায়
  • ছবির ফাইল সবসময় ক্ষতিকর হয়
  • HTTPS ব্যবহার করা হয়েছে

একটি host allowlist-এ আছে, কিন্তু তার DNS উত্তর এখন 10.0.0.8 দেখাচ্ছে। Fetch service কী করবে?

  • Host তালিকায় আছে, তাই সংযোগ করবে
  • সংযোগের ঠিক আগে আবার DNS করে যা আসে সেখানেই যাবে
  • অনুরোধ বাতিল করে log রাখবে
  • Redirect চালু করবে

ব্যবহারকারীর দেওয়া নাম দিয়ে ফাইল খোলার আগে নিরাপদ যাচাই কোনটি?

  • আসল পূর্ণ path বের করে base ফোল্ডারের ভেতরে আছে কি না অংশ ধরে যাচাই
  • নামের ভেতর থেকে নির্দিষ্ট অক্ষর মুছে দেওয়া
  • শুধু browser-এর JavaScript-এ নাম যাচাই
  • পূর্ণ লেখাটি base দিয়ে শুরু কি না দেখা

Upload-এর সময় client Content-Type: image/png পাঠাল। এই তথ্য সম্পর্কে কোন কথাটি ঠিক?

  • Browser পাঠিয়েছে, তাই এটি নিশ্চিত প্রমাণ
  • এটি থাকলে আর কোনো যাচাই লাগে না
  • এটি antivirus-এর ফল
  • এটি client-এর ঘোষণা, server-কে byte দেখে আলাদাভাবে যাচাই করতে হয়

অবিশ্বস্ত উৎস থেকে আসা ডেটা পড়ার জন্য সাধারণভাবে কোন পছন্দটি সবচেয়ে নিরাপদ?

  • Python-এর pickle
  • JSON parse করে schema যাচাই করা
  • Java native serialization
  • External entity চালু রেখে XML parse

২ MB-র একটি archive-এর জন্য অনুপাতের সীমা ১০০ : ১ আর মোট আকারের সীমা ৫০০ MB. খোলা শুরু হলে সর্বোচ্চ কত MB পর্যন্ত যাবে?

  • ২ MB
  • ৫০০ MB
  • ২০০ MB
  • ৫০,০০০ MB

শব্দকোষ

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

মনে রাখুন

  1. SSRF: Server-এর পরিচয় ধার করে ভেতরে পৌঁছানো। Cloud metadata থাকলে ঝুঁকি বড়, IMDSv2 আর কম অধিকারের role দুটোই লাগে।
  2. URL fetch: Allowlist, প্রতিটি IP যাচাই, সেই IP-তেই সংযোগ, redirect বন্ধ, egress firewall, আলাদা fetch service.
  3. Path: আসল path বের করে base-এর সাথে অংশ ধরে মেলান। ভালো হয় যদি নামটি server নিজেই বানায়।
  4. Upload ও download: ঘোষণা নয়, byte; মূল নাম নয়, এলোমেলো নাম; web root নয়, object storage বা বাইরের ফোল্ডার; আর প্রতিটি অনুরোধে অধিকার যাচাই।
  5. Parser ও সীমা: JSON আর schema, XML-এ external entity বন্ধ, archive-এ আকার, অনুপাত, সংখ্যা আর path-এর সীমা, সবখানে rate limit.

পরের পর্বে: এবার পুরো web app-এর দরজা, অর্থাৎ API. দেখব OWASP API Top 10, rate limit আর schema যাচাই কীভাবে একসাথে কাজ করে।

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

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