Secure Code Review — যেভাবে ভাঙা যায়, সেভাবেই পড়া
Secure Code Review: Reading Code Like an Attacker
Security review মূলত একটা mindset shift — "এটা কাজ করে কিনা" থেকে "এটা কীভাবে ভাঙা যায়" — আর সেই shift-কে trust-boundary-ভিত্তিক checklist দিয়ে ব্যবহারযোগ্য বানানো যায়; একটা ৩৫-লাইনের বাস্তবসম্মত কোডে SQL injection, IDOR, আর unsalted password hash — তিনটা bug হাতে-কলমে খুঁজে, module-এর আগের লেসনগুলোর নির্দিষ্ট mitigation দিয়ে ফিক্স করে দেখানো হয়েছে, আর দেখানো হয়েছে কেন এই দক্ষতা একবার শেখা checklist না, বরং প্রতিটা diff পড়ার একটা স্থায়ী অভ্যাস।
আগে এটা বুঝি
উনিশটা লেসন ধরে এই module-এ একই প্যাটার্ন বারবার ফিরে এসেছে: একটা primitive বা প্যাটার্ন শেখা হয়েছে (hash function, RBAC, TLS, buffer bound-check), তারপর দেখানো হয়েছে সেটা ভাঙলে কী হয়, তারপর একটা সুনির্দিষ্ট mitigation দেওয়া হয়েছে। আজকের লেসন নতুন কোনো primitive শেখায় না — আজ প্রশ্নটা উল্টে দেওয়া হচ্ছে। এতদিন প্রশ্ন ছিল “এই primitive-টা কীভাবে কাজ করে, আর কীভাবে সঠিকভাবে ব্যবহার করব”। আজকের প্রশ্ন: কারো লেখা কোড সামনে থাকলে, সেই উনিশটা লেসনের জ্ঞান একসাথে কীভাবে প্রয়োগ করব — সিস্টেমেটিকভাবে, একটার পর একটা bug miss না করে?
এইখানেই একটা mindset shift ঘটে যেটা প্রতিটা অভিজ্ঞ security-conscious engineer-এর কাজের ভিত্তি। একজন developer কোড পড়ে যে প্রশ্নটা স্বাভাবিকভাবে জিজ্ঞেস করেন — “এটা কাজ করে কিনা” (does it compile, does it pass tests, does it return the right value for the happy path) — সেটা প্রয়োজনীয়, কিন্তু security review-এর প্রশ্ন সম্পূর্ণ ভিন্ন: “এটা কীভাবে ভাঙা যায়” (how would I, as an attacker, misuse this input, this endpoint, এই memory-access, এই dependency)। Michael Howard ও David LeBlanc তাঁদের ক্লাসিক বই Writing Secure Code-এ এই পার্থক্যটাকেই Microsoft-এর নিজস্ব Security Development Lifecycle-এর কেন্দ্রে বসিয়েছিলেন — কোড “সঠিক” হওয়া আর কোড “নিরাপদ” হওয়া দুইটা আলাদা প্রশ্ন, দুইটা আলাদা পড়ার-পদ্ধতি দাবি করে।
Functional review প্রশ্ন করে: এই input-এ এই output ঠিক আসছে কি?
Security review প্রশ্ন করে: কোন input-এ এই কোড ভুল আচরণ করবে,
আর সেই ভুল আচরণ কার জন্য উপকারী?আজকের লেসন এই দ্বিতীয় প্রশ্নটাকে প্রয়োগযোগ্য বানাবে তিনটা ধাপে। প্রথমে একটা checklist — কিন্তু random প্রশ্নের তালিকা না, বরং trust boundary-অনুযায়ী সাজানো, যেখানে module-এর প্রতিটা আগের লেসন একটা নির্দিষ্ট boundary-র নির্দিষ্ট প্রশ্নের সাথে বসে। তারপর একটা সত্যিকারের, বাস্তবসম্মত ছোট কোড — যেখানে তিনটা bug ইচ্ছাকৃতভাবে বসানো, ঠিক সেভাবেই যেভাবে সেগুলো বাস্তব pull request-এ চুপচাপ ঢুকে পড়ে — সেটা লাইনে-লাইনে review করে, exploit করে, আর আগের লেসনের precise mitigation দিয়ে ফিক্স করে দেখানো হবে। শেষে review-কে একটা প্রক্রিয়া হিসেবে — threat-model পাস, automated tooling, আর habitual reading — cover করে module বন্ধ করা হবে।
মূল ধারণা
Trust boundary checklist — প্রতিটা প্রশ্নের পেছনে একটা লেসন
“সব কিছু চেক করুন” — এটা কোনো checklist না, এটা একটা আশা। ব্যবহারযোগ্য checklist-এর একটা কাঠামো লাগে, আর সবচেয়ে শক্তিশালী কাঠামোটা হলো trust boundary: প্রোগ্রামের যে জায়গাগুলোতে ডেটা এক বিশ্বাসের স্তর থেকে আরেক স্তরে পার হয় — network থেকে process-এ, unauthenticated থেকে authenticated-এ, একজন user-এর resource থেকে আরেকজনের-এ, source code থেকে third-party dependency-তে। প্রতিটা boundary-তে একটাই প্রশ্ন করতে হয়: এই পাশের ধরে নেওয়া assumption কি ওই পাশে সত্যিই যাচাই হয়েছে?
নিচের টেবিলটাই আজকের কেন্দ্রীয় হাতিয়ার — প্রতিটা row একটা trust boundary, একটা প্রশ্ন, আর ঠিক কোন আগের লেসন সেই প্রশ্নের উত্তর দিয়েছিল:
| Trust boundary | রিভিউয়ের প্রশ্ন | কোন লেসনে শেখা হয়েছে | সাধারণ code-smell |
|---|---|---|---|
| Untrusted input → interpreter | ডেটা কি কখনো syntax-string-এ concatenate হচ্ছে (SQL/shell/template/HTML)? | injection-attacks, xss-csrf-ssrf | f-string দিয়ে বানানো query/command, innerHTML = userInput |
| Identity claim → পরবর্তী প্রতিটা request | Token/session সঠিকভাবে verify হচ্ছে, শুধু পড়া হচ্ছে না তো? | authentication, digital-signatures-pki | "alg": "none", signature-check বাদ, forge করা যায় এমন claim বিশ্বাস |
| Authenticated request → resource access | এই নির্দিষ্ট resource-টা কি request-কারীর, প্রতি-request যাচাই হচ্ছে? | authorization, threat-modeling-stride | SELECT ... WHERE id = ? — owner_id তুলনা নেই (IDOR) |
| Secret data → storage/transmission | ঠিক primitive ব্যবহার হচ্ছে — encryption reversible-এর জন্য, hash one-way-এর জন্য? | cryptography-fundamentals, symmetric-encryption, asymmetric-crypto, password-storage | XOR/ECB “encryption”, password-এ raw sha256, home-rolled cipher |
| Client ↔ server চ্যানেল | Channel authenticated ও confidential, downgrade-প্রতিরোধী? | tls-security, digital-signatures-pki | plaintext HTTP-তে token, cert-verification disabled |
| Attacker-controlled size/length → memory | ম্যানুয়াল size arithmetic bound-checked, overflow-safe? | memory-safety-buffer-overflow, integer-overflow-vulnerabilities, use-after-free-double-free, mitigations-canaries-aslr-nx-cfi | strcpy/memcpy attacker-length দিয়ে, free-করা pointer পরে ব্যবহার |
| Secret তুলনা → timing | তুলনাটা constant-time, early-exit না? | side-channels-spectre-meltdown, password-storage | secret == guess, branch যা secret-এর মান-নির্ভর |
| Third-party কোড → build/deploy | Dependency pinned-by-hash, patched, প্রয়োজনীয় সুযোগ-সীমিত? | supply-chain-security | floating version, unverified registry, transitive dependency অডিট হয়নি |
| Deployed process → হোস্ট/network | Process least-privilege-এ চলছে, sandbox/seccomp আছে? | sandboxing-isolation, threat-modeling-stride | root-হিসেবে চলা container, unrestricted syscall, overbroad IAM role |
Least privilege-এর নীতিটা এই পুরো টেবিলের একটা meta-প্যাটার্ন হিসেবে বারবার ফিরে আসে — প্রতিটা row-ই আসলে একটা প্রশ্নের রূপভেদ: “এই কোডটুকু যতটুকু access/trust পাচ্ছে, ততটুকুই কি তার সত্যিই দরকার, আর সেই সীমা কি প্রতিবার সক্রিয়ভাবে যাচাই হচ্ছে?” নিচের “hood” সেকশনে এই টেবিলের তিনটা row একই সময়ে, একটা মাত্র ৩৫-লাইনের কোডে ভাঙা দেখানো হবে।
ভেতরে কী ঘটছে
একটা বাস্তবসম্মত রিভিউ — কোড পড়ে বাগ খোঁজা
নিচের কোডটা একটা ছোট “notes” API-এর মূল অংশ — signup, login, আর নিজের note পড়া। দেখতে সম্পূর্ণ স্বাভাবিক, প্রতিটা function কাজ করে (functional review পাস করবে), কিন্তু concept সেকশনের checklist দিয়ে লাইনে-লাইনে পড়লে তিনটা আলাদা trust-boundary-তে তিনটা distinct bug ধরা পড়ে। কোডটা আসলেই চালানো:
# notes_api_vulnerable.py -- একটা ছোট "notes" API, তিনটা bug ইচ্ছাকৃতভাবে বসানো
import sqlite3, hashlib
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, password_hash TEXT)")
conn.execute("CREATE TABLE notes (id INTEGER PRIMARY KEY, owner_id INTEGER, body TEXT)")
def signup(username, password):
# BUG #1: fast general-purpose hash, কোনো salt নেই (password-storage লেসন)
pw_hash = hashlib.sha256(password.encode()).hexdigest()
cur = conn.execute(
"INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, pw_hash)
)
conn.commit()
return cur.lastrowid
def login(username, password):
# BUG #2: raw string-concatenated SQL query (injection-attacks লেসন)
pw_hash = hashlib.sha256(password.encode()).hexdigest()
query = (f"SELECT id, username FROM users WHERE username = '{username}' "
f"AND password_hash = '{pw_hash}'")
return conn.execute(query).fetchone()
def get_note(session_user_id, note_id):
# BUG #3: owner_id-এর সাথে session_user_id কখনো তুলনা হয়নি (authorization/IDOR লেসন)
return conn.execute(
"SELECT id, owner_id, body FROM notes WHERE id = ?", (note_id,)
).fetchone()
alice_id = signup("alice", "alice-secret-99")
bob_id = signup("bob", "bob-hunter2")
conn.execute("INSERT INTO notes (owner_id, body) VALUES (?, ?)", (alice_id, "alice-এর ব্যক্তিগত note"))
conn.execute("INSERT INTO notes (owner_id, body) VALUES (?, ?)", (bob_id, "bob-এর bank-routing number"))
conn.commit()
print("=== EXPLOIT #1 (injection-attacks প্যাটার্ন): SQL injection দিয়ে login bypass ===")
payload_user = "' OR '1'='1' -- "
row = login(payload_user, "whatever-never-checked")
print(f" login({payload_user!r}, ...) -> {row}")
print(f" authentication বাইপাস হলো: {row is not None}")
print()
print("=== EXPLOIT #2 (authorization/IDOR প্যাটার্ন): অন্যের note পড়া ===")
print(f" alice_id={alice_id}, bob_id={bob_id}")
row = get_note(alice_id, 2) # alice-এর session, কিন্তু note-id ২ (bob-এর)
print(f" get_note(session_user_id=alice_id, note_id=2) -> {row}")
print(f" IDOR leak হলো: {row is not None and row[1] != alice_id}")
print()
print("=== EXPLOIT #3 (password-storage প্যাটার্ন): unsalted SHA-256 precomputed dictionary ===")
common_guesses = ["123456", "password", "hunter2", "bob-hunter2", "qwerty"]
precomputed = {hashlib.sha256(g.encode()).hexdigest(): g for g in common_guesses}
stolen = conn.execute("SELECT password_hash FROM users WHERE username='bob'").fetchone()[0]
print(f" stolen password_hash for bob: {stolen}")
print(f" dictionary lookup result : {precomputed.get(stolen)!r}")প্রত্যাশিত output (deterministic — SQLite in-memory auto-increment id আর SHA-256 fixed input দিয়ে হুবহু এটাই আসবে):
=== EXPLOIT #1 (injection-attacks প্যাটার্ন): SQL injection দিয়ে login bypass ===
login("' OR '1'='1' -- ", ...) -> (1, 'alice')
authentication বাইপাস হলো: True
=== EXPLOIT #2 (authorization/IDOR প্যাটার্ন): অন্যের note পড়া ===
alice_id=1, bob_id=2
get_note(session_user_id=alice_id, note_id=2) -> (2, 2, 'bob-এর bank-routing number')
IDOR leak হলো: True
=== EXPLOIT #3 (password-storage প্যাটার্ন): unsalted SHA-256 precomputed dictionary ===
stolen password_hash for bob: 4bbb193bbf021bfe65179c6bb90bc88e457e75415801140d24ee952b3a035f4b
dictionary lookup result : 'bob-hunter2'তিনটা exploit-ই সফল — আর প্রতিটার root cause checklist-এর একটা row-এ
হুবহু বসে যায়। EXPLOIT #1: login()-এ username সরাসরি SQL
query-string-এ concatenate হয়েছে — injection-attacks লেসনের ভাষায়, DATA
আর CODE একই চ্যানেলে মিশেছে, ' OR '1'='1' -- payload password-check-টাই
comment করে ফেলেছে। EXPLOIT #2: get_note() শুধু note_id দিয়ে
lookup করে, owner_id-র সাথে session_user_id কখনো তুলনা করে না —
authorization লেসনের সংজ্ঞা অনুযায়ী এটা classic IDOR, alice কোনো বিশেষ
টুল ছাড়াই bob-এর bank-routing note পড়ে ফেলল। EXPLOIT #3: signup()
password-storage লেসনের misconception-টাই পুনরাবৃত্তি করছে — plain,
fast, unsalted sha256(password), তাই একটা ছোট precomputed dictionary
দিয়েই leaked hash থেকে plaintext password ফেরত পাওয়া গেল, কোনো
brute-force ছাড়াই।
উদাহরণ
ফিক্স — প্রতিটা bug-এর জন্য আগের লেসনের নির্দিষ্ট mitigation
Review-এর কাজ শুধু bug চিহ্নিত করা না — প্রতিটার জন্য সঠিক, module-এ ইতিমধ্যে শেখা mitigation প্রয়োগ করা। নিচের কোডে তিনটা bug-ই ফিক্স করা হয়েছে, প্রতিটা ফিক্স তার নিজের লেসনের mitigation-pattern হুবহু অনুসরণ করে:
# notes_api_fixed.py -- একই তিনটা bug, এবার আগের লেসনগুলোর mitigation দিয়ে ফিক্সড
import sqlite3, bcrypt, hashlib
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, password_hash TEXT)")
conn.execute("CREATE TABLE notes (id INTEGER PRIMARY KEY, owner_id INTEGER, body TEXT)")
def signup(username, password):
# FIX #1 (password-storage লেসন): bcrypt -- নিজেই random salt বসায়, ইচ্ছাকৃত ধীর
pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
cur = conn.execute(
"INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, pw_hash)
)
conn.commit()
return cur.lastrowid
def login(username, password):
# FIX #2 (injection-attacks লেসন): parameterized query -- placeholder + params
row = conn.execute(
"SELECT id, username, password_hash FROM users WHERE username = ?", (username,)
).fetchone()
if row is None:
return None
user_id, uname, stored_hash = row
if not bcrypt.checkpw(password.encode(), stored_hash):
return None
return (user_id, uname)
def get_note(session_user_id, note_id):
# FIX #3 (authorization/IDOR লেসন): resource-নির্দিষ্ট ownership check
row = conn.execute(
"SELECT id, owner_id, body FROM notes WHERE id = ?", (note_id,)
).fetchone()
if row is None or row[1] != session_user_id:
return None
return row
alice_id = signup("alice", "alice-secret-99")
bob_id = signup("bob", "bob-hunter2")
conn.execute("INSERT INTO notes (owner_id, body) VALUES (?, ?)", (alice_id, "alice-এর ব্যক্তিগত note"))
conn.execute("INSERT INTO notes (owner_id, body) VALUES (?, ?)", (bob_id, "bob-এর bank-routing number"))
conn.commit()
print("=== একই EXPLOIT #1 payload, fixed login-এর বিরুদ্ধে ===")
payload_user = "' OR '1'='1' -- "
row = login(payload_user, "whatever-never-checked")
print(f" login({payload_user!r}, ...) -> {row}")
print(f" authentication বাইপাস হলো: {row is not None}")
print()
print("=== sanity check: বৈধ credential আগের মতোই কাজ করে ===")
print(f" login('alice', 'alice-secret-99') -> {login('alice', 'alice-secret-99')}")
print()
print("=== একই EXPLOIT #2 চেষ্টা, fixed get_note-এর বিরুদ্ধে ===")
row = get_note(alice_id, 2)
print(f" get_note(session_user_id=alice_id, note_id=2) -> {row}")
print(f" IDOR ব্লক হলো: {row is None}")
print(f" get_note(session_user_id=alice_id, note_id=1 [নিজের]) -> {get_note(alice_id, 1)}")
print()
print("=== একই EXPLOIT #3 dictionary, bcrypt hash-এর বিরুদ্ধে ===")
common_guesses = ["123456", "password", "hunter2", "bob-hunter2", "qwerty"]
precomputed = {hashlib.sha256(g.encode()).hexdigest(): g for g in common_guesses}
stolen = conn.execute("SELECT password_hash FROM users WHERE username='bob'").fetchone()[0]
print(f" stolen password_hash for bob (bcrypt): {stolen[:29].decode()}...")
print(f" plain SHA-256 dictionary lookup result: {precomputed.get(stolen)!r}")
print(f" (bcrypt hash কখনো SHA-256-dictionary-তে মিলবে না -- ভিন্ন format, ভিন্ন algorithm)")প্রত্যাশিত output (bcrypt নিজে random salt generate করে বলে hash-string-এর
exact byte প্রতিবার আলাদা হবে — নিচের $2b$12$...-string তাই গঠনের
প্রতিনিধি; boolean আর None/not-None ফলাফল সবসময় হুবহু এই প্যাটার্নে আসবে):
=== একই EXPLOIT #1 payload, fixed login-এর বিরুদ্ধে ===
login("' OR '1'='1' -- ", ...) -> None
authentication বাইপাস হলো: False
=== sanity check: বৈধ credential আগের মতোই কাজ করে ===
login('alice', 'alice-secret-99') -> (1, 'alice')
=== একই EXPLOIT #2 চেষ্টা, fixed get_note-এর বিরুদ্ধে ===
get_note(session_user_id=alice_id, note_id=2) -> None
IDOR ব্লক হলো: True
get_note(session_user_id=alice_id, note_id=1 [নিজের]) -> (1, 1, 'alice-এর ব্যক্তিগত note')
=== একই EXPLOIT #3 dictionary, bcrypt hash-এর বিরুদ্ধে ===
stolen password_hash for bob (bcrypt): $2b$12$cFs8ydsXbOBRABzLopWmW....
plain SHA-256 dictionary lookup result: None
(bcrypt hash কখনো SHA-256-dictionary-তে মিলবে না -- ভিন্ন format, ভিন্ন algorithm)তিনটা ফিক্স-ই “একটা লাইন যোগ করা” স্তরের ছোট, অথচ প্রতিটাই একটা পুরো লেসনের যুক্তি বহন করে। নিচের টেবিলে সারমর্ম:
| Bug | Exploit | কোন লেসনের mitigation | ফিক্সের সারমর্ম |
|---|---|---|---|
| #1 unsalted fast hash | precomputed dictionary lookup, brute-force ছাড়াই crack | password-storage | hashlib.sha256 → bcrypt.hashpw (per-user salt, tunable slow) |
| #2 string-concatenated SQL | ' OR '1'='1' -- দিয়ে auth bypass | injection-attacks | f-string query → placeholder (?) + params |
| #3 owner-check অনুপস্থিত | note_id বদলে অন্যের note পড়া | authorization (IDOR) | lookup-এ row[1] != session_user_id চেক যোগ |
ছবিতে
- client inputusername, password, note_id -- এখনো সম্পূর্ণ untrusted, কোনো assumption নেই
- parsing / interpreter boundaryএই স্ট্রিং কি কোথাও SQL/shell/HTML syntax-এর ভেতরে বসছে? (injection-attacks, xss-csrf-ssrf)
- authentication boundaryidentity claim কি cryptographically verify হয়েছে, নাকি শুধু পড়া হয়েছে? (authentication)
- authorization boundaryএই resource কি request-কারীর নিজের, এই request-এই যাচাই হয়েছে? (authorization/IDOR)
- storage boundarysecret store হওয়ার সময় ঠিক primitive (hash vs encryption, salt+slow-KDF) ব্যবহার হয়েছে? (password-storage, cryptography-fundamentals)
- dependency / deployment boundaryতৃতীয়-পক্ষের কোড pinned ও patched, process least-privilege-এ চলছে? (supply-chain-security, sandboxing-isolation)
আজকের get_note() bug ঠিক authorization boundary-তে বসে ছিল বলেই
এখানে সেই layer-টা “here” হিসেবে চিহ্নিত — কিন্তু বাস্তব review-এ bug যে
কোনো layer-এ থাকতে পারে, আর একটা মাত্র bug-ই যথেষ্ট পুরো request-চেইনের
নিরাপত্তা ভেঙে দিতে। এই কারণেই checklist প্রতিটা layer কভার করে,
শুধু সেই layer-টাই না যেখানে “সাধারণত” bug থাকে।
diff-এর প্রতিটা নতুন লাইনে যেখানে বাইরে থেকে আসা ডেটা প্রথম স্পর্শ করে:
১. এই ভ্যারিয়েবলটা কোথা থেকে এসেছে? (request body/param, DB row, file, env)
│
▼
২. এটা কি কোনো interpreter-এর syntax-string-এ ঢুকছে? ── হ্যাঁ ──► injection-attacks checklist
│ না
▼
৩. এটা কি কোনো resource-এর ID/owner নির্ধারণ করছে? ── হ্যাঁ ──► authorization/IDOR checklist
│ না
▼
৪. এটা কি কোনো memory-size/length নির্ধারণ করছে? ── হ্যাঁ ──► memory-safety checklist
│ না
▼
৫. এটা কি কোনো secret (password, token, key)? ── হ্যাঁ ──► crypto/password-storage checklist
│ না
▼
এই নির্দিষ্ট ভ্যারিয়েবলের জন্য review সম্পন্ন -- পরের external input-এ যাননিজে চালিয়ে দেখুন
ব্যাচ-ভেরিফিকেশন — একগুচ্ছ attack payload, vulnerable বনাম fixed
Hood আর example সেকশনে একটা করে payload/pair দিয়ে bug দেখানো হয়েছে — কিন্তু একটা bug “ফিক্সড” বলার আগে একাধিক variant-এ যাচাই করা ভালো অভ্যাস। নিচের কোডটা আসলেই চালানো, চারটা SQL-injection payload আর চারটা (session-user, note-id) জোড়া — vulnerable ও fixed দুইটার বিরুদ্ধেই:
# review_batch.py -- vulnerable বনাম fixed, একগুচ্ছ attack payload ব্যাচ-টেস্টে
import sqlite3, hashlib, bcrypt
def build_vulnerable():
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, password_hash TEXT)")
conn.execute("CREATE TABLE notes (id INTEGER PRIMARY KEY, owner_id INTEGER, body TEXT)")
def signup(username, password):
pw_hash = hashlib.sha256(password.encode()).hexdigest()
cur = conn.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, pw_hash))
conn.commit()
return cur.lastrowid
def login(username, password):
pw_hash = hashlib.sha256(password.encode()).hexdigest()
query = (f"SELECT id FROM users WHERE username = '{username}' "
f"AND password_hash = '{pw_hash}'")
return conn.execute(query).fetchone()
def get_note(session_user_id, note_id):
return conn.execute("SELECT id, owner_id FROM notes WHERE id = ?", (note_id,)).fetchone()
a = signup("alice", "alice-secret-99")
b = signup("bob", "bob-hunter2")
conn.execute("INSERT INTO notes (owner_id, body) VALUES (?, ?)", (a, "alice-note"))
conn.execute("INSERT INTO notes (owner_id, body) VALUES (?, ?)", (b, "bob-note"))
conn.commit()
return login, get_note, a, b
def build_fixed():
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, password_hash TEXT)")
conn.execute("CREATE TABLE notes (id INTEGER PRIMARY KEY, owner_id INTEGER, body TEXT)")
def signup(username, password):
pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=10))
cur = conn.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, pw_hash))
conn.commit()
return cur.lastrowid
def login(username, password):
row = conn.execute("SELECT id, password_hash FROM users WHERE username = ?", (username,)).fetchone()
if row is None or not bcrypt.checkpw(password.encode(), row[1]):
return None
return (row[0],)
def get_note(session_user_id, note_id):
row = conn.execute("SELECT id, owner_id FROM notes WHERE id = ?", (note_id,)).fetchone()
if row is None or row[1] != session_user_id:
return None
return row
a = signup("alice", "alice-secret-99")
b = signup("bob", "bob-hunter2")
conn.execute("INSERT INTO notes (owner_id, body) VALUES (?, ?)", (a, "alice-note"))
conn.execute("INSERT INTO notes (owner_id, body) VALUES (?, ?)", (b, "bob-note"))
conn.commit()
return login, get_note, a, b
SQLI_PAYLOADS = [
"' OR '1'='1' -- ",
"' OR 1=1 -- ",
"admin' -- ",
"nonexistent_user", # negative control -- এটার বাইপাস হওয়ার কথাই না
]
v_login, v_get_note, va, vb = build_vulnerable()
f_login, f_get_note, fa, fb = build_fixed()
print("== ব্যাচ ১: SQL-injection payload-সেট, login()-এর বিরুদ্ধে ==")
print(f" {'payload':22} {'vulnerable bypass?':20} {'fixed bypass?'}")
v_bypasses = f_bypasses = 0
for p in SQLI_PAYLOADS:
vr = v_login(p, "wrong-password-xyz") is not None
fr = f_login(p, "wrong-password-xyz") is not None
v_bypasses += vr
f_bypasses += fr
print(f" {p!r:22} {str(vr):20} {fr}")
print(f" মোট বাইপাস: vulnerable {v_bypasses}/{len(SQLI_PAYLOADS)}, fixed {f_bypasses}/{len(SQLI_PAYLOADS)}")
print()
print("== ব্যাচ ২: (session_user, note_id) জোড়া, get_note()-এর বিরুদ্ধে ==")
pairs = [(va, 1), (va, 2), (vb, 1), (vb, 2)] # (session-user, note-id) -- note 1=alice-র, 2=bob-এর
print(f" {'session_user':13} {'note_id':8} {'vulnerable leak?':18} {'fixed leak?'}")
v_leaks = f_leaks = 0
for su, nid in pairs:
vr = v_get_note(su, nid)
fr = f_get_note((fa if su == va else fb), nid)
v_leak = vr is not None and vr[1] != su
f_leak = fr is not None and fr[1] != (fa if su == va else fb)
v_leaks += v_leak
f_leaks += f_leak
print(f" {su:<13} {nid:<8} {str(v_leak):18} {f_leak}")
print(f" মোট leak: vulnerable {v_leaks}/{len(pairs)}, fixed {f_leaks}/{len(pairs)}")প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):
== ব্যাচ ১: SQL-injection payload-সেট, login()-এর বিরুদ্ধে ==
payload vulnerable bypass? fixed bypass?
"' OR '1'='1' -- " True False
"' OR 1=1 -- " True False
"admin' -- " False False
'nonexistent_user' False False
মোট বাইপাস: vulnerable 2/4, fixed 0/4
== ব্যাচ ২: (session_user, note_id) জোড়া, get_note()-এর বিরুদ্ধে ==
session_user note_id vulnerable leak? fixed leak?
1 1 False False
1 2 True False
2 1 True False
2 2 False False
মোট leak: vulnerable 2/4, fixed 0/4দুইটা সংখ্যাই আজকের সবচেয়ে গুরুত্বপূর্ণ প্রমাণ। SQLi ব্যাচে
"admin' -- " payload ব্যর্থ হয়েছে (কোনো admin user নেই বলে) —
এটা মনে করিয়ে দেয় injection সবসময় সফল হয় না, payload-টা database-এর
প্রকৃত content-এর সাথে মিলতে হয়; কিন্তু '1'='1'-জাতীয় tautology
payload-গুলো condition-নির্বিশেষে সফল হয়, যা attacker-এর জন্য সবচেয়ে
নির্ভরযোগ্য কৌশল। IDOR ব্যাচে leak ঠিক তখনই ঘটেছে যখন session_user
আর note-এর প্রকৃত owner ভিন্ন ছিল (সারি ২ আর ৩) — নিজের note পড়ায়
(সারি ১ আর ৪) কোনো leak নেই, যা প্রমাণ করে fix over-restrictive না,
শুধু ownership-mismatch-এই block করছে। দুইটা ক্ষেত্রেই fixed কোড
সম্পূর্ণ ০ — প্রতিটা variant-এ, একবারও না।
একই vulnerability-class-এর একাধিক payload-variant চালালে vulnerable কোডে বারবার bypass/leak ঘটে (SQLi payload-এ ২/৪, IDOR pair-এ ২/৪), কিন্তু হুবহু একই payload-সেট fixed কোডের বিরুদ্ধে প্রতিবার ০/৪ — single-case demo নয়, একটা পরিসংখ্যানগত প্যাটার্ন হিসেবে fix-এর সম্পূর্ণতা প্রমাণ করে।
নিজে বানান
security_regression_tests.py — আজকের bug-গুলোকে স্থায়ী, স্বয়ংক্রিয় regression test বানানো
- তিনটা assertion-based test function লিখুন -- test_no_sqli_bypass, test_no_idor, test_password_not_fast_plain_hash
- একটা run_suite() হেল্পার লিখুন যা তিনটা test চালায়, PASS/FAIL প্রিন্ট করে, ব্যর্থ হলে AssertionError catch করে (crash না করে)
- vulnerable app-এর ওপর suite চালিয়ে দেখান তিনটাই FAIL করছে -- অর্থাৎ merge-এর আগেই এই bug ধরা পড়ত
- fixed app-এর ওপর একই suite চালিয়ে দেখান তিনটাই PASS করছে
- ভাবুন -- এই suite-টা যদি CI pipeline-এ প্রতিটা pull request-এ চলত, আজকের তিনটা bug কি merge হতে পারত?
লক্ষ্য: checklist-এর প্রশ্নগুলোকে শুধু review-এর সময়কার মানসিক চেকলিস্ট না রেখে, কোডে এনকোড করে ফেলা — যাতে ভবিষ্যতে কেউ ভুলে গেলেও, বা তাড়াহুড়ায় review skip করলেও, automated test সেই একই bug ধরে ফেলে। এটাই OWASP-এর ও NIST SSDF-এর সুপারিশের ব্যবহারিক রূপ: security review-এর ফলাফলকে শুধু একবারের approval না রেখে, একটা পুনরাবৃত্তিযোগ্য test-এ রূপান্তর করা।
# security_regression_tests.py -- আজকের তিনটা bug-কে স্থায়ী, স্বয়ংক্রিয় regression test বানানো
import sqlite3, hashlib, bcrypt
def test_no_sqli_bypass(login_fn):
result = login_fn("' OR '1'='1' -- ", "wrong-password-xyz")
assert result is None, "SQL injection payload authentication বাইপাস করেছে!"
def test_no_idor(get_note_fn, alice_id, bobs_note_id):
result = get_note_fn(alice_id, bobs_note_id)
assert result is None, "IDOR: alice bob-এর note পড়তে পেরেছে!"
def test_password_not_fast_plain_hash(stored_hash):
looks_like_raw_sha256 = isinstance(stored_hash, str) and len(stored_hash) == 64
assert not looks_like_raw_sha256, "stored hash দেখতে raw SHA-256 hexdigest-এর মতো -- slow KDF নেই!"
def run_suite(name, login_fn, get_note_fn, alice_id, bobs_note_id, stored_hash):
print(f"--- {name} ---")
checks = [
("no_sqli_bypass", lambda: test_no_sqli_bypass(login_fn)),
("no_idor", lambda: test_no_idor(get_note_fn, alice_id, bobs_note_id)),
("password_not_fast_plain_hash", lambda: test_password_not_fast_plain_hash(stored_hash)),
]
passed = 0
for check_name, fn in checks:
try:
fn()
print(f" PASS {check_name}")
passed += 1
except AssertionError as e:
print(f" FAIL {check_name}: {e}")
print(f" {passed}/{len(checks)} passed\n")
# ---------- vulnerable app-এর ওপর suite চালানো ----------
conn_v = sqlite3.connect(":memory:")
conn_v.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, password_hash TEXT)")
conn_v.execute("CREATE TABLE notes (id INTEGER PRIMARY KEY, owner_id INTEGER, body TEXT)")
def v_signup(username, password):
h = hashlib.sha256(password.encode()).hexdigest()
cur = conn_v.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, h))
conn_v.commit()
return cur.lastrowid
def v_login(username, password):
h = hashlib.sha256(password.encode()).hexdigest()
q = f"SELECT id FROM users WHERE username = '{username}' AND password_hash = '{h}'"
return conn_v.execute(q).fetchone()
def v_get_note(session_user_id, note_id):
return conn_v.execute("SELECT id, owner_id FROM notes WHERE id = ?", (note_id,)).fetchone()
va = v_signup("alice", "alice-secret-99")
vb = v_signup("bob", "bob-hunter2")
conn_v.execute("INSERT INTO notes (owner_id, body) VALUES (?, ?)", (va, "alice-note"))
conn_v.execute("INSERT INTO notes (owner_id, body) VALUES (?, ?)", (vb, "bob-note"))
conn_v.commit()
v_hash = conn_v.execute("SELECT password_hash FROM users WHERE username='bob'").fetchone()[0]
run_suite("vulnerable app (আজকের review-এর আগের কোড)", v_login, v_get_note, va, 2, v_hash)
# ---------- fixed app-এর ওপর একই suite চালানো ----------
conn_f = sqlite3.connect(":memory:")
conn_f.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, password_hash TEXT)")
conn_f.execute("CREATE TABLE notes (id INTEGER PRIMARY KEY, owner_id INTEGER, body TEXT)")
def f_signup(username, password):
h = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=10))
cur = conn_f.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, h))
conn_f.commit()
return cur.lastrowid
def f_login(username, password):
row = conn_f.execute("SELECT id, password_hash FROM users WHERE username = ?", (username,)).fetchone()
if row is None or not bcrypt.checkpw(password.encode(), row[1]):
return None
return (row[0],)
def f_get_note(session_user_id, note_id):
row = conn_f.execute("SELECT id, owner_id FROM notes WHERE id = ?", (note_id,)).fetchone()
if row is None or row[1] != session_user_id:
return None
return row
fa = f_signup("alice", "alice-secret-99")
fb = f_signup("bob", "bob-hunter2")
conn_f.execute("INSERT INTO notes (owner_id, body) VALUES (?, ?)", (fa, "alice-note"))
conn_f.execute("INSERT INTO notes (owner_id, body) VALUES (?, ?)", (fb, "bob-note"))
conn_f.commit()
f_hash = conn_f.execute("SELECT password_hash FROM users WHERE username='bob'").fetchone()[0]
run_suite("fixed app (review-এর পরের কোড)", f_login, f_get_note, fa, 2, f_hash)প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):
--- vulnerable app (আজকের review-এর আগের কোড) ---
FAIL no_sqli_bypass: SQL injection payload authentication বাইপাস করেছে!
FAIL no_idor: IDOR: alice bob-এর note পড়তে পেরেছে!
FAIL password_not_fast_plain_hash: stored hash দেখতে raw SHA-256 hexdigest-এর মতো -- slow KDF নেই!
0/3 passed
--- fixed app (review-এর পরের কোড) ---
PASS no_sqli_bypass
PASS no_idor
PASS password_not_fast_plain_hash
3/3 passedমূল মুহূর্ত: vulnerable app-এ ০/৩ পাস করেছে — মানে যদি এই তিনটা assertion কোনো CI pipeline-এ pull-request-এর সময় চলত, আজকের bug-গুলো কখনো merge-ই হতে পারত না। Fixed app-এ ৩/৩ পাস — একই কোড, একই assertion, শুধু review-এর ফলাফলটাই এখন কোডে “লক” হয়ে গেছে, ভবিষ্যতের কোনো refactor ভুলবশত এই bug-গুলো ফিরিয়ে আনলে test সাথে সাথে ধরবে।
নিজে বাড়ান:
test_password_not_fast_plain_hash-এর check-টা bytes-ভিত্তিক bcrypt hash-এর জন্য কাজ করে, কিন্তু Argon2-hash-string শনাক্ত করতে এই heuristic-টা যথেষ্ট কিনা যাচাই করুন — কী বাড়তি শর্ত লাগবে?- একটা চতুর্থ test যোগ করুন যা যাচাই করে
get_note-এর মতো প্রতিটা resource-lookup function সবসময় দুইটা প্যারামিটার নেয় (session_user_idআরresource_id) — শুধুresource_id-নেওয়া কোনো function থাকলে সেটা IDOR-এর ঝুঁকি হিসেবে flag করুন - এই suite-টাকে
pytest-এ রূপান্তর করুন (assertstatement প্রায় একই থাকবে) — আর ভাবুন CI-তে এই test-file-এর নামকরণ ও অবস্থান কেমন হওয়া উচিত যাতে টিমের সবাই বোঝে এগুলো “সাধারণ” test না, security regression test
বাস্তব সিস্টেমে
রিভিউ একটা প্রক্রিয়া — শুধু একটা মুহূর্ত না
আজকের hood/example/experiment/build — চারটাই একটা মানুষ একা বসে কোড পড়ছে এই দৃশ্য দেখিয়েছে। বাস্তব organization-এ effective review তিনটা স্তরে চলে, প্রতিটা স্তর আগেরটার ফাঁক পূরণ করে।
স্তর ১ — threat-model-informed pass। এই module-এর প্রথম লেসন, threat-modeling-stride, একটা প্রশ্ন-কাঠামো দিয়েছিল: Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege। একটা diff review করার সময় প্রতিটা category ধরে জিজ্ঞেস করা — “এই পরিবর্তন কি কাউকে অন্য কারো ছদ্মবেশ ধরতে দেয়? কাউকে privilege বাড়াতে দেয়?” — আজকের trust-boundary checklist-এর একটা complementary স্তর, বিশেষত এমন bug ধরার জন্য যেগুলো কোনো single trust-boundary-তে না থেকে একাধিক component-এর মিথষ্ক্রিয়ায় জন্ম নেয় (যেমন race condition বা একাধিক microservice-এর মধ্যে inconsistent trust assumption)।
স্তর ২ — automated tooling, প্রথম ফিল্টার হিসেবে। SAST (Static
Application Security Testing) টুল — যেমন CodeQL, Semgrep — কোডে
নির্দিষ্ট bad pattern (string-concatenated SQL, eval()-এ user input,
হার্ডকোড করা secret) স্বয়ংক্রিয়ভাবে flag করে, প্রতিটা commit-এ, কোনো
মানুষের ক্লান্তি বা সময়ের চাপ ছাড়াই। Dependency scanner — যেমন
Dependabot, Snyk — supply-chain-security লেসনের ভাষায়, known-vulnerable
version pin হয়ে গেলে সতর্ক করে। এই টুলগুলো দ্রুত, সস্তা, আর ক্লান্ত হয়
না — কিন্তু আজকের EXPLOIT #2 (IDOR)-এর দিকে ফিরে তাকান: কোনো
সাধারণ SAST টুল get_note(session_user_id, note_id)-এর ভেতরে
session_user_id আসলে ব্যবহার হচ্ছে না সেটা সাধারণভাবে ধরতে পারে না
— এই bug ধরতে business-logic-এর semantic বোঝা লাগে (এই resource-এর
owner কে হওয়া উচিত), যা pattern-matching-ভিত্তিক tool-এর কাছে অস্বচ্ছ।
তাই SAST/dependency-scanner প্রথম ফিল্টার, শেষ কথা না।
স্তর ৩ — মানুষের review, habitual reading হিসেবে। Google-এর নিজস্ব engineering-practices ডকুমেন্টেশন (আজকের একটা reference) একটা নীতি জোর দিয়ে বলে: ছোট, ঘন ঘন change, বাধ্যতামূলক review, আর review-কে “gate” না ভেবে বরং টিমের normal অভ্যাস হিসেবে গড়ে তোলা। এই তৃতীয় স্তরেই আজকের mindset shift আসলে কাজ করে — একজন মানুষ diff পড়ছে “এটা কীভাবে ভাঙা যায়” প্রশ্ন নিয়ে, checklist সামনে রেখে, ঠিক যেভাবে আজকের hood সেকশনে দেখানো হয়েছে।
Review culture বনাম review checklist — দুইটাই লাগে। OWASP Code Review Guide-এর কেন্দ্রীয় বার্তা: একটা checklist ছাড়া review অসংগঠিত, কিন্তু শুধু checklist দিয়ে review “টিক-বক্স” আচরণে পরিণত হতে পারে — প্রকৃত threat-thinking ছাড়াই প্রতিটা বাক্সে টিক দিয়ে দেওয়া। আজকের checklist তাই একটা শুরুর বিন্দু, শেষ কথা না — প্রতিটা row-এর পেছনের “কেন” (আগের লেসনের পুরো যুক্তি) মনে রাখাই আসল দক্ষতা, শুধু row-এর নাম মুখস্থ করা না।
যে ভুলগুলো সবাই করে
“Automated tool (SAST, dependency scanner) যথেষ্ট শক্তিশালী হলে মানুষের code review-এর আর দরকার নেই।”
Realworld সেকশনেই এটা সরাসরি ভাঙা হয়েছে — SAST টুল pattern-matching-ভিত্তিক,
কিন্তু আজকের EXPLOIT #2 (IDOR) একটা semantic bug: কোডে কোনো “খারাপ”
syntax pattern নেই (কোনো string-concatenation, কোনো eval), শুধু একটা
প্রয়োজনীয় তুলনা অনুপস্থিত। এই ধরনের bug ধরতে বুঝতে হয় “এই resource-এর
owner কে হওয়া উচিত” — এটা ব্যবসায়িক যুক্তি, syntax pattern না। Automated
tool প্রথম ফিল্টার হিসেবে অমূল্য, কিন্তু business-logic bug-এর জন্য মানুষের
review প্রতিস্থাপনযোগ্য না।
“সব automated test pass করেছে আর demo কাজ করছে মানেই কোড নিরাপদ।”
Functional test আর demo “does it work” প্রশ্নের উত্তর দেয় — happy-path input-এ প্রত্যাশিত output আসছে কিনা। Hood সেকশনের vulnerable কোডটাও স্বাভাবিক login/signup/get-note flow-তে perfectly কাজ করে, প্রতিটা সাধারণ test pass করবে। Bug ধরা পড়ে শুধু তখনই যখন কেউ ইচ্ছাকৃতভাবে “how would I attack this” প্রশ্ন নিয়ে adversarial input (payload username, অন্যের resource-id, precomputed dictionary) দিয়ে কোড পরীক্ষা করে — এটাই আজকের build সেকশনের security-regression-test-এর উদ্দেশ্য, সাধারণ functional test-এর পরিপূরক হিসেবে, প্রতিস্থাপক না।
“Security review একবার সম্পন্ন হয়ে গেলে (launch-এর আগে) কোড চিরকালের জন্য নিরাপদ।”
hash-functions লেসনের SHA-1 timeline মনে করুন — ২০০৫-এ তাত্ত্বিক দুর্বলতা, ২০১৭-এ practical exploit, ১২ বছরের ব্যবধান। supply-chain-security লেসনের ভাষায়, একটা dependency আজ নিরাপদ কাল একটা নতুন CVE পেতে পারে। একটা কোডবেস আজ “review-pass” করলেও কাল একটা নতুন attack technique, নতুন dependency vulnerability, বা নতুন feature (যা পুরনো কোডের সাথে নতুনভাবে মিথষ্ক্রিয়া করে) পুরনো নিশ্চয়তা ভেঙে দিতে পারে। এই কারণেই আজকের next সেকশনে review-কে “সম্পন্ন checklist” না বলে “habitual reading mode” বলা হচ্ছে — প্রতিটা নতুন diff-এ পুনরাবৃত্ত একটা অভ্যাস, একবারের গেট না।
“Security review শুধু crypto/authentication-সংক্রান্ত কোডে দরকার -- সাধারণ business logic (যেমন একটা notes app) কম ঝুঁকিপূর্ণ।”
আজকের তিনটা bug-এর মধ্যে দুইটা (IDOR, SQL injection) কোনো cryptographic কোডে ছিল না — সাধারণ CRUD business logic-এ ছিল। Realworld সেকশনের First American Financial (৮৮.৫ কোটি ডকুমেন্ট) আর USPS (৬ কোটি অ্যাকাউন্ট) breach — authorization লেসনে দেখা দুইটা উদাহরণ — দুইটাই ছিল সাধারণ “document দেখান” বা “account তথ্য দেখান” business-logic endpoint-এ, কোনো crypto-primitive-এর ব্যর্থতা না। বাস্তবে সবচেয়ে সাধারণ, সবচেয়ে বড়-স্কেল breach-গুলো প্রায়ই সবচেয়ে “সাধারণ দেখতে” কোডে ঘটে, কারণ সেটাই সবচেয়ে কম review-attention পায়।
বুঝেছেন কি না দেখুন
1আজকের লেসনের কেন্দ্রীয় mindset shift এক বাক্যে কী, আর Howard ও LeBlanc-এর
বইয়ে এটা কোন প্রক্রিয়ার ভিত্তি হয়ে দাঁড়িয়েছিল?
স্মরণ
“এটা কাজ করে কিনা” (functional correctness, happy-path) থেকে “এটা কীভাবে ভাঙা যায়” (adversarial input, misuse) — এই প্রশ্ন-পরিবর্তনই কেন্দ্রীয় mindset shift। Writing Secure Code-এ Michael Howard ও David LeBlanc এই শিফটকে Microsoft-এর Security Development Lifecycle (SDL)-এর একটা মূল ভিত্তি বানিয়েছিলেন — কোড “সঠিক” আর কোড “নিরাপদ” আলাদা প্রশ্ন, আলাদা পড়ার-পদ্ধতি দাবি করে।
2Hood সেকশনের vulnerable কোডে তিনটা bug-ই আলাদা trust boundary-তে ছিল,
তবু concept সেকশনের Callout-এ বলা হয়েছে এগুলোর একটা সাধারণ মূল-কারণ-প্যাটার্ন
আছে। সেই সাধারণ প্যাটার্নটা কী?
যুক্তি
প্রতিটা bug-ই ঘটেছে কোডটা একটা implicit বিশ্বাস করেছে বলে, যেটা
explicit ভাবে যাচাই করা উচিত ছিল: username-কে “শুধু ডেটা” ধরে নেওয়া
হয়েছে (আসলে SQL-syntax হয়ে গেছে), note_id-কে “caller-এর নিজের” ধরে
নেওয়া হয়েছে (আসলে caller প্রমাণ করেনি), আর password_hash-কে “যথেষ্ট
নিরাপদ” ধরে নেওয়া হয়েছে (আসলে fast ও deterministic বলে precompute-যোগ্য)।
Trust boundary ভিন্ন হলেও, প্রতিটাতেই “একটা assumption-কে explicit
verification-এ রূপান্তর না করা”-ই মূল কারণ।
3Experiment-এর ব্যাচ ১-এ "admin' -- " payload vulnerable login()-এর
বিরুদ্ধেও ব্যর্থ হয়েছিল (bypass = False), কিন্তু "' OR '1'='1' -- "
সফল হয়েছিল। দুইটা payload-ই তো -- দিয়ে বাকি query কমেন্ট করে দিচ্ছে —
তাহলে পার্থক্যটা কোথায়?
প্রয়োগ
"admin' -- " payload vulnerable login()-এর
বিরুদ্ধেও ব্যর্থ হয়েছিল (bypass = False), কিন্তু "' OR '1'='1' -- "
সফল হয়েছিল। দুইটা payload-ই তো -- দিয়ে বাকি query কমেন্ট করে দিচ্ছে —
তাহলে পার্থক্যটা কোথায়?-- কমেন্ট-মার্কার দুইটাতেই password-check অংশ বাদ দিয়ে দিচ্ছে ঠিকই,
কিন্তু বাকি থাকা WHERE username = '...' অংশটা তখনও evaluate হয়।
"admin' -- "-এ শর্তটা হয় username = 'admin' — আর ডেটাবেসে admin
নামে কোনো user-ই নেই (শুধু alice আর bob আছে), তাই কোনো row match করে না।
"' OR '1'='1' -- "-এ শর্তটা হয় username = '' OR '1'='1' — যেখানে
'1'='1' সবসময় true, তাই username-নির্বিশেষে পুরো table-ই match করে।
পার্থক্যটা comment-marker-এর কার্যকারিতায় না, বরং OR-tautology কীভাবে
username-check-কেও বাইপাস করে দেয় তাতে।
4Example সেকশনে fixed কোড-এর dictionary-attack-check-এ precomputed.get(stolen)
সবসময় None ফেরত দিয়েছে, এমনকি bcrypt hash-টাও precompute করা যেত এমন
সন্দেহ থাকলেও। কেন — এটা কি শুধু dictionary ছোট ছিল বলেই, নাকি আরও
structural কারণ আছে?
যুক্তি
precomputed.get(stolen)
সবসময় None ফেরত দিয়েছে, এমনকি bcrypt hash-টাও precompute করা যেত এমন
সন্দেহ থাকলেও। কেন — এটা কি শুধু dictionary ছোট ছিল বলেই, নাকি আরও
structural কারণ আছে?আরও structural কারণ আছে — এটা শুধু dictionary-র আকারের প্রশ্ন না।
precomputed dictionary-টা hashlib.sha256(...).hexdigest() দিয়ে
বানানো (৬৪-character hex string), কিন্তু bcrypt-এর output সম্পূর্ণ ভিন্ন
format ($2b$12$...-স্টাইল, ভিন্ন algorithm, প্রতি-hash এমবেডেড random
salt) — দুইটা string কখনো syntactically মিলতেই পারবে না, dictionary
যত বড়ই হোক। এমনকি যদি attacker bcrypt দিয়েই dictionary বানাত, প্রতি-user
আলাদা salt-এর কারণে (bcrypt নিজেই বসায়) precomputation-এর সুবিধাই থাকত
না — password-storage লেসনের salting-যুক্তি এখানেও প্রযোজ্য।
5আপনার টিম একটা SAST টুল চালু করেছে যা প্রতিটা pull request-এ স্বয়ংক্রিয়ভাবে
চলে। এটা আজকের EXPLOIT #1 (SQL injection) আর EXPLOIT #3 (unsalted hash)
সহজেই ধরতে পারবে বলে আশা করা যায়, কিন্তু EXPLOIT #2 (IDOR) ধরতে পারবে
কিনা নিয়ে সন্দেহ থাকা উচিত কেন?
প্রয়োগ
SQL injection আর unsalted-hash — দুইটাই syntactic pattern-এ ধরা
পড়ে: f-string দিয়ে বানানো SQL query, বা hashlib.sha256-এর সরাসরি
ব্যবহার password-এর ওপর — এই pattern-গুলো টুল নিয়ম হিসেবে এনকোড করতে
পারে (“রেড-ফ্ল্যাগ এই API-কল দেখলেই”)। কিন্তু IDOR bug-এ কোনো “খারাপ”
API-কল নেই — get_note() সম্পূর্ণ স্বাভাবিক দেখতে একটা DB lookup, সমস্যা
হলো একটা তুলনা অনুপস্থিত। এটা বুঝতে টুলকে জানতে হবে “এই resource-এর
owner কে হওয়া উচিত, আর কে request করছে” — এটা ব্যবসায়িক প্রসঙ্গ (semantic
knowledge), সাধারণ pattern-matching SAST-এর ক্ষমতার বাইরে।
6একটা fintech স্টার্টআপে নতুন engineer-দের জন্য একটা code-review onboarding
checklist ডিজাইন করছেন — টিম ছোট (৮ জন), প্রতি সপ্তাহে ৩০-৪০টা pull
request, কোনো dedicated security team নেই। কী কী স্তর/প্রক্রিয়া রাখবেন,
আর আজকের module-এর কোন কোন লেসন-নির্দিষ্ট চেক অগ্রাধিকার পাবে?
ডিজাইন
তিন-স্তরের প্রক্রিয়া বাস্তবসম্মত: (১) CI-তে বাধ্যতামূলক automated tooling — একটা open-source SAST (Semgrep) আর dependency-scanner (Dependabot/Snyk) প্রতিটা PR-এ, merge-blocking না হলেও visible; এটা supply-chain-security আর injection-attacks-এর syntactic-pattern অংশ সস্তায় কভার করে। (২) প্রতিটা PR-এ কমপক্ষে একজন human reviewer বাধ্যতামূলক, আজকের trust-boundary checklist টেমপ্লেট হিসেবে PR-description-এ pinned — বিশেষভাবে অগ্রাধিকার: যেকোনো নতুন resource-lookup endpoint-এ ownership-check আছে কিনা (IDOR, business-logic bug বলে automated tool মিস করবে) আর যেকোনো নতুন password/secret-handling কোডে salt+slow-KDF আছে কিনা — দুইটাই dedicated security team ছাড়া সবচেয়ে সহজে miss হওয়া bug-class। (৩) security_regression_tests.py-এর প্যাটার্নে, প্রতিটা found bug-কে স্থায়ী assertion-এ রূপান্তর করে test-suite-এ যোগ করা — ছোট টিমে প্রতিটা bug একবার শেখা, বারবার না-ভোলার নিশ্চয়তা। Threat-modeling-stride-এর পূর্ণ পাস প্রতিটা ছোট PR-এ বাস্তবসম্মত না (সময়ের খরচ বেশি), কিন্তু নতুন বড় feature/endpoint-এর জন্য আলাদাভাবে schedule করা উচিত।
এরপর কী
এরপর কী — module সম্পূর্ণ
Threat modeling দিয়ে শুরু হয়েছিল এই module — attacker কে, কী চায়, কী দাম দিতে রাজি সেই ভাষা তৈরি করে। তারপর cryptography-র primitive (hash, symmetric, asymmetric, signature, TLS), তারপর application-logic-এর স্তর (authentication, authorization), তারপর memory-র স্তর (buffer overflow, use-after-free, integer overflow, mitigation), তারপর application-boundary আক্রমণ (injection, XSS/CSRF/SSRF), তারপর system-স্তর (side-channel, sandboxing, supply chain)। আজকের লেসন কোনো নতুন স্তর যোগ করেনি — বরং দেখিয়েছে কীভাবে এই সবগুলো স্তর একসাথে, একটা মানুষের কোড-পড়ার প্রক্রিয়ায়, প্রয়োগ হয়।
এই module-এর জ্ঞান এখন বইয়ের পাতায় সীমাবদ্ধ থাকার কথা না — curriculum-এর project-track-এ চারটা hands-on প্রজেক্ট অপেক্ষা করছে, প্রতিটাই এই module-এর একটা বড় অংশ একসাথে প্রয়োগ করায়: Threat Model Document (threat-modeling-stride-কে একটা বাস্তব সিস্টেমে প্রয়োগ করে সম্পূর্ণ দলিল বানানো), Crypto Primitives Lab (hash, symmetric, asymmetric, signature নিজে হাতে বাস্তবায়ন ও ভাঙা), Overflow Lab (memory-safety module-এর buffer overflow, mitigation, use-after-free নিজে exploit ও প্যাচ করা), আর Hardened HTTP Server (আজকের লেসনের মতোই, কিন্তু সম্পূর্ণ একটা server-এ — injection, authorization, TLS, least-privilege deployment সবকিছু একসাথে সঠিকভাবে বসানো)।
Security module এখানে শেষ, কিন্তু curriculum-এর পরের module — Advanced Architecture & Performance (level ১১) — একটা ভিন্ন কোণ থেকে সিস্টেম-ডিজাইনে ফিরে আসবে: কীভাবে আজকের নিরাপদ, সঠিক সিস্টেমগুলোকেই দ্রুত, স্কেলযোগ্য, আর হার্ডওয়্যার-সচেতন করে তোলা যায়।
আরও পড়ুন
- OWASP Code Review Guide — OWASP Foundation · Code review-কে একটা systematic, threat-driven প্রক্রিয়া হিসেবে সংজ্ঞায়িত করার আনুষ্ঠানিক community-driven guide — আজকের checklist-কাঠামোর একটা মূল উৎস
- How to Do a Code Review — Google Engineering Practices — Google Inc. · Google-এর ভেতরের code-review culture-এর আনুষ্ঠানিক ডকুমেন্টেশন — ছোট CL, বাধ্যতামূলক review, আর review-কে habitual practice বানানোর নীতিগত ভিত্তি
- Writing Secure Code, 2nd Edition — Michael Howard, David LeBlanc — Microsoft Press, 2002 · "Does it work" বনাম "how would I attack this" — এই mindset shift-এর ক্লাসিক, সবচেয়ে প্রভাবশালী industry-articulation, Microsoft-এর নিজস্ব SDL প্রক্রিয়ার একটা ভিত্তি
- Secure Software Development Framework (SSDF) — NIST SP 800-218 — National Institute of Standards and Technology (NIST) · Review, testing, আর automated tooling-কে পুরো software-development-lifecycle-এর একটা আনুষ্ঠানিক, repeatable অংশ হিসেবে সংজ্ঞায়িত করা সরকারি framework