এই পর্বে যা শিখবেন
- কোডের ভেতরে trust boundary চিহ্নিত করা, আর সেখানে allowlist দিয়ে input যাচাই
- Parse don't validate, আর কেন আগে canonicalise, পরে check
- Integer overflow, truncation আর sign error ধরার নিরাপদ কৌশল
- Race condition, TOCTOU, resource limit আর regex থেকে denial of service
- Error, log, secret, randomness আর constant-time তুলনার পেশাদার অভ্যাস
- বিপজ্জনক API, safe default, linter, type system, code review checklist, SEI CERT আর CWE Top 25
এই পর্ব ভাষা-নিরপেক্ষ অভ্যাস শেখায়। Web-এর নির্দিষ্ট প্রতিরক্ষা আছে পর্ব ৩৮ থেকে ৪২-এ। সব কোড খেলনা ডেটায় বা নিজের তৈরি অস্থায়ী ফোল্ডারে চলে। নিজের কোড পরীক্ষা করুন। অন্যের system পরীক্ষার জন্য লিখিত অনুমতি লাগে, আইনের সীমা দেখুন পর্ব ৬-এ।
১ · Trust boundary — কোডের ভেতরে দারোয়ান কোথায়
একটি অফিসের গেট ভাবুন। দারোয়ান গেটেই পরিচয়পত্র দেখেন, তারপর ভেতরে একটি পাস দেন। ভেতরের প্রতিটি টেবিলে আর পরিচয়পত্র দেখতে হয় না, পাস দেখলেই চলে। কিন্তু গেটে কেউ না থাকলে প্রতিটি কর্মচারীকে নিজে নিজে সন্দেহ করতে হতো, আর একজন ভুলে গেলেই বিপদ।
কোডেও একই ব্যাপার। Trust boundary হলো সেই রেখা, যেখানে ডেটা কম বিশ্বাসের জায়গা থেকে বেশি বিশ্বাসের জায়গায় আসে। এটি পর্ব ৪-এর data flow diagram-এর রেখা, এবার সেটি কোডে নামল। শুধু web form নয়, নিচের সবকিছুই সীমানা:
- ব্যবহারকারীর input, URL, header, cookie, mobile app-এর অনুরোধ
- অন্য service-এর API উত্তর, webhook, message queue
- আপলোড করা ফাইল, config ফাইল, environment, command-line argument
- ডেটাবেস থেকে পড়া পুরোনো মান, যদি অন্য কেউ লিখে থাকে (second-order ঝুঁকি)
দ্বিতীয় দিকটিও জরুরি। ভেতরের ডেটা যেখানে কোনো interpreter বা বাইরের জগতে যায়, সেই জায়গাকে বলে sink: HTML পাতা, SQL, shell, log, ফাইলের পথ। ভুল সাধারণত হয় দুই জায়গায়, ঢোকার সময় আর বের হওয়ার সময়। মাঝখানের কোড সেই দুই পাহারার ওপর নির্ভর করে।
কোডের ভেতরে দুই সীমানা
Parse at the way in, encode at the way out
Secure coding মানে নিরাপত্তার কোনো আলাদা স্তর যোগ করা নয়। এর মানে বারবার একই কয়েকটি প্রশ্ন করা: এই ডেটা কোথা থেকে এল? এটি কোথায় যাচ্ছে? ব্যর্থ হলে কী হবে? সীমার বাইরে গেলে কী হবে? দুটি কাজ একসাথে হলে কী হবে?
২ · Input — allowlist, parse আর canonicalise
মোবাইল ব্যাংকিংয়ের PIN-এর ঘরে শুধু সংখ্যা চলে, আর নির্দিষ্ট দৈর্ঘ্যের। এই হলো allowlist: কী চলবে তা বলা, কী চলবে না তার তালিকা নয়। Denylist সবসময় অসম্পূর্ণ, কারণ খারাপ সবকিছুর তালিকা কেউ জানে না। Input validation-এর তিন স্তর মনে রাখুন: গঠন (সংখ্যা তো?), পরিসর (১ থেকে ৫০,০০০ টাকার মধ্যে?), আর ব্যবসার নিয়ম (এই হিসাবে এত টাকা আছে?)।
পরের ধাপটি আরও শক্তিশালী। সাধারণ যাচাই একটি হ্যাঁ বা না দিয়ে ডেটাকে আগের মতোই string রেখে দেয়। তারপর প্রতিটি ভেতরের ফাংশনকে ভাবতে হয়, “এটি কি যাচাই করা?” Parse, don't validate বলে: যাচাই করেই ডেটাকে একটি নতুন ধরনে রূপ দিন। Alexis King ২০১৯ সালের একটি লেখায় কথাটি জনপ্রিয় করেন। যাচাই না হওয়া ডেটা তখন সেই ধরনে থাকতেই পারে না।
আর একটি ক্রম না মানলে সব যাচাই বৃথা: আগে canonicalise, পরে check. একই জিনিসের অনেক রূপ থাকলে আগে সেটিকে একটি মানক রূপে আনতে হয়। যাচাই আগে হলে, আর পরে কোনো ধাপে রূপ বদলালে, যা যাচাই হলো আর যা ব্যবহার হলো তা আলাদা জিনিস। এই ত্রুটির নিজের নামই আছে: CWE-180. পথের ক্ষেত্রে একই নীতি resolve-then-check, দেখুন পর্ব ৪২।
টাকার একটি ঘর ধাপে ধাপে দেখুন। ব্যবহারকারী কোথাও থেকে কপি করে পেস্ট করেছেন, তাই শুরুতে ফাঁকা জায়গা আর দুটি full-width অঙ্ক আছে।
| ধরন | |
| বিশ্বাস |
- সীমানায় পৌঁছাল। এটি এখনো অবিশ্বস্ত লেখা, যদিও দেখতে টাকার অঙ্কের মতো।
- আকার সীমা। লেখাটি ১০ অক্ষরের, সীমা ৩২. সবচেয়ে সস্তা যাচাই সবার আগে, যাতে বিশাল লেখায় পরের ধাপের খরচ না হয়।
- Canonicalise. NFKC full-width অঙ্ককে সাধারণ অঙ্কে আনে,
stripফাঁকা জায়গা ফেলে। কোনো যাচাই এখনো হয়নি, এবার হবে। - Allowlist. পুরো লেখাটিকেই মিলতে হবে, তাই
fullmatch.[0-9]লেখা হয়েছে ইচ্ছে করে, কারণ Python-এ\dবাংলা অঙ্কও ধরে। - Parse. টাকাকে পয়সায় পূর্ণসংখ্যা করা হলো: 125050. Float নয়, কারণ দশমিক হিসাবে সূক্ষ্ম ভুল থাকে।
- পরিসর। ব্যবসার সীমা: ১ টাকা থেকে ৫০,০০০ টাকা, অর্থাৎ 100 থেকে 5000000 পয়সা। 125050 এই সীমার ভেতরে।
- Typed value. এখন মানটি
Amountধরনের। ভেতরের ফাংশন শুধু এই ধরন নেয়, তাই কেউ ভুলে কাঁচা string পাঠাতে পারবে না। - খারিজের পথ।
-5-এ allowlist মেলে না, কাজ সেখানেই থামে। ব্যবহারকারী পান ঘরটি ঠিক করার সাধারণ বার্তা। ভেতরে কিছুই ঢোকেনি।
AMOUNT_RE = re.compile(r"[0-9]{1,7}(\.[0-9]{2})?")
@dataclass(frozen=True)
class Amount:
paisa: int
def parse_amount(raw: str) -> Amount:
if len(raw) > 32:
raise BadInput("too long")
text = unicodedata.normalize("NFKC", raw).strip()
if not AMOUNT_RE.fullmatch(text):
raise BadInput("format")
taka, _, frac = text.partition(".")
paisa = int(taka) * 100 + int(frac or "0")
if not 100 <= paisa <= 5_000_000:
raise BadInput("range")
return Amount(paisa)Validation সব রোগের ওষুধ নয়। O'Brien বা “বিশ্বাস & সন্স”-এর মতো সৎ মানেও বিশেষ চিহ্ন থাকে। তাই খোলা লেখার ঘরে validation আকার আর encoding দেখবে, আর সুরক্ষা আসবে sink-এ। ব্যবহারকারীর নাম জোর করে ইংরেজি অক্ষরে বেঁধে ফেলা নিরাপত্তা নয়, ভুল নকশা। আর browser-এর যাচাই শুধু সুবিধার জন্য, সীমানা থাকে server-এ।
৩ · Output — প্রতিটি প্রসঙ্গের নিজের নিয়ম
একই খবর আদালতে, চায়ের দোকানে আর টেলিগ্রামে আলাদা ভাষায় বলতে হয়। ডেটাও তেমন। < চিহ্নটি HTML-এ বিশেষ, shell-এও বিশেষ, কিন্তু অর্থ আলাদা। একই মান কোথায় যাচ্ছে, সেটিই ঠিক করে কী করতে হবে। একে বলে encoding context.
তাই output encoding হয় ডেটা ব্যবহারের ঠিক আগে, ঢোকার সময় নয়। ঢোকার সময় কোনো “সর্বজনীন পরিষ্কার” ফাংশন চালালে ডেটা নষ্ট হয়, আর দুইবার encode হওয়ার ঝুঁকি থাকে। ডেটাবেসে আসল মান থাকবে, দেখানোর সময় প্রসঙ্গ অনুযায়ী রূপ বদলাবে।
| Sink | নিরাপদ পথ | যে অভ্যাস ছাড়বেন |
|---|---|---|
| HTML লেখা | Auto-escaping template, textContent | লেখা জুড়ে HTML বানানো |
| SQL | Parameterized query | Query-তে মান জোড়া |
| Shell বা process | Argument array, library | shell=True, system() |
| ফাইলের পথ | নিজের দেওয়া id, resolve-then-check | ব্যবহারকারীর নাম সরাসরি পথে |
| Log | Structured log, নতুন লাইন encode | কাঁচা লেখা log-এ (log injection) |
| JSON | Serializer library | হাতে লেখা জোড়া দিয়ে JSON |
| CSV বা spreadsheet | = বা + দিয়ে শুরু ঘর নিরপেক্ষ করা | ঘর যাচাই ছাড়াই export |
Injection-এর মূল ধারণা আর SQL-এর দিক আছে পর্ব ৩৯-এ, HTML-এর দিক পর্ব ৪০-এ। এখানে একটি কথাই নিয়ে যান: আদেশ আর ডেটার পথ আলাদা রাখুন, আর encoding-এর জন্য নিজের ফাংশন নয়, পরীক্ষিত library ব্যবহার করুন।
৪ · সংখ্যার ফাঁদ — overflow, truncation আর sign
পুরোনো গাড়ির ওডোমিটার ভাবুন। ৯৯,৯৯৯ কিলোমিটারের পরে মিটার ০০,০০০ দেখায়, গাড়ি নতুন হয়নি। কম্পিউটারের নির্দিষ্ট আকারের সংখ্যাও এমন। ৩২-bit signed সংখ্যার সর্বোচ্চ মান 231 − 1 = 2,147,483,647. এরপর এক যোগ করলে সবচেয়ে ছোট ঋণাত্মক মানে ঘুরে যেতে পারে। তিনটি ভুল মনে রাখুন:
Integer overflow
ফল ধরন-এর সীমা পেরিয়ে যায়। মান ঘুরে যেতে পারে। C-তে signed overflow-এর ফল নির্দিষ্টই নয়।
CWE-190Truncation
বড় ধরন থেকে ছোট ধরনে নামালে ওপরের bit হারিয়ে যায়। মান চুপচাপ ছোট হয়।
CWE-197Sign error
ঋণাত্মক মান অপ্রত্যাশিত জায়গায় ঢোকে, বা signed আর unsigned-এর মধ্যে রূপ বদলে বিশাল সংখ্যা হয়।
CWE-195এবার একটি খেলনা দোকান, যেখানে মোট দাম পয়সায় ৩২-bit signed ঘরে থাকে। Math.imul ঠিক এমন একটি ঘরের মতো গুণ করে। checkedTotal আগে সঠিক ফল বের করে, তারপর ঘরে রাখার আগে সীমা দেখে।
// Toy shop: money in paisa, stored in a signed 32-bit field. No real payment.
const INT32_MAX = 2147483647;
// What a C-like signed 32-bit field does: wrap around, silently.
const wrappedTotal = (price, qty) => Math.imul(price, qty);
// Safe: compute exactly, then check the range BEFORE storing.
function checkedTotal(price, qty) {
if (!Number.isInteger(price) || !Number.isInteger(qty)) return 'REJECT type';
if (price <= 0 || qty <= 0) return 'REJECT sign';
const exact = BigInt(price) * BigInt(qty);
if (exact > BigInt(INT32_MAX)) return 'REJECT too large';
return Number(exact);
}
const orders = [[250000, 4], [3000000, 1000], [3000000, 1500], [250000, -4]];
for (const [price, qty] of orders) {
console.log(`price=${price} qty=${qty}`);
console.log(' exact :', (BigInt(price) * BigInt(qty)).toString());
console.log(' wrapped:', wrappedTotal(price, qty));
console.log(' checked:', checkedTotal(price, qty));
}
// Truncation: the check sees 32 bits, the copy keeps only 16.
const len = 70000;
const LIMIT32 = 100000;
console.log('length', len, 'passes 32-bit check:', len <= LIMIT32);
console.log('kept by a 16-bit field:', len & 0xffff);প্রথম অর্ডারে তিনটি মানই 1000000, সবকিছু ঠিক দেখায়। দ্বিতীয়টিতে আসল ফল 3000000000, কিন্তু ঘরে থাকে -1294967296, মানে বিল ঋণাত্মক। তৃতীয়টির ঘুরে যাওয়া মান 205032704 দেখতে স্বাভাবিক, অথচ আসল বিলের প্রায় ২২ ভাগের ১ ভাগ। এটিই সবচেয়ে বিপজ্জনক, কারণ কেউ সন্দেহ করবে না। হিসাবটি হলো 4,500,000,000 − 4,294,967,296 = 205,032,704. চতুর্থটিতে REJECT sign, কারণ ঋণাত্মক পরিমাণের মানে ফেরত দেওয়া হয়ে যেতে পারে। শেষে 70000 যাচাই পেরিয়ে যায়, কিন্তু ১৬-bit ঘরে থাকে 4464. অর্থাৎ যে আকার যাচাই হলো আর যে আকার ব্যবহার হলো, তা আলাদা। মেমরির ক্ষেত্রে এই অমিল কেমন বিপদ আনে, দেখবেন পর্ব ৪৭-এ।
নিরাপদ যাচাই কীভাবে লিখবেন
- কাজের আগে যাচাই করুন, পরে নয়।
a * bকরে ফল ঋণাত্মক কি না দেখা C-তে ভুল পদ্ধতি, কারণ overflow হয়ে গেলে compiler ধরে নিতে পারে এমন কিছু ঘটেইনি। - ভাষার checked সুবিধা নিন। Java-তে
Math.multiplyExact, Rust-এchecked_mul, GCC ও Clang-এ__builtin_mul_overflow, C23-এckd_mul. - Python-এও সাবধান। Python-এর int সীমাহীন, কিন্তু ডেটাবেসের কলাম, NumPy আর binary format-এর ঘর সীমিত। JavaScript-এ
Numberনিরাপদ পূর্ণসংখ্যা শুধু 253 − 1 পর্যন্ত। - টাকায় float নয়। পয়সায় পূর্ণসংখ্যা বা decimal ধরন রাখুন। Python-এ
0.1 + 0.2দেয়0.30000000000000004.
/* price and qty are already known to be positive int32_t values */
if (price > INT32_MAX / qty) {
return ERR_TOO_LARGE; /* reject before any overflow can happen */
}
int32_t total = price * qty;C-র বিস্তারিত দেখুন undefined behavior আর secure coding পর্বে।
৫ · Race condition আর TOCTOU — যাচাই আর কাজের মাঝখানের ফাঁক
একটি যৌথ হিসাবের কথা ভাবুন। স্বামী আর স্ত্রী দুই শাখায় একই সময়ে কাউন্টারে গেলেন। দুই ক্যাশিয়ারই খাতা দেখলেন ১,০০০ টাকা আছে। দুজনেই ৮০০ টাকা দিয়ে দিলেন। কারও ভুল ছিল না, ভুল ছিল “দেখা” আর “দেওয়া” দুটি আলাদা ধাপ হওয়ায়।
ফল কাজের সময়ের ক্রমের ওপর নির্ভর করলে তাকে বলে race condition. এর সবচেয়ে পরিচিত রূপ TOCTOU, মানে time of check to time of use: আপনি যা যাচাই করেছেন, ব্যবহারের মুহূর্তে সেটি আর সত্য নাও থাকতে পারে। নিচে একটি খেলনা ওয়ালেট দেখুন, যেখানে ৮০০ টাকা তোলার দুটি অনুরোধ একসাথে এসেছে।
| A দেখেছে | |
| B দেখেছে | |
| ডেটাবেসে balance | |
| মোট দেওয়া |
- শুরু। ওয়ালেটে 1000. কোডটি একা চললে একদম ঠিক। সমস্যা শুধু একসাথে চলার সময়।
- A পড়ল। A দেখল 1000, শর্ত সত্য। এটিই time of check.
- B পড়ল। A তখনো লেখেনি, তাই B-ও 1000 দেখল। দুজনের হাতেই এখন একই পুরোনো মান।
- A লিখল। balance হলো 200, দেওয়া হলো 800. এই পর্যন্ত সব ঠিক।
- B লিখল। B নিজের পুরোনো মান থেকে হিসাব করে আবার 200 লিখল। মোট দেওয়া 1600. একে বলে lost update.
- ক্ষতি। 1000 টাকা থেকে 1600 টাকা বেরিয়ে গেল, আর বাকি দেখাচ্ছে 200. গরমিল 800 টাকা। সাধারণ test এটি ধরে না, কারণ test একা একা চলে।
- Atomic সমাধান। নতুন করে 1000 থেকে শুরু। যাচাই আর লেখা এখন একটি মাত্র SQL statement. A-র কাজে ১টি row বদলাল, balance 200, দেওয়া 800.
- B ব্যর্থ। ডেটাবেস B-কে A-র ফলের পরেই দেখায়। 200 ≥ 800 মিথ্যা, তাই ০ row বদলাল। Row সংখ্যা দেখেই কোড টাকা দেওয়া বন্ধ করে। মোট 800.
সমাধানের সারকথা: যাচাই আর কাজ একই atomic operation-এ করুন। ডেটাবেসে শর্তসহ UPDATE, transaction, row lock, unique constraint. মেমরিতে lock বা atomic compare-and-swap. ফাইলে “না থাকলে তবেই তৈরি” একটি ধাপে। নিচের কোড নিজের বানানো অস্থায়ী ফোল্ডারে দুই ধরনের কাজ দেখায়। দুই কর্মীর ক্রম কোডে স্থির করা, যাতে ফল প্রতিবার একই আসে।
# Works only inside a fresh temp folder that this block creates and deletes.
import os, tempfile
def racy(path, trace):
# Fixed interleaving of two workers, so the result repeats on every run.
a_free = not os.path.exists(path) # A: time of check
b_free = not os.path.exists(path) # B: time of check
trace.append(f"A sees free={a_free}, B sees free={b_free}")
if a_free:
with open(path, "w") as f: f.write("worker A")
trace.append("A wrote the file")
if b_free:
with open(path, "w") as f: f.write("worker B")
trace.append("B wrote the file (A's work is lost)")
def atomic(path, name, trace):
flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY
try:
fd = os.open(path, flags, 0o600) # check and create are ONE step
except FileExistsError:
trace.append(f"{name}: already exists, stop safely")
return
with os.fdopen(fd, "w") as f:
f.write(f"worker {name}")
trace.append(f"{name}: created the file")
with tempfile.TemporaryDirectory() as d:
log = []
p1, p2 = os.path.join(d, "racy.txt"), os.path.join(d, "atomic.txt")
racy(p1, log)
atomic(p2, "A", log)
atomic(p2, "B", log)
print("\n".join(log))
print("racy file holds :", open(p1).read())
print("atomic file holds:", open(p2).read())প্রথম ধরনে দুজনই দেখে free=True, তারপর B wrote the file (A's work is lost). শেষে ফাইলে থাকে worker B. O_CREAT | O_EXCL-এ অপারেটিং সিস্টেম যাচাই আর তৈরি একসাথে করে। তাই A পায় created the file, আর B পায় already exists, stop safely, ফাইলে থাকে worker A. দুই ক্ষেত্রেই কাজের ক্রম একই ছিল, শুধু ফাঁকটি নেই। Mode 0o600 মানে ফাইলটি শুধু মালিকের। অস্থায়ী ফাইলের জন্য নিজে নাম বানাবেন না, tempfile ব্যবহার করুন। পুরোনো mktemp-ধরনের API-তেও এই একই ফাঁক আছে।
একই নীতি idempotency-তে কাজে লাগে। পেমেন্ট অনুরোধে একটি অনন্য key রাখলে, আর সেই key-তে unique constraint দিলে, একই অনুরোধ দুইবার এলেও টাকা একবারই কাটে। Race ধরতে test-এ একাধিক thread চালান, আর race detector আছে এমন ভাষায় সেটি ব্যবহার করুন, যেমন Go-র -race বা C-তে ThreadSanitizer.
৬ · Resource limit আর ReDoS — অসীম কিছুই নয়
ঈদের আগে দর্জির দোকান ভাবুন। একজন গ্রাহক ৫০০টি পাঞ্জাবির অর্ডার দিলে বাকি সবাই বসে থাকবেন। তাই দোকানি সীমা বেঁধে দেন। সফটওয়্যারেও প্রতিটি অনুরোধ সময়, মেমরি, CPU আর সংযোগ খায়। সীমা না থাকলে একটি বড় অনুরোধই বাকিদের আটকে দিতে পারে। এই ঝুঁকি পর্ব ২৮-এর denial of service-এর কোড-স্তরের রূপ। CWE Top 25-এর শেষ ঘরে আছে CWE-770: সীমা ছাড়া resource বরাদ্দ।
| Resource | নিয়ন্ত্রণ |
|---|---|
| Input-এর আকার | Body, ফাইল, তালিকা আর লেখার সর্বোচ্চ আকার |
| গঠনের গভীরতা | JSON বা XML-এর nesting সীমা, recursion সীমা |
| সংকুচিত ডেটা | খোলার পরের আকারে সীমা (decompression bomb) |
| সময় | প্রতিটি network call, query আর কাজে timeout |
| সংখ্যা | Pagination, rate limit, thread আর connection pool-এর সীমা |
Regular expression থেকে denial of service
Regex একটি ছোট বিশেষ ইঞ্জিন চালায়। অনেক ভাষার ইঞ্জিন backtracking করে: একটি পথে না মিললে ফিরে গিয়ে আরেক পথ দেখে। সাধারণ pattern-এ এতে সমস্যা নেই। কিন্তু যদি pattern-এ একটি পুনরাবৃত্তির ভেতরে আরেকটি পুনরাবৃত্তি থাকে, বা দুই বিকল্প একই লেখা মেলাতে পারে, তাহলে না-মেলা লেখায় পথের সংখ্যা ঘাতীয় হারে বাড়তে পারে। এতে একটি ছোট input-ও CPU আটকে দেয়। একে বলে ReDoS (CWE-1333).
হিসাবটি দেখুন। ধরুন pattern-এর পথের সংখ্যা 2n, যেখানে n হলো input-এর দৈর্ঘ্য, আর ইঞ্জিন সেকেন্ডে 108 ধাপ চালায়। এই গতি একটি ধরে নেওয়া সংখ্যা।
- n = ২০: 220 ÷ 108 ≈ ০.০১ সেকেন্ড
- n = ৩০: 230 ÷ 108 ≈ ১০.৭ সেকেন্ড
- n = ৪০: 240 ÷ 108 ≈ ১১,০০০ সেকেন্ড, মানে প্রায় ৩ ঘণ্টা
মাত্র ২০টি অক্ষর বাড়লে সময় প্রায় ১০ লাখ গুণ হলো। প্রতিরক্ষা কয়েকটি স্তরে:
- Linear-time engine: RE2, Go-র
regexpআর Rust-এরregexcrate নকশাগতভাবে ঘাতীয় backtracking এড়ায়। .NET 7 থেকেRegexOptions.NonBacktrackingআছে। - Timeout: .NET-এর
matchTimeout, বা Python-এর তৃতীয় পক্ষেরregexmodule-এরtimeout. Python-এর নিজেরre-তে timeout নেই, তবে ৩.১১ থেকে atomic group আছে। - আকার সীমা: Regex চালানোর আগে input-এর দৈর্ঘ্য সীমিত করুন। উপরের হিসাবে n ছোট রাখাই বড় প্রতিরক্ষা।
- ব্যবহারকারীর দেওয়া pattern নয়: বাইরের কেউ regex লিখতে পারলে সেটি কোড চালানোর সমান ঝুঁকি।
- Review আর tool: নিজের regex-এ নেস্টেড পুনরাবৃত্তি খুঁজুন, আর parser থাকলে regex বাদ দিন।
৭ · Error আর logging — চুপ করা নয়, ফাঁস করাও নয়
বিদ্যুৎ চলে গেলে ব্যাংকের লকার খুলে যায় না, বন্ধ থাকে। এটিই fail closed, যা পর্ব ৩-এ দেখেছেন। কোডে নিয়মটি সহজ: permission যাচাইয়ের সময় error হলে উত্তর “না”। উল্টোটা, মানে fail open, CWE-636. নিচের অনিরাপদ ধরনটি প্রায়ই দেখা যায়:
# Unsafe pattern: a swallowed exception turns an error into "allowed"
def can_view(user, doc):
allowed = True
try:
allowed = policy.check(user, doc)
except Exception:
pass # the failure disappears here
return allowed
# Safer: deny by default, record the real cause, show a plain message
def can_view(user, doc):
try:
return policy.check(user, doc) is True
except PolicyError:
log.exception("policy_check_failed", extra={"doc_id": doc.id})
return Falseএখানে তিনটি নিয়ম কাজ করছে। প্রথমত, শুরুর মান “না”। দ্বিতীয়ত, শুধু চেনা exception ধরা, সব নয়। তৃতীয়ত, exception গিলে ফেলা নয়। গিলে ফেলা error (CWE-390) পরে অন্য জায়গায় অদ্ভুত আচরণ হয়ে দেখা দেয়, আর তদন্ত তখন অন্ধকারে হয়।
ব্যবহারকারীকে কী দেখাবেন আর log-এ কী রাখবেন, এই দুটি আলাদা। Stack trace, SQL, ফাইলের পথ বা library version বাইরে গেলে হামলাকারীর কাজ সহজ হয় (CWE-209). পর্ব ৪৪-এর নিরাপদ error-এর মতো সাধারণ বার্তার সাথে একটি correlation id দিন, যা দিয়ে আপনার দল log-এ আসল কারণ খুঁজে পায়।
Log-এর উল্টো বিপদও আছে: log নিজেই ফাঁসের পথ (CWE-532). Log অনেক মানুষ পড়েন, অনেক system-এ কপি হয়, আর অনেকদিন থাকে। Password, OTP, PIN, token, পূর্ণ কার্ড নম্বর কখনো log-এ নয়। ফোন নম্বরের মতো ব্যক্তিগত তথ্য যতটা সম্ভব কম। নিচের খেলনা redactor দুই পদ্ধতি পাশাপাশি দেখায়।
// Toy log redactor on made-up lines. Phone numbers and tokens are fake.
const RULES = [
[/\b(?:\+?88)?01[3-9]\d{8}\b/g, m => '***PHONE-' + m.slice(-2)],
[/\bBearer\s+[A-Za-z0-9._-]{1,200}/g, () => 'Bearer ***TOKEN'],
[/\b(token|password|otp|pin)=[^\s&]{1,200}/gi, (m, k) => k + '=***'],
];
function redactText(line) {
return RULES.reduce((s, [re, fn]) => s.replace(re, fn), line);
}
// Better: a structured event keeps only fields on an allowlist.
const SAFE_FIELDS = new Set(['event', 'user_id', 'ip', 'status']);
function safeEvent(ev) {
return Object.fromEntries(
Object.entries(ev).filter(([k]) => SAFE_FIELDS.has(k)));
}
const lines = [
'login ok user=u1042 phone=01900000001 ip=203.0.113.7',
'GET /api/me Authorization: Bearer example-token-1234',
'reset request token=example-token-9 otp=000000 status=200',
'contact phone ০১৯০০০০০০০০১ noted by agent',
];
for (const l of lines) console.log(redactText(l));
const ev = { event: 'pay', user_id: 'u1042', status: 200,
pin: '0000', phone: '০১৯০০০০০০০০১' };
console.log(JSON.stringify(safeEvent(ev)));প্রথম তিন লাইনে ফোন আর token মুছে গেছে: phone=***PHONE-01, Bearer ***TOKEN, token=*** otp=***. কিন্তু চতুর্থ লাইনে বাংলা অঙ্কের নম্বরটি অক্ষত রয়ে গেছে: contact phone ০১৯০০০০০০০০১ noted by agent. কারণ regex একটি denylist, আর denylist সবকিছু জানে না। শেষের লাইনে কোনো অনুমান নেই: {"event":"pay","user_id":"u1042","status":200}. Allowlist-এ যা নেই, তা log-এ পৌঁছায়ই না, লেখার ভাষা যাই হোক। তাই redaction শেষ সুরক্ষা, প্রথম নয়। কী লিখবেন, কতদিন রাখবেন, তা পর্ব ৫৫-এর বিষয়।
৮ · Secret, randomness আর তুলনা
লকারের চাবি লকারের গায়ে টেপ দিয়ে আটকে রাখলে তালার দাম থাকে না। কোডে লেখা password বা API key ঠিক তাই (CWE-798). কোড অনেকের হাতে যায়, পুরোনো commit-এ থেকে যায়, আর backup-এ ছড়ায়। একবার ছড়ালে একে বলে secret sprawl.
- কোডে নয়: মান আসবে বাইরে থেকে। ছোট প্রকল্পে environment variable বা .env ফাইল চলে, আর ফাইলটি version control-এ যাবে না। মনে রাখুন, environment child process পায়, আর crash report-এও উঠতে পারে।
- Vault ভালো পথ: বড় system-এ secret থাকে vault বা cloud secret manager-এ, অধিকার আর audit সহ। Dynamic secret আরও ভালো, কারণ সেটি অল্প সময় বাঁচে। প্রথম চাবির সমস্যা, মানে secret zero, দেখেছেন পর্ব ২০-এ।
- Commit-এর আগেই ধরুন: Pre-commit hook আর CI-তে secret scanning রাখুন। বিস্তারিত Git নিরাপত্তা পর্বে।
- ফাঁস হলে আগে rotate: Commit মুছলেই secret মোছে না। কেউ হয়তো আগেই কপি করেছে। তাই আগে rotation করে পুরোনোটি বাতিল করুন, তারপর log দেখুন, শেষে ইতিহাস পরিষ্কার করুন।
Randomness: শুধু CSPRNG
লুডুর ছক্কার জন্য যে র্যান্ডম চলে, ব্যাংকের OTP-র জন্য তা চলে না। সাধারণ PRNG তৈরি হয় দ্রুততা আর সমান বণ্টনের জন্য, অনুমান ঠেকানোর জন্য নয়। Token, session id, reset link, salt, key আর nonce-এর জন্য লাগে CSPRNG: আগের সব আউটপুট দেখেও পরেরটি অনুমান করা বাস্তবে অসম্ভব। Python-এ secrets, Node.js-এ crypto.randomBytes, browser-এ crypto.getRandomValues, Java-তে SecureRandom, Go-তে crypto/rand, PHP-তে random_bytes.
তুলনা: সময়ও তথ্য ফাঁস করে
ধরুন সিন্দুকের তালা প্রথম অঙ্ক ভুল হলেই সাথে সাথে শব্দ করে, দ্বিতীয় অঙ্ক ভুল হলে একটু পরে। শব্দের সময় শুনেই কেউ এক এক অঙ্ক করে এগোতে পারত। সাধারণ == প্রথম অমিলেই থেমে যায়, তাই সময় বলে দেয় কতটা মিলেছে। MAC, token বা signature তুলনায় লাগে constant-time comparison: Python-এ hmac.compare_digest, Node.js-এ crypto.timingSafeEqual, Java-তে MessageDigest.isEqual, PHP-তে hash_equals, Go-তে subtle.ConstantTimeCompare. এই বিষয়ের ভিত্তি দেখুন পর্ব ৯-এ।
import hmac, math, random, secrets
# 1. random: same seed, same "token". Never use it for secrets.
one, two = random.Random(2026), random.Random(2026)
print("random A:", "%032x" % one.getrandbits(128))
print("random B:", "%032x" % two.getrandbits(128))
# 2. secrets: from the OS CSPRNG. Do not print real tokens; show only shape.
token = secrets.token_urlsafe(32)
print("secrets token length:", len(token), "chars from 32 random bytes")
print("two secrets tokens equal?", secrets.token_hex(32) == secrets.token_hex(32))
# 3. How much guessing room?
print("6-digit code bits:", round(math.log2(10**6), 1))
print("32-byte token bits:", 32 * 8)
# 4. Early-exit compare leaks HOW FAR the match went (toy counter, no timing).
def toy_equal(a, b):
steps = 0
for x, y in zip(a, b):
steps += 1
if x != y:
return False, steps
return len(a) == len(b), steps
stored = "example-sig-7f3a"
for guess in ("Xxample-sig-7f3a", "example-Xig-7f3a", "example-sig-7f3X"):
print(guess, "toy steps:", toy_equal(stored, guess)[1],
"| compare_digest:", hmac.compare_digest(stored, guess))
print("match:", hmac.compare_digest(stored, "example-sig-7f3a"))দুই random generator একই seed পেয়ে হুবহু একই লেখা দিয়েছে: f38b2ffc80a4df5a51c9bc701e7ea419. Generator-এর ভেতরের অবস্থা জানা গেলে পরের সব মানও জানা যায়। secrets দেয় 43 অক্ষর, ৩২ byte থেকে। আর দুটি token মেলে না: False. ৬ অঙ্কের কোডে আছে মাত্র 19.9 bit, তাই OTP-কে rate limit আর অল্প মেয়াদ বাঁচায়, আর ৩২ byte token-এ 256 bit. শেষ অংশে খেলনা গণনায় প্রথম অক্ষর ভুল হলে ধাপ 1, নবম অক্ষর ভুল হলে 9, শেষ অক্ষর ভুল হলে 16. অর্থাৎ early-exit তুলনার কাজের পরিমাণ মিলের দৈর্ঘ্য বলে দেয়। এখানে সত্যিকারের সময় মাপা হয়নি, শুধু ধারণাটি গোনা হয়েছে। compare_digest ঠিক সিদ্ধান্ত দেয় আর সময়ের এই পার্থক্য এড়ানোর জন্যই তৈরি।
৯ · বিপজ্জনক API, safe default আর review-এর অভ্যাস
কিছু API হলো ধারালো ছুরি। এদের কাজই হলো লেখাকে কোড বানানো, বা বাইরের ডেটা থেকে object তৈরি করা। ভেতর থেকে আসা ডেটায় এগুলো চলে, কিন্তু সীমানার বাইরের ডেটা পেলেই বিপদ।
| শ্রেণি | কিছু উদাহরণ | নিরাপদ বিকল্প |
|---|---|---|
| লেখা থেকে কোড (CWE-94) | eval, exec, new Function | Map থেকে ফাংশন বাছাই, JSON.parse, Python-এ ast.literal_eval |
| অনিরাপদ deserialization (CWE-502) | Python pickle, Java ObjectInputStream, PHP unserialize, yaml.load | JSON আর schema যাচাই, yaml.safe_load |
| Shell চালানো (CWE-78) | os.system, shell=True, Node exec, C system() | Argument array, বা library |
| নিরাপত্তা বন্ধ করা | verify=False, wildcard CORS | আসল কারণ ঠিক করা |
| মেমরি-অনিরাপদ C ফাংশন | gets (C11-এ বাদ), strcpy | সীমাসহ ফাংশন, দেখুন পর্ব ৪৭ |
Library বাছাইয়ের সময় safe default দেখুন। ডিফল্টে নিরাপদ পথ থাকলে ভুলে যাওয়া কোনো বিপদ আনে না। তবে নিজে যাচাই করুন, কারণ ডিফল্ট সবসময় নিরাপদ নয়। যেমন Jinja2-তে auto-escaping নিজে থেকে চালু থাকে না, যদিও Flask সাধারণত চালু করে দেয়। Python-এর requests-এ timeout না দিলে কোনো সময়সীমা থাকে না। তাই প্রতিটি নতুন library-তে দেখুন: TLS যাচাই, timeout, escaping আর আকার সীমার ডিফল্ট কী?
Linter আর type system — যন্ত্রকে পাহারায় বসান
মানুষ ক্লান্ত হয়, যন্ত্র হয় না। Linter আর SAST tool কোড না চালিয়েই বিপজ্জনক ধরন খোঁজে: Python-এ Bandit বা Ruff, বহু ভাষায় Semgrep, C-তে compiler-এর -Wall -Wextra -Wconversion. এগুলো উদাহরণ মাত্র, আর false positive থাকবেই। বিস্তারিত পর্ব ৪৯-এ।
Type system আরও শক্ত দেয়াল। Amount, UserId বা TrustedHtml-এর মতো আলাদা ধরন থাকলে “যাচাই না হওয়া মান sink-এ গেল” ভুলটি build-এর সময়ই ধরা পড়ে। Browser-এর Trusted Types ঠিক এই চিন্তার ফল। আর Rust-এর মতো ভাষা অনেক মেমরি ত্রুটি compile-এর সময়েই আটকায়।
Code review — একটি ভুল থেকে একটি নিয়ম
একটি ভুল ঠিক করলে একটি ঘর বাঁচে। ভুলটি থেকে নিয়ম বানালে পুরো মহল্লা বাঁচে। নিচে একটি pull request-এর review থেকে স্থায়ী নিয়ম হওয়ার পথ দেখুন।
- ধরাfinding
- নামCWE
- ঠিকfix + test
- খোঁজএকই ভুল
- নিয়মrule
- পাহারাCI
token = str(random.random())
send_reset_link(user, token)
CWE-338
Use of Cryptographically Weak PRNG
প্রভাব: reset token অনুমানযোগ্য হতে পারে
token = secrets.token_urlsafe(32)
send_reset_link(user, token)
def test_tokens_are_long_and_unique():
tokens = {new_token() for _ in range(1000)}
assert len(tokens) == 1000
assert all(len(t) >= 43 for t in tokens)
$ grep -rnE 'random\.(random|randint|choice)\(' src/
src/invite.py:18: code = random.randint(0, 999999)
src/banner.py:7: tip = random.choice(tips)
rules:
- id: no-random-for-tokens
languages: [python]
severity: ERROR
message: Use the secrets module for tokens
pattern-either:
- pattern: random.random(...)
- pattern: random.randint(...)
CI: নতুন নিয়ম চলে, ভাঙলে merge বন্ধ Checklist: “নতুন token কি CSPRNG থেকে?” ব্যতিক্রম: কারণসহ লেখা, যেমন banner.py
- ধরা পড়ল। Reviewer দেখলেন password reset token আসছে
random.random()থেকে। পরীক্ষায় সব পাস করেছিল, কারণ token কাজ ঠিকই করে। - নাম দিন। ভুলটির নাম CWE-338. নাম থাকলে তীব্রতা, প্রতিকার আর দলের আলোচনা সহজ হয়। প্রভাবও লিখুন, শুধু “খারাপ কোড” নয়।
- ঠিক করুন আর test দিন।
secretsব্যবহার করুন, আর একটি regression test যোগ করুন, যাতে ভুলটি ফিরে না আসে। - একই ভুল খুঁজুন। নিজের কোডে একই ধরন খুঁজুন।
invite.py-এর আমন্ত্রণ কোডও একই ভুল। কিন্তুbanner.py-এর পরামর্শ বাছাই নিরাপত্তার বিষয় নয়। তাই প্রতিটি ফল নিজে যাচাই করুন। - নিয়ম লিখুন। একই ভুল ভবিষ্যতে যন্ত্র ধরবে। পরিচিত tool-এ এমন নিয়ম আগে থেকেই থাকতে পারে, যেমন Bandit-এর B311, তাই আগে সেটি দেখুন।
- পাহারা বসান। নিয়মটি CI-তে চলবে, আর review checklist-এ একটি লাইন যোগ হবে। Banner-এর মতো ব্যতিক্রম কারণসহ লেখা থাকবে। এখন মানুষের মনোযোগ নতুন ভুলের জন্য বাঁচল।
প্রতিটি পরিবর্তনে এই দশটি প্রশ্ন করতে পারেন। বিস্তারিত পদ্ধতি আছে code review পর্বে।
- নতুন কোনো trust boundary তৈরি হলো? সেখানে parse আছে?
- প্রতিটি sink-এ প্রসঙ্গের সঠিক encoding বা bind আছে?
- Authentication আর authorization প্রতিটি পথে আছে? (পর্ব ৩৮)
- সংখ্যা আর আকারের সীমা, শূন্য আর ঋণাত্মক মান ভাবা হয়েছে?
- যাচাই আর কাজের মাঝে ফাঁক আছে? দুটি অনুরোধ একসাথে এলে?
- আকার, সময় আর সংখ্যার সীমা আছে? Regex কি নিরাপদ?
- Error হলে দরজা বন্ধ থাকে? কোনো exception গিলে ফেলা হয়েছে?
- Log বা error-এ secret বা ব্যক্তিগত তথ্য গেল?
- Secret, token, nonce আর তুলনা ঠিক পদ্ধতিতে হচ্ছে?
- বিপজ্জনক API, নতুন dependency বা বন্ধ করা নিরাপত্তা আছে?
SEI CERT আর CWE Top 25
দুটি উৎস মনে রাখুন। SEI CERT coding standards Carnegie Mellon-এর Software Engineering Institute-এর ভাষাভিত্তিক নিয়মের সংকলন: C, C++, Java, Android আর Perl. INT32-C বলে signed overflow ঠেকাতে, INT31-C বলে রূপান্তরে ডেটা না হারাতে, FIO45-C বলে ফাইলে TOCTOU এড়াতে, MSC30-C বলে rand() ব্যবহার না করতে, আর ENV33-C বলে system() না ডাকতে। অর্থাৎ এই পর্বের প্রায় সব অভ্যাসের একটি লিখিত নিয়ম আছে, যা দলে মানদণ্ড হিসেবে নেওয়া যায়।
CWE Top 25 হলো MITRE-র বার্ষিক তালিকা, প্রকাশিত CVE ডেটা থেকে হিসাব করা। ৩১ অক্টোবর ২০২৬-এর যাচাই অনুযায়ী সর্বশেষ সংস্করণ ২০২৫-এরটি, প্রকাশ ডিসেম্বর ২০২৫, প্রায় ৩৯ হাজার CVE রেকর্ড থেকে। পুরো তালিকা MITRE-র সাইটে দেখে নিন, কারণ প্রতি বছর ক্রম বদলায়।
| ক্রম (২০২৫) | CWE | এই পর্বে |
|---|---|---|
| ১ | CWE-79 XSS | Output, পর্ব ৪০ |
| ২ | CWE-89 SQL injection | Output, পর্ব ৩৯ |
| ৯ | CWE-78 OS command injection | Shell API |
| ১০ | CWE-94 Code injection | eval শ্রেণি |
| ১৫ | CWE-502 Deserialization | pickle শ্রেণি |
| ১৮ | CWE-20 Improper input validation | Input সীমানা |
| ২০ | CWE-200 তথ্য ফাঁস | Error আর log |
| ২৫ | CWE-770 সীমাহীন resource | Resource limit |
তালিকায় না থাকা মানে নিরাপদ নয়। Integer overflow (CWE-190) আগের কয়েকটি তালিকায় ছিল, ২০২৫-এর তালিকায় নেই, কিন্তু ভুলটি হারিয়ে যায়নি। তালিকা ডেটা-নির্ভর, আপনার প্রকল্পের ঝুঁকি মাপে না। সেই কাজ করে threat modeling, আর পুরো প্রক্রিয়াটি পর্ব ৪৫-এর secure SDLC-এর অংশ।
আরও গভীরে: Regex ছাড়াও input-এর আকার অনুযায়ী অস্বাভাবিক খরচ হতে পারে। CVE-2020-10735-এর পরে Python খুব লম্বা সংখ্যা-লেখাকে int-এ রূপান্তরে ডিফল্ট সীমা বসায়, ৪,৩০০ অঙ্ক, কারণ সেই রূপান্তরের খরচ দৈর্ঘ্যের বর্গের মতো বাড়ে। একে বলা যায় algorithmic complexity-র ঝুঁকি। একই শ্রেণিতে পড়ে hash table-এর সংঘর্ষ, যার কারণে অনেক ভাষা এখন প্রতিবার ভিন্ন seed দিয়ে hash শুরু করে। শিক্ষা: সবচেয়ে খারাপ ক্ষেত্রের খরচও আপনার নকশার অংশ।
পরীক্ষা ও ইন্টারভিউতে:
- Allowlist আর denylist-এর পার্থক্য? Allowlist যা চলবে তা বলে, অচেনাকে খারিজ করে। Denylist অসম্পূর্ণ থাকতে বাধ্য।
- TOCTOU কীভাবে ঠেকাবেন? যাচাই আর কাজ একই atomic ধাপে করে: শর্তসহ UPDATE,
O_EXCL, lock. - Encoding কখন করবেন? Output-এর ঠিক আগে, প্রসঙ্গ দেখে।
- Reset token-এ
randomকেন ভুল? সেটি অনুমান ঠেকানোর জন্য তৈরি নয়। CSPRNG লাগে। - Secret commit হয়ে গেলে প্রথম কাজ? Rotate করে পুরোনোটি বাতিল করা। ইতিহাস মোছা তার পরের কাজ।
১০ · AI-এর যুগে secure coding
AI খুব দ্রুত কাজের কোড লেখে। কিন্তু “চলছে” আর “নিরাপদ” এক জিনিস নয়, আর AI প্রায়ই প্রথমটি দেখিয়েই থেমে যায়। সে আপনার threat model জানে না, আর আপনার কোডের সীমানাও চেনে না। তাই এই পর্বের প্রশ্নগুলো আপনাকেই করতে হবে।
| AI-এর সাধারণ ভুল | কেন সমস্যা | যাচাই |
|---|---|---|
Error ঠিক করতে verify=False, chmod 777 বা খোলা CORS | নিরাপত্তা বন্ধ করে error লুকায় | আসল কারণ চান, diff পড়ুন |
except: pass দিয়ে crash থামানো | Error গিলে ফেলে, fail open হতে পারে | নির্দিষ্ট exception, ডিফল্ট “না” |
Token-এ random, hash-এ MD5, ECB mode | পুরোনো উদাহরণ থেকে শেখা | CSPRNG আর পর্ব ১৪ মিলিয়ে দেখুন |
| Input জোড়া query বা authorization বাদ দেওয়া | Sample কোডে নিরাপত্তা থাকে না | প্রতিটি sink আর পথ review করুন |
| Secret কোডে বসানো, বা log-এ পুরো অনুরোধ লেখা | ফাঁসের পথ তৈরি হয় | Secret scanning চালান, log দেখুন |
| বানানো CVE, ভুল CWE বা অচল standard | ভুল প্রমাণের ওপর সিদ্ধান্ত | MITRE, NVD বা মূল নথি পড়ুন |
| “এই কোড নিরাপদ” নিশ্চিত সুরে বলা | অতিরিক্ত আত্মবিশ্বাস | Test, linter আর মানুষের review |
AI-কে কোড লিখতে বলার পরে আলাদা করে reviewer-এর ভূমিকা দিন। কোডটি আগে নিজের করুন, আর secret বা ব্যক্তিগত তথ্য সরিয়ে নিন।
AI-এর কোড merge করার আগে
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১“যা পাবেন উদারভাবে নিন” — এই পুরোনো পরামর্শ কেন নিরাপত্তার সাথে সংঘাতে যায়, আর parse don't validate কী বদলায়?
ইন্টারনেটের শুরুর দিনে Jon Postel-এর নামে পরিচিত robustness principle বলত: নিজে পাঠানোর সময় কড়া হোন, নেওয়ার সময় উদার হোন। RFC 761 আর পরের RFC-তে এই ভাবনা আছে। সেদিন এটি দারুণ কাজ করেছিল, কারণ ভিন্ন ভিন্ন system-কে জোড়া লাগানোই ছিল বড় চ্যালেঞ্জ।
সমস্যা হলো, উদার parser ভুল input-কে “যা ভাবছি তাই” ধরে নেয়, আর দুটি parser একই input-কে আলাদাভাবে বোঝে। একটি যাচাই করে, অন্যটি ব্যবহার করে, আর মাঝের অমিলই ত্রুটি হয়। ২০২৩ সালে IETF-এর RFC 9413 (Maintaining Robust Protocols) সরাসরি বলে যে এই নীতির অন্ধ অনুসরণ দীর্ঘমেয়াদে protocol-কে দুর্বল করতে পারে।
LangSec গবেষকেরা (Sassaman, Patterson, Bratus প্রমুখ) ২০১০-এর দশকে আরও গভীরে বলেন: input একটি ভাষা, আর সেই ভাষার parser-ই নিরাপত্তার সীমানা। কোড জুড়ে ছড়ানো ছোট ছোট যাচাইকে তাঁরা shotgun parsing বলেন। এই ধরনে যেকোনো একটি জায়গায় যাচাই বাদ পড়লেই বিপদ। ২০১৪ সালের Heartbleed-ও এই শ্রেণির শিক্ষা দেয়: বার্তার দাবি করা দৈর্ঘ্য আর আসল দৈর্ঘ্য মিলিয়ে দেখা হয়নি।
Alexis King-এর ২০১৯ সালের “Parse, don't validate” এই ভাবনাকে দৈনন্দিন কোডে নামিয়ে আনে। যাচাইয়ের ফল একটি হ্যাঁ বা না নয়, একটি নতুন ধরন। এতে “ভুলে যাচাই বাদ পড়ল” ধরনের ভুল compile-এর সময়েই ধরা যায়, আর ফাংশনের স্বাক্ষরই বলে দেয় কী নিশ্চিত।
তবে কড়া হওয়ারও দাম আছে। অতি কড়া নিয়ম সৎ ব্যবহারকারীকে আটকায়, আর পুরোনো client ভাঙে। তাই ভালো পথ হলো প্রোটোকল আর ধরন স্পষ্টভাবে লেখা, অস্পষ্টতায় খারিজ করা, আর বাংলা নাম বা সৎ বিশেষ চিহ্নের মতো বাস্তব ডেটা নিয়ে test করা।
মূল উপলব্ধি: উদারতা থাকুক মানুষের অভিজ্ঞতায়, কিন্তু সীমানা পেরোনোর পরে ডেটার অর্থ যেন একটিই থাকে।
প্র ০২একটি সংখ্যার রূপান্তর কীভাবে একটি রকেট হারাতে পারে, আর ২৪৮ দিন কোথা থেকে আসে?
১৯৯৬ সালের ৪ জুন Ariane 5-এর প্রথম উড্ডয়ন প্রায় ৪০ সেকেন্ডের মধ্যে ব্যর্থ হয়। তদন্ত প্রতিবেদন অনুযায়ী, Ariane 4 থেকে নেওয়া একটি কোডে ৬৪-bit দশমিক মান ১৬-bit signed পূর্ণসংখ্যায় রূপান্তরের সময় মান সীমা ছাড়ায়। সেই exception ধরা হয়নি, দুটি inertial reference system-ই বন্ধ হয়ে যায়, কারণ দুটিতে একই কোড ছিল। আর রকেটটি পথ হারায়।
লক্ষ করার মতো কয়েকটি দিক আছে। প্রথমত, কোডটি পুরোনো রকেটে ঠিক ছিল, কারণ সেখানে মান সীমার ভেতরে থাকত। ধরে নেওয়া সীমা লিখিত, পরীক্ষিত কোনো চুক্তি ছিল না। দ্বিতীয়ত, দ্বিগুণ ব্যবস্থা (redundancy) সাহায্য করেনি, কারণ একই বাগ দুটিতেই। এটি common-cause failure. তৃতীয়ত, ভুলটি ছিল এমন একটি অংশে, যা ওড়ার পরে আর দরকারই ছিল না।
২০১৫ সালে মার্কিন FAA একটি নির্দেশ জারি করে, যাতে বলা হয় Boeing 787-এর একটি বিদ্যুৎ নিয়ন্ত্রণ ইউনিট টানা ২৪৮ দিন চালু থাকলে নিরাপদ মোডে যেতে পারে। সংখ্যাটি হিসাব করা যায়। যদি একটি signed ৩২-bit counter সেকেন্ডে ১০০ বার বাড়ে, তাহলে 231 ÷ 100 ÷ 86,400 ≈ ২৪৮.৫৫ দিনে সেটি উপচে পড়ে। প্রকাশিত বিশ্লেষণে এই হিসাবকেই সম্ভাব্য কারণ বলা হয়েছে। সাময়িক প্রতিকার ছিল নির্দিষ্ট সময় পরপর system আবার চালু করা।
নিরাপত্তার কোডে একই গণিত কাজ করে। আকারের যাচাইয়ে overflow হলে ছোট buffer বরাদ্দ হয়, আর টাকার হিসাবে ঘুরে গেলে ভুল বিল হয়। পার্থক্য শুধু একটি: এখানে কেউ হয়তো ইচ্ছে করে সীমায় ঠেলছে। তাই “বাস্তবে এত বড় মান আসবে না” কথাটি সীমানার বাইরের ডেটার জন্য কোনো যুক্তি নয়।
মূল উপলব্ধি: প্রতিটি ধরনের সীমা একটি লিখিত নকশার সিদ্ধান্ত হওয়া উচিত। পুরোনো কোড নতুন পরিবেশে নিলে তার ধরে নেওয়া সীমাগুলোও আবার যাচাই করতে হয়।
প্র ০৩Fail closed কি সবসময় ঠিক? “goto fail” আর availability-র টানাপোড়েন থেকে কী শিখি?
২০১৪ সালে Apple-এর TLS কোডে একটি ত্রুটি প্রকাশ পায়, CVE-2014-1266, যা “goto fail” নামে পরিচিত। Certificate-এর signature যাচাইয়ের ফাংশনে একটি goto fail; লাইন দুইবার লেখা ছিল। দ্বিতীয় লাইনটি কোনো শর্তের অধীনে ছিল না, তাই পরের যাচাই আর চলত না। আর সেই মুহূর্তে error-এর মান ছিল “সফল”। ফলে যাচাই বাদ পড়লেও ফাংশন সাফল্য জানাত।
এই ঘটনা থেকে কয়েকটি প্রতিরক্ষার শিক্ষা আসে। ফলাফলের ডিফল্ট মান ব্যর্থতা হলে এই ভুল কম ক্ষতি করত। Compiler-এর unreachable code warning, বন্ধনী { } ছাড়া if এড়ানোর style নিয়ম, আর ত্রুটিপূর্ণ certificate দিয়ে negative test, যেকোনো একটি এটি ধরতে পারত। সাধারণ সফল-পথের test ধরে না, কারণ সেখানে সব ঠিকই থাকে।
এবার বিপরীত প্রশ্ন। সব জায়গায় fail closed কি ঠিক? Authorization, signature আর payment-এ হ্যাঁ। কিন্তু হাসপাতালের জরুরি রোগীর তথ্য বা অগ্নিকাণ্ডের সময় স্বয়ংক্রিয় দরজায় উল্টো সিদ্ধান্ত হতে পারে। দরজা আগুনের সময় খুলে যাওয়াকে বলে fail safe, আর সেখানে মানুষের জীবন আগে।
আরেকটি বিপদ হলো নিরাপত্তা-ব্যবস্থা নিজেই বড় বিভ্রাট তৈরি করতে পারে। ২০১৯ সালের জুলাইয়ে Cloudflare-এর প্রকাশিত প্রতিবেদন অনুযায়ী, একটি WAF নিয়মের regex-এর backtracking CPU-কে আটকে দিয়ে প্রায় আধা ঘণ্টার বিভ্রাট ঘটায়। অর্থাৎ নিরাপত্তার কোডের সীমাও সীমাহীন হতে পারে না। ভালো নকশায় তাই নিরাপত্তা-ব্যবস্থার জন্যও ধাপে ধাপে rollout, দ্রুত ফেরার পথ আর break-glass থাকে।
সিদ্ধান্তটি তাই আগে থেকে, ঝুঁকি দেখে নিতে হয়। লিখে রাখুন: এই কাজ ব্যর্থ হলে কী হবে, কে সিদ্ধান্ত নেবেন, আর সেটি কীভাবে দেখা যাবে।
মূল উপলব্ধি: ব্যর্থতার আচরণ একটি নকশার সিদ্ধান্ত, কোডের দুর্ঘটনা নয়। ডিফল্ট “না”, আর ব্যতিক্রম হবে ভেবেচিন্তে, লিখিতভাবে।
প্র ০৪একটি warning বন্ধ করতে একটি লাইন মোছায় কীভাবে এক প্রজন্মের key দুর্বল হয়ে গেল?
২০০৮ সালের মে মাসে Debian প্রকাশ করে CVE-2008-0166. ২০০৬ সালের একটি পরিবর্তনে Debian-এর OpenSSL package-এর random সংখ্যা তৈরির কোড থেকে এমন লাইন বাদ পড়েছিল, যা entropy মেশাত। মূলত Valgrind নামের মেমরি-পরীক্ষার tool-এর একটি warning দূর করতে পরিবর্তনটি করা হয়েছিল। এরপর প্রায় দুই বছর ধরে Debian আর Ubuntu-তে তৈরি হওয়া key-র প্রধান পরিবর্তনশীল উৎস ছিল process id, যার মান সীমিত। ফলে সম্ভাব্য key-র সংখ্যা বিপজ্জনকভাবে ছোট হয়ে যায়।
ভয়ের দিক হলো, কিছুই “ভাঙেনি”। Key দেখতে স্বাভাবিক ছিল, encryption চলছিল, test পাস করছিল। Randomness-এর ত্রুটি চোখে দেখা যায় না, কারণ একটি খারাপ random সংখ্যাও দেখতে random লাগে। সমাধানও ছিল বেদনাদায়ক: package ঠিক করাই যথেষ্ট ছিল না, ঐ সময়ে তৈরি SSH আর TLS key আবার বানাতে হয়েছিল।
এই ঘটনা কয়েকটি অভ্যাস শেখায়। Cryptography-র কোড বদলাতে বিশেষজ্ঞের review লাগে, আর সম্ভব হলে মূল প্রকল্পের সাথে আলোচনা। Warning দূর করা মানে warning-এর কারণ বোঝা, লাইন মুছে দেওয়া নয়। আর নিজে entropy জোড়া দেওয়ার বদলে অপারেটিং সিস্টেমের পরীক্ষিত উৎস ব্যবহার করা উচিত। Python-এর secrets module (PEP 506, Python ৩.৬) এই কারণেই এসেছে: সঠিক পথকে সহজ পথ বানাতে।
একই শিক্ষা nonce-এর ক্ষেত্রেও। ECDSA-তে একই nonce দুইবার ব্যবহার করলে private key উদ্ধারযোগ্য হয়ে যায়, তাই RFC 6979-এর deterministic পদ্ধতিও এসেছে। অর্থাৎ randomness-এর ত্রুটি একটি অ্যালগরিদমের সমস্ত গাণিতিক শক্তি বাতিল করে দিতে পারে।
আজকের প্রসঙ্গে এই গল্পের একটি নতুন রূপ আছে। AI বা সহকর্মী warning দূর করতে একটি পরিবর্তন প্রস্তাব করলে সেটি দেখতে খুব নিরীহ লাগে। তাই প্রশ্ন করুন, “এই লাইনটি কেন ছিল?” এর উত্তর না জেনে কোনো নিরাপত্তা-কোড মোছা ঠিক নয়।
মূল উপলব্ধি: নিরাপত্তার ত্রুটি অনেক সময় নীরব। যা চোখে পরীক্ষা করা যায় না, তার জন্য পরীক্ষিত উৎস, review আর দায়িত্বশীল পরিবর্তন প্রক্রিয়াই ভরসা।
প্র ০৫এত বিপজ্জনক হলে pickle বা eval ভাষায় এখনো আছে কেন? দোষ কি প্রোগ্রামারের, না নকশার?
এই API-গুলো কাজের বলেই তৈরি হয়েছিল। pickle যেকোনো Python object সহজে জমা রাখতে পারে, আর eval ছোট গণনা সহজ করে। ঝুঁকি থাকে একটি ধরে নেওয়া কথায়: ডেটা আপনার নিজের। Python-এর নথিতেই সতর্কবার্তা আছে যে অবিশ্বস্ত উৎসের pickle খোলা নিরাপদ নয়, কারণ খোলার সময় কোড চলতে পারে। সমস্যা হয় যখন একই ডেটা পরে network, cache বা upload-এর পথে আসে, আর সেই ধরে নেওয়া কথাটি কেউ মনে রাখে না।
Java জগতে ২০১৫ সালের দিকে deserialization-এর ঝুঁকি ব্যাপক আলোচনায় আসে, যখন গবেষকেরা দেখান যে বহুল ব্যবহৃত library-র class মিলে অপ্রত্যাশিত আচরণ তৈরি হতে পারে। অর্থাৎ ঝুঁকি আপনার নিজের কোডে নয়, classpath-এ থাকা জিনিসের মধ্যে। Microsoft পরে .NET-এর BinaryFormatter-কে নিরুৎসাহিত করে, আর .NET ৯-এ তার implementation সরিয়ে নেয়। আবার PyYAML ৫.১ (২০১৯) থেকে yaml.load-এ Loader বলে দেওয়ার দিকে যায়, কারণ পুরোনো ডিফল্ট ঝুঁকিপূর্ণ ছিল।
২০২১ সালের Log4Shell এর সবচেয়ে বড় উদাহরণ। Log4j-র একটি lookup সুবিধা ডিফল্টে চালু ছিল, যাতে log-এর লেখা বাইরের resource খোঁজার কারণ হতে পারত। একটি “log লেখা” কাজ যে লেখাকে আদেশ বানাতে পারে, অনেক দলই তা জানত না। পরে এই আচরণ ডিফল্টে বন্ধ করা হয়।
তাই দোষ শুধু প্রোগ্রামারের নয়। মানুষ ক্লান্ত হলে যা সবচেয়ে সহজ, তাই করে। নকশার দায়িত্ব হলো সহজ পথটিকেই নিরাপদ পথ করা, আর ঝুঁকির পথে আলাদা, স্পষ্ট নাম দেওয়া। yaml.safe_load আর dangerouslySetInnerHTML এর ভালো উদাহরণ, কারণ নামটিই সতর্ক করে। এটিই secure by default-এর চিন্তা।
আপনার দলের জন্য বাস্তব পদক্ষেপ: বিপজ্জনক API-র একটি তালিকা রাখুন, linter দিয়ে নতুন ব্যবহার আটকান, আর ব্যতিক্রম হলে কারণ লিখুন। প্রয়োজনে HMAC দিয়ে সই করা ডেটা শুধু অখণ্ডতা দেয়, কিন্তু key ফাঁস হলে আবার সেই ঝুঁকি। তাই সাধারণত JSON-এর মতো শুধু-ডেটা format-ই ভালো।
মূল উপলব্ধি: ভালো নিরাপত্তা মানুষের সতর্কতার ওপর ভর করে না। সে এমন নকশা বানায়, যেখানে অসতর্ক হলেও নিরাপদ পথেই থাকা যায়।
অনুশীলন
প্রথমে নিজে চেষ্টা করুন, তারপর উত্তর মিলিয়ে নিন।
-
হিসাব: একটি signed ৩২-bit ঘরে দাম 3,000,000 পয়সা আর পরিমাণ 2,000 গুণ করা হলো। (ক) আসল ফল কত? (খ) ঘরে কী থাকবে? (গ) গুণের আগে কোন যাচাই এটি থামাত? (ঘ) ৪ অঙ্কের PIN-এ কত bit, আর ১৬ byte token-এ কত bit?
(ক) 3,000,000 × 2,000 = 6,000,000,000.
(খ) 232 = 4,294,967,296 বাদ দিলে থাকে 1,705,032,704. এটি 231 − 1-এর চেয়ে ছোট, তাই ঘরে থাকবে ধনাত্মক
1705032704. দেখতে স্বাভাবিক, কিন্তু ভুল।(গ)
price > INT32_MAX / qty. 2,147,483,647 ÷ 2,000 ≈ 1,073,741. দাম এর চেয়ে বড়, তাই খারিজ।(ঘ) log2(10,000) ≈ ১৩.৩ bit, আর 16 × 8 = ১২৮ bit. PIN-কে তাই চেষ্টার সীমা বাঁচায়, দৈর্ঘ্য নয়।
-
আউটপুট অনুমান করুন: নিচের Python কোড চালালে তিন লাইনে কী ছাপা হবে? কেন?
import re print(70000 & 0xFFFF) print(bool(re.fullmatch(r"\d+", "৪২"))) print(int("৪২") + 1)4464,True,43.প্রথম লাইনে ওপরের bit বাদ পড়ে, ঠিক ১৬-bit ঘরে truncation-এর মতো: 70000 − 65536 = 4464. দ্বিতীয় লাইনে Python-এর
\dযেকোনো Unicode দশমিক অঙ্ক মেলায়, তাই বাংলা অঙ্কও মিলল। তৃতীয় লাইনেint-ও বাংলা অঙ্ক পড়তে পারে। এটি বাগ নয়, কিন্তু আপনি হয়তো শুধু ASCII অঙ্ক ভেবেছিলেন। তাই নিয়ম স্পষ্ট লিখুন:[0-9]অথবাre.ASCII. আর বাংলা অঙ্ক চাইলে সেটিও ইচ্ছে করে, ভেবেচিন্তে অনুমতি দিন। -
Code review: নিচের কাল্পনিক ফাংশনে অন্তত ছয়টি সমস্যা খুঁজে প্রতিটির সংশোধন লিখুন।
API_KEY = "example-token" def issue_reset(user_id, raw_amount): try: limit = int(raw_amount) token = str(random.random()) log.info(f"reset for {user_id} token={token} key={API_KEY}") os.system("send_mail " + user_id) return token except: pass def check(sent, stored): return sent == stored- Secret কোডে: vault বা environment থেকে আনুন, আর ফাঁস ধরে rotate করুন।
- Input parse নেই:
user_idআরraw_amount-কে সীমানায় typed value বানান, পরিসর দেখুন। - দুর্বল randomness:
secrets.token_urlsafe(32). - Log-এ secret: token আর key বাদ দিন, শুধু event আর user id রাখুন।
- Shell জোড়া: mail library, বা argument array দিয়ে
subprocess.run. - Exception গিলে ফেলা: নির্দিষ্ট exception ধরুন, log করুন, ব্যর্থতা স্পষ্ট ফেরত দিন।
- সাধারণ
==:hmac.compare_digest. আর token জমা রাখুন hash করে, মেয়াদ আর একবার ব্যবহারের নিয়মসহ।
-
নিজের কোডে খোঁজ: নিজের কোনো প্রকল্পে নিচের কমান্ডটি চালান। প্রতিটি ফল তিন ভাগে ভাগ করুন: ঠিক আছে, ঠিক করতে হবে, নিয়ম লিখতে হবে। প্রথম ধাপের ফলাফল থেকে একটি নিয়ম কীভাবে বানাবেন?
# Run only on your own source tree. A starting hint, not proof. grep -rnE 'eval\(|pickle\.loads?\(|shell=True|verify=False' src/ grep -rnE 'except:|except Exception:' src/ | head -n 20প্রতিটি ফলের জন্য তিনটি প্রশ্ন করুন: ডেটা কি সীমানার বাইরে থেকে আসে? ব্যর্থ হলে কী হয়? নিরাপদ বিকল্প আছে? যেমন test-এর ভেতরের
verify=Falseহয়তো গ্রহণযোগ্য, যদি শুধু নিজের স্থানীয় test server-এর জন্য হয়। কিন্তু production-এর একই লাইন জরুরি ত্রুটি।এরপর অংশ ৯-এর review-এর পথ ধরুন: নাম দিন (CWE-502 বা CWE-295), ঠিক করুন, test দিন, একই ধরন খুঁজুন, linter-এর নিয়ম চালু করুন, CI-তে বসান।
grepশুধু শুরু। এটি মন্তব্যও ধরে, আর নতুন ধরন ছেড়ে দেয়। প্রমাণ আসে পড়ে বুঝে নেওয়া থেকে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি input ঢোকা থেকে বের হওয়া পর্যন্ত নিরাপদ ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- আকারের সীমা দেখা
- Canonicalise করে একটি মানক রূপে আনা
- Allowlist দিয়ে গঠন মেলানো
- Typed value-তে parse করে পরিসর দেখা
- ভেতরের কোডে ব্যবসার কাজ করা
- Sink-এর ঠিক আগে প্রসঙ্গ অনুযায়ী encode বা bind করা
“Parse, don't validate” নীতির মূল কথা কোনটি?
- যাচাই না করে ডেটা সরাসরি ব্যবহার করা
- যাচাই করেই ডেটাকে নির্দিষ্ট typed value-তে রূপ দেওয়া
- প্রতিটি ফাংশনে আলাদা করে string যাচাই করা
- শুধু browser-এ যাচাই করা
একটি system আগে নাম যাচাই করে, তারপর সেটিকে Unicode normalize করে সংরক্ষণ করে। মূল ত্রুটিটি কী?
- Unicode ব্যবহার করাই ভুল
- যাচাইকে আরও লম্বা denylist দিয়ে বদলানো দরকার
- যাচাই হয়েছে canonicalise-এর আগে, তাই যাচাই করা মান আর জমা মান আলাদা হতে পারে
- Output encoding বাদ পড়েছে
দুটি অনুরোধ একই মুহূর্তে balance 1000 দেখে প্রত্যেকে 800 তুলে নিল। সবচেয়ে সরাসরি প্রতিকার কোনটি?
- যাচাই আর লেখার মাঝে sleep দেওয়া
- প্রতিটি উত্তোলন log করা
- Browser-এ বোতাম একবার চাপার পরে বন্ধ করে দেওয়া
- যাচাই আর লেখা একই atomic ধাপে করা, যেমন শর্তসহ UPDATE
একটি permission যাচাইয়ের service-এ timeout হলো। Authorization-এর ক্ষেত্রে নিরাপদ ডিফল্ট আচরণ কী?
- অনুরোধ অস্বীকার করা, আর কারণ log করা
- অনুরোধ অনুমতি দেওয়া, যাতে কাজ না থামে
- Exception ধরে কিছু না করা
- ব্যবহারকারীকে পুরো stack trace দেখানো
Password reset token তৈরির জন্য Python-এ কোনটি সঠিক?
str(random.random())secrets.token_urlsafe(32)md5(str(time.time()))- ব্যবহারকারীর id আর একটি ক্রমিক সংখ্যা
একটি API key ভুলে public repository-তে commit হয়ে গেছে। প্রথম কাজ কোনটি?
- শুধু commit history পরিষ্কার করা
- Repository private করে দেওয়া, আর কিছু না করা
- Key rotate করে পুরোনোটি বাতিল করা, তারপর ব্যবহারের log দেখা
- পরের commit-এ শুধু লাইনটি মুছে ফেলা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের শব্দগুলো এখানে একসাথে:
মনে রাখুন
- Input সীমানায়: আকার → canonicalise → allowlist → typed parse. Output-এ প্রসঙ্গ দেখে encode বা bind.
- Integer সীমা কাজের আগে যাচাই করুন, আর সব resource-এ সীমা রাখুন: আকার, সময়, সংখ্যা, regex.
- যাচাই আর কাজ এক atomic ধাপে। মাঝের ফাঁকই TOCTOU.
- ব্যর্থ হলে fail closed. Exception গিলবেন না, আর error বা log-এ secret দেবেন না।
- Secret কোডের বাইরে, ফাঁস হলে আগে rotate. Token-এ CSPRNG, তুলনায় constant-time, আর একটি ত্রুটিকে স্থায়ী নিয়মে বদলান।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৭ · Memory safety — overflow-এর ধারণা, mitigation আর নিরাপদ ভাষাপরবর্তী পর্বআকারের ভুল মেমরিতে গেলে কী হয়।
- সাইবার নিরাপত্তা · কোর্স হোম৮০টি পর্বপুরো কোর্সের রোডম্যাপ।
- Code reviewপাশের ট্র্যাকReview-এর পদ্ধতি আর দলের অভ্যাস।
- Secrets আর KMSপাশের ট্র্যাকCloud-এ secret রাখা আর rotate করা।
- Undefined behavior আর secure codingগভীরে যানC-তে overflow আর নিরাপদ অভ্যাস।