এই পর্বে যা শিখবেন
- Broken access control কেন এত সাধারণ, আর কেন সাধারণ tool একে সহজে ধরতে পারে না
/orders/<id>ধরনের IDOR-এর কারণ আর অনিরাপদ-নিরাপদ কোডের পাশাপাশি রূপ- Function-level check, mass assignment, multi-tenant ফাঁস আর business logic ত্রুটি চেনা
- Deny by default আর এক জায়গায় enforcement দিয়ে নকশা করা, মালিকানা query-তে বসানো
- অনুমোদন-ছকের test লেখা, প্রত্যাখ্যান log করা আর alert থেকে সাড়া দেওয়া
১ · পরিচয়পত্র দেখা হলো, লকার মিলিয়ে দেখা হলো না
একটি ব্যাংকের লকার কক্ষের কথা ভাবুন। দারোয়ান পরিচয়পত্র দেখে আপনাকে ভেতরে যেতে দিলেন। ভেতরে গিয়ে আপনি একটি চাবি হাতে ৪২ নম্বর লকারের সামনে দাঁড়ালেন। লকারটি আপনার নয়, কিন্তু কর্মকর্তা খাতায় মিলিয়ে দেখলেন না, শুধু ভাবলেন “ভেতরে যখন এসেছেন, ঠিকই আছে।” দারোয়ানের কাজে কোনো ভুল ছিল না। ভুল ছিল পরের ধাপে, যেখানে কেউ জিজ্ঞেস করেনি: এই লকার কি এই মানুষের?
Broken access control ঠিক এই ভুল। Authentication বলে “আপনি কে”, আর authorization বলে “আপনি এখানে কী করতে পারেন”. RBAC, ABAC আর least privilege-এর নকশা পর্ব ১৯-এ দেখেছেন। সেখানে প্রশ্ন ছিল নিয়মটি কেমন হবে। এই পর্বের প্রশ্ন আলাদা: নিয়মটি লেখা আছে, কিন্তু কোডের প্রতিটি পথে সেটি সত্যিই প্রয়োগ হচ্ছে কি না।
Authorization হলো server-এর সিদ্ধান্ত, প্রতিটি অনুরোধে। ব্যবহারকারী URL, form, header, cookie-র যেকোনো কিছু বদলে পাঠাতে পারেন। তাই server ধরে নেবে অনুরোধের সবকিছুই দাবি, প্রমাণ নয়। এটাই complete mediation, Saltzer ও Schroeder-এর ১৯৭৫ সালের নীতি, যা পর্ব ৩-এ এসেছে।
কেন এটি তালিকার শীর্ষে
OWASP Top 10 হলো web application-এর প্রধান ঝুঁকির একটি সচেতনতা-তালিকা। পুরো তালিকা পর্ব ৩৭-এ পাবেন। ২০২১ সালে broken access control পঞ্চম স্থান থেকে উঠে প্রথমে আসে, আর ২০২৫ সংস্করণেও প্রথমেই আছে। OWASP-এর ২০২৫ পাতা অনুযায়ী পরীক্ষিত application-গুলোর সবকটিতে (১০০%) এর কোনো না কোনো রূপ মিলেছে, আর এই শ্রেণির সঙ্গে ৪০টি CWE যুক্ত। সংখ্যাগুলো অংশ নেওয়া প্রতিষ্ঠানের ডেটা থেকে এসেছে, তাই পুরো ইন্টারনেটের চিত্র হিসেবে নয়, একটি দিকনির্দেশ হিসেবে পড়ুন।
কারণগুলো গঠনগত, শুধু অসাবধানতা নয়:
- নিয়মটি প্রতিটি app-এর নিজস্ব। “অর্ডার তার মালিক দেখবেন” কথাটি কোডের ভাষায় কোথাও লেখা না থাকলে যন্ত্র বুঝবে কী করে? Injection-এর একটি চেনা আকৃতি আছে, মালিকানার নেই।
- ভুলটি অনুপস্থিতি। ত্রুটি কোনো খারাপ লাইনে নেই, একটি না-লেখা লাইনে। পড়ে ধরা কঠিন।
- সাধারণ ব্যবহারে কিছুই ভাঙে না। রিনা নিজের অর্ডারই দেখেন, তাই test পাস করে, গ্রাহক অভিযোগ করেন না।
- পথ অসংখ্য। একটি নতুন route, একটি পুরোনো API version বা একটি CSV export, যেকোনো একটিতে যাচাই বাদ পড়লেই যথেষ্ট।
আইনি সীমা: এই পর্বের সব পরীক্ষা নিজের app-এ, নিজের test account দিয়ে, অথবা লিখিত অনুমতি নিয়ে। অন্যের site-এ URL-এর সংখ্যা বদলে অন্য কারও তথ্য দেখা “সহজ ছিল” বলে বৈধ হয়ে যায় না; এটি অনুমতি ছাড়া প্রবেশ। হঠাৎ এমন কিছু চোখে পড়লে থামুন, আর কিছু দেখবেন না বা নামাবেন না, আর প্রতিষ্ঠানের ঘোষিত পথে জানান। বিস্তারিত পর্ব ৬-এ আছে।
২ · Horizontal আর vertical — অনুমতি ভাঙার দুই দিক
একটি কাল্পনিক অনলাইন বাজার ধরুন। রিনা আর মিতু গ্রাহক, কামাল admin. অনুমতি দুই দিকে ভাঙতে পারে। রিনা যদি মিতুর অর্ডার দেখতে পান, সেটি horizontal সমস্যা: পদ একই, কিন্তু অন্যের জিনিস। রিনা যদি refund দেওয়ার মতো admin কাজ করতে পারেন, সেটি vertical সমস্যা: নিজের চেয়ে ওপরের ক্ষমতা, যাকে elevation of privilege বলে।
অনুমতি ভাঙার দুই দিক
Horizontal: same level, someone else's data. Vertical: a higher level's power.
একই ভাগ OWASP API Security Top 10 (২০২৩)-এ আরও সূক্ষ্মভাবে আছে। মিলিয়ে নিন, কারণ ইন্টারভিউতে এই নামগুলো প্রায়ই আসে:
| ধরন | প্রশ্নটি যা বাদ পড়েছে | প্রচলিত নাম |
|---|---|---|
| Object-স্তর | এই বস্তুটি কি এই মানুষের? | IDOR, BOLA (API1), CWE-639 |
| Function-স্তর | এই কাজটি কি এই পদের? | BFLA (API5), CWE-862 |
| Field-স্তর | এই ঘরটি কি এই মানুষ পড়তে বা লিখতে পারেন? | Mass assignment, BOPLA (API3), CWE-915 |
| Tenant-স্তর | এই ডেটা কি এই প্রতিষ্ঠানের? | Cross-tenant ফাঁস |
| প্রবাহ-স্তর | এই কাজের ক্রম, পরিমাণ আর মান কি নিয়মের ভেতরে? | Business logic ত্রুটি, API6 |
৩ · IDOR — /orders/<id> যেখানে server মালিক দেখে না
IDOR মানে Insecure Direct Object Reference. কঠিন নামের পেছনের ঘটনাটি সহজ। অর্ডারের পাতার ঠিকানা /orders/1042. শেষের সংখ্যাটি database-এর একটি সারিকে সরাসরি চিহ্নিত করে। Server সেই সংখ্যা দিয়ে সারি এনে দেখিয়ে দেয়, কিন্তু সারিটি কার, তা মেলায় না। ঠিকানার সংখ্যা ব্যবহারকারীর হাতে থাকা একটি দাবি, আর server সেটিকে অনুমতি ধরে নিয়েছে।
পাশাপাশি দেখুন। দুই কোডেই login আছে, দুটিই ঠিকঠাক চলে, পার্থক্য একটি শর্তে। এগুলো Express-ধাঁচের pseudocode, চালানোর জন্য নয়:
অনিরাপদ
app.get('/orders/:id', requireLogin,
async (req, res) => {
const order = await db.orders
.findById(req.params.id);
// কার অর্ডার, দেখা হলো না
res.json(order);
});নিরাপদ
app.get('/orders/:id', requireLogin,
async (req, res) => {
const order = await db.orders.findOne({
id: req.params.id,
ownerId: req.session.userId,
});
if (!order) return res.status(404).end();
res.json(order);
});নিরাপদ রূপে ownerId আসছে যাচাই করা session থেকে, URL বা form থেকে নয়। ?user=rina ধরনের মান দিয়ে পরিচয় ঠিক করলে আবার একই ভুল হবে। Session-এর ভিত্তি পর্ব ১৭-এ আছে। এবার একটি অনুরোধ ধাপে ধাপে দেখুন: রিনা login করা অবস্থায় এমন একটি অর্ডার চাইছেন, যা তাঁর নয়।
- অনুরোধ
/orders/1043দাবি - Session
rinaপ্রমাণ - Query
id + ownerদুই শর্ত - উত্তর
404ডেটা নেই
GET /orders/1043
Cookie: session=example-token
example-token -> user = rina
role = customer
SELECT * FROM orders
WHERE id = 1043
AND owner = 'rina'
id owner item 1042 rina Panjabi 1043 mitu Saree ফল: 0 সারি
HTTP 404 Not Found
event=order.not_visible user=rina object=1043 status=404
- অনুরোধ এলো। URL-এর
1043ব্যবহারকারীর লেখা একটি মান। Server এটিকে শুধু “কোন বস্তুর কথা হচ্ছে” হিসেবে পড়ে, অনুমতি হিসেবে নয়। - পরিচয় এলো session থেকে। Cookie দেখে server নিজের খাতা থেকে জানল ব্যবহারকারী
rina. এই মান ব্যবহারকারী লিখতে পারেন না। - Query-তে দুই শর্ত। শুধু
idনয়,owner = 'rina'-ও আছে। অর্থাৎ প্রশ্নটি “১০৪৩ কোনটি” নয়, “রিনার অর্ডারগুলোর মধ্যে ১০৪৩ কোনটি”. - কোনো সারি মেলেনি। অর্ডার ১০৪৩ আছে, কিন্তু মালিক
mitu. তাই দুই শর্ত একসাথে কোথাও সত্য নয়, ফল ০ সারি। - উত্তর 404. ডেটা database থেকে বেরই হয়নি। ব্যবহারকারী জানলেন না অর্ডারটি অন্যের, নাকি একেবারেই নেই।
- Log রইল। কে, কোন বস্তু চেয়েছিলেন, তা লেখা থাকল। অনেকগুলো ভিন্ন বস্তুতে এমন ঘটনা হলে অংশ ৯-এর alert বাজবে।
এবার একই অনুরোধ দুই handler-এ চালিয়ে দেখুন। সবকিছু কয়েকটি JavaScript object-এ আছে; কোনো আসল server বা network নেই।
// কাল্পনিক দোকান: in-memory ডেটা, কোনো আসল network নেই
const orders = [
{ id: 1042, owner: 'rina', item: 'Panjabi', total: 1800 },
{ id: 1043, owner: 'mitu', item: 'Saree', total: 3500 },
];
const deny = (status) => ({ status });
// অনিরাপদ: login দেখে, মালিকানা দেখে না
function getOrderVulnerable(req) {
if (!req.user) return deny(401);
const o = orders.find((x) => x.id === req.id);
return o ? { status: 200, body: o } : deny(404);
}
// নিরাপদ: মালিকানা lookup-এর শর্তের ভেতরে
function getOrderFixed(req) {
if (!req.user) return deny(401);
const o = orders.find((x) => x.id === req.id && x.owner === req.user);
return o ? { status: 200, body: o } : deny(404);
}
const requests = [
{ user: 'rina', id: 1042 }, // নিজের অর্ডার
{ user: 'rina', id: 1043 }, // অন্যের অর্ডার
{ user: 'rina', id: 9999 }, // নেই এমন অর্ডার
{ user: null, id: 1042 }, // login ছাড়া
];
for (const [name, handler] of [['vulnerable', getOrderVulnerable], ['fixed', getOrderFixed]]) {
console.log('--- ' + name + ' ---');
for (const r of requests) {
const res = handler(r);
const leak = res.body ? ' owner=' + res.body.owner : '';
console.log(`${r.user} GET /orders/${r.id} -> ${res.status}${leak}`);
}
}Output:
--- vulnerable ---
rina GET /orders/1042 -> 200 owner=rina
rina GET /orders/1043 -> 200 owner=mitu
rina GET /orders/9999 -> 404
null GET /orders/1042 -> 401
--- fixed ---
rina GET /orders/1042 -> 200 owner=rina
rina GET /orders/1043 -> 404
rina GET /orders/9999 -> 404
null GET /orders/1042 -> 401
দুই অংশে পার্থক্য একটি লাইনে: rina GET /orders/1043. অনিরাপদ handler 200 দিয়ে মিতুর অর্ডার পাঠিয়েছে, আর নিরাপদ handler 404 দিয়েছে। নিজের অর্ডার, নেই-এমন অর্ডার আর login-হীন অনুরোধের ফল দুই জায়গায় হুবহু এক। তাই সাধারণ test এই ভুল ধরত না; ধরা পড়ে শুধু “অন্যের বস্তু” পরীক্ষা করলে। খেয়াল করুন, নিরাপদ handler-এ ১০৪৩ আর ৯৯৯৯-এর উত্তর একই। এতে বাইরে থেকে বোঝা যায় না কোন অর্ডার আসলে আছে।
৪ · লুকানো বোতাম সুরক্ষা নয় — function-level check আর forced browsing
অনেক app-এ customer-এর পর্দায় “Refund” বোতাম দেখা যায় না, তাই দল ধরে নেয় কাজটি সুরক্ষিত। কিন্তু বোতাম শুধু একটি অনুরোধ পাঠানোর সুবিধা। আসল দরজা server-এর route. Browser-এর পাতা, JavaScript আর mobile app ব্যবহারকারীর যন্ত্রে থাকে, তাই সেখানে থাকা যেকোনো যাচাই ব্যবহারকারীর নিয়ন্ত্রণে। বাড়ির সদর দরজায় পর্দা টানলে দরজা তালাবদ্ধ হয় না।
UI-তে বোতাম লুকানো ভালো অভ্যাস, কারণ মানুষ অপ্রয়োজনীয় বিকল্প দেখেন না। তবে এটি ব্যবহারযোগ্যতার কাজ, নিরাপত্তার নয়। নিয়ম মনে রাখুন: UI বলে কী দেখাবে, server বলে কী হবে।
Missing function-level check মানে কাজটির route-এ পদের যাচাই নেই। Flask-ধাঁচের pseudocode-এ দেখুন:
অনিরাপদ: login-ই যথেষ্ট
@app.post("/admin/refunds")
@login_required
def refund():
# যেকোনো login করা মানুষ চালাতে পারেন
return issue_refund(request.json)নিরাপদ: কাজের অনুমতি আগে
@app.post("/admin/refunds")
@login_required
@require_permission("refund.create")
def refund():
return issue_refund(request.json)এই যাচাই handler-এর ভেতরে নয়, তার আগে বসানো ভালো। তখন handler-এর কোনো লাইন, কোনো database কল, কোনো ইমেইল শুরুই হয় না। Express-এ এই স্তরকে middleware বলে। নিচে দুটি অনুরোধ অনুসরণ করুন: প্রথমে customer রিনা, তারপর admin কামাল।
1function requireRole(role) {2 return (req, res, next) => {3 if (!req.user) return res.sendStatus(401);4 if (req.user.role !== role)5 return res.sendStatus(403);6 next();7 };8}9// handler-এর আগে role check10app.post('/admin/refunds',11 requireRole('admin'), issueRefund);
| user | |
| role | |
| handler | |
| status |
- রিনার অনুরোধ।
POST /admin/refundsroute মিলল। লাইন ১০-১১ অনুযায়ী আগেrequireRole('admin')চলবে,issueRefundএখনো চলেনি। - লাইন ৩: login আছে?
req.userআছে, তাই 401 নয়। পরের লাইনে যাওয়া হলো। - লাইন ৪: পদ মেলে?
customerআরadminআলাদা, তাই শর্ত সত্য। Status 403 ঠিক হলো। - লাইন ৫: এখানেই শেষ। 403 response গেল।
next()ডাকা হয়নি, তাই handler একটি লাইনও চলেনি, কোনো refund হয়নি। - কামালের অনুরোধ। একই route, এবার user
kamal, roleadmin. মান নতুন করে session থেকে এলো। - লাইন ৩ ও ৪ পার। Login আছে, আর
adminমিলেছে, তাই কোনো status পাঠানো হলো না। - লাইন ৬:
next(). এবার নিয়ন্ত্রণissueRefund-এ গেল, handler চলছে। - Handler শেষ। উত্তর 200. একই middleware একটি জায়গায় লেখা, দুই মানুষের জন্য দুই ফল। পদ বদলাতে handler ছুঁতে হলো না।
401 আর 403: 401 মানে পরিচয় পাওয়া যায়নি, আবার login করুন। 403 মানে পরিচয় জানা গেছে, কিন্তু অনুমতি নেই। এক পদ্ধতিতে check আছে মানেই অন্যটিতে আছে, তা নয়: GET সুরক্ষিত হলেও একই ঠিকানার PUT বা DELETE খোলা থাকতে পারে। প্রতিটি পথ আর প্রতিটি method আলাদা দরজা।
Forced browsing: ধারণাটি
Forced browsing মানে কোনো link অনুসরণ না করে সরাসরি একটি ঠিকানায় যাওয়া, যেমন admin পাতা, পুরোনো backup ফাইল বা ভুলে যাওয়া report. দলের ধারণা থাকে “এই পাতার link কোথাও নেই, তাই কেউ পাবে না”. এটি security through obscurity: ঠিকানা গোপন থাকাই একমাত্র বাধা। ঠিকানা কিন্তু ইতিহাস, log, JavaScript ফাইল আর পুরোনো ডকুমেন্টে ছড়িয়ে পড়ে। এর প্রতিকার গোপন ঠিকানা নয়:
- প্রতিটি পাতা আর ফাইলে একই অনুমতি যাচাই।
- Backup, metadata আর config ফাইল web root-এর বাইরে; directory listing বন্ধ।
- পুরোনো API version বন্ধ করা, আর সব route-এর একটি তালিকা রাখা।
- Production-এ debug আর test পথ না রাখা।
৫ · Mass assignment — যে ঘর form-এ ছিলই না
ধরুন profile form-এ দুটি ঘর: নাম আর শহর। কোডের সুবিধার জন্য অনেকে পুরো body সরাসরি user object-এ ঢেলে দেন। Object-এ কিন্তু role বা creditBalance-ও আছে। Form-এ না থাকলেও অনুরোধে ঘর যোগ করা যায়, কারণ অনুরোধের গঠনও ব্যবহারকারীর হাতে। এই ভুলের নাম mass assignment, আর ASP.NET জগতে একে over-posting বলে।
ব্যাংকের ঠিকানা-পরিবর্তনের ফর্মে আপনি হাতে একটি লাইন বাড়িয়ে লিখলেন, “আমার ব্যালেন্স ৫ লাখ টাকা করে দিন”. ভালো কেরানি শুধু ঠিকানার ঘরটিই পড়বেন। খারাপ কেরানি ফর্মে যা লেখা, সব খাতায় তুলে দেবেন। Mass assignment হলো সেই খারাপ কেরানি।
// কাল্পনিক ব্যবহারকারী আর অনুরোধ: in-memory ডেটা
const fresh = () => ({ name: 'Rina', city: 'Dhaka', role: 'customer', credit: 0 });
const body = { name: 'Rina Akter', city: 'Rajshahi', role: 'admin', credit: 500000 };
// অনিরাপদ: যা এলো, সব বসে গেল
function updateUnsafe(user, input) {
Object.assign(user, input);
}
// নিরাপদ: শুধু allowlist-এর ঘর; বাকিগুলো বাদ, আর log
const EDITABLE = new Set(['name', 'city']);
function updateSafe(user, input) {
const dropped = [];
for (const [key, value] of Object.entries(input)) {
if (EDITABLE.has(key)) user[key] = value;
else dropped.push(key);
}
if (dropped.length) console.log('LOG dropped fields:', dropped.join(', '));
}
const a = fresh();
updateUnsafe(a, body);
console.log('unsafe:', JSON.stringify(a));
const b = fresh();
updateSafe(b, body);
console.log('safe: ', JSON.stringify(b));Output:
unsafe: {"name":"Rina Akter","city":"Rajshahi","role":"admin","credit":500000}
LOG dropped fields: role, credit
safe: {"name":"Rina Akter","city":"Rajshahi","role":"customer","credit":0}
একই body, দুই ফল। অনিরাপদ রূপে role হলো admin আর credit হলো 500000, অর্থাৎ profile সম্পাদনা থেকে সরাসরি vertical ত্রুটি। নিরাপদ রূপে শুধু নাম আর শহর বদলেছে, আর বাদ পড়া ঘরগুলো log-এ গেছে। Allowlist পদ্ধতিতে পরে নতুন কোনো সংবেদনশীল ঘর যোগ হলেও সেটি নিজে থেকেই সুরক্ষিত থাকে। Denylist হলে সেটি যোগ করতে মনে রাখতে হতো।
বাস্তবে এর জন্য আলাদা input DTO বা schema রাখা হয়, যেখানে শুধু লেখার অনুমতি থাকা ঘর ঘোষণা করা থাকে। উল্টো দিকের ভুলও আছে: পুরো object পাঠিয়ে দিয়ে UI-তে কিছু ঘর না দেখানো। Password hash বা অভ্যন্তরীণ নোট তখন response-এ চলে যায়, শুধু পর্দায় দেখা যায় না। তাই লেখার allowlist-এর মতো পড়ার allowlist-ও লাগে। OWASP API3:2023 এই দুই দিককে একসাথে ধরে। ২০১২ সালে GitHub-এর একটি প্রকাশিত ঘটনা এই ভুলের দিকে পুরো Rails জগতের নজর ফেরায়; ভাবনার প্রশ্ন ২-এ এর শিক্ষা আছে।
৬ · Multi-tenant ডেটা ফাঁস — পাশের ফ্ল্যাটের চাবি
Multi-tenancy-র ভিত্তি আর নকশার বিকল্প পর্ব ১৯-এ দেখেছেন: প্রতিটি query-তে tenant শর্ত, আর tenant আসবে যাচাই করা session থেকে। এখানে দেখি বাস্তবে ফাঁসটি হয় কোন পথে। Cross-tenant ত্রুটি হলো IDOR-এর সবচেয়ে দামি রূপ, কারণ একটি ভুলে এক প্রতিষ্ঠানের ডেটা আরেক প্রতিষ্ঠানে যায়।
মূল পাতায় শর্তটি সাধারণত থাকে। ফাঁস হয় পাশের পথে। সবচেয়ে সাধারণ উদাহরণ cache:
অনিরাপদ key
// দুই tenant-এর একই key
const key = `report:${month}`;
const data = await cache.get(key);নিরাপদ key
// tenant আসে session থেকে
const t = req.session.tenantId;
const key = `t:${t}:report:${month}`;
const data = await cache.get(key);অনিরাপদ রূপে প্রথম প্রতিষ্ঠানের report cache-এ ঢোকে, আর একই মাসে দ্বিতীয় প্রতিষ্ঠান সেটিই পায়। Query নির্ভুল ছিল, ভুল হলো এক স্তর ওপরে। একই পরীক্ষা করুন search index, CSV export, background job, ইমেইল টেমপ্লেট, ফাইল storage-এর path আর WebSocket channel-এ।
হাতে লেখা শর্তের ওপর একটি নিরাপত্তা-জাল হলো row-level security, যেখানে database নিজেই ছাঁকে। PostgreSQL-এর একটি উদাহরণ, নিজের database-এ চেষ্টা করার জন্য:
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant_id')::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
-- প্রতিটি transaction-এর শুরুতে, session-এর tenant দিয়ে
SET LOCAL app.tenant_id = '<tenant-id-from-session>';এখানে দুটি ফাঁদ আছে। প্রথমত, table-এর মালিক সাধারণভাবে RLS এড়িয়ে যান, তাই FORCE দরকার; superuser আর BYPASSRLS role তবু এড়ায়, তাই app-কে সেই ক্ষমতা দেবেন না। দ্বিতীয়ত, connection pool-এ একটি connection বারবার ব্যবহার হয়। মান SET LOCAL দিয়ে transaction-এর ভেতরে না রাখলে আগের অনুরোধের tenant পরের অনুরোধে থেকে যেতে পারে। Database-স্তরের আরও অনুমতি দেখুন DBMS কোর্সের database নিরাপত্তা পর্বে।
দুই tenant-এর test বাধ্যতামূলক রাখুন: Test ডেটায় অন্তত দুটি প্রতিষ্ঠান রাখুন, আর ডেটা এমন রাখুন যাতে চেনা যায়। এক tenant-এ login করে প্রতিটি পাতা, export আর API চালিয়ে দেখুন অন্য tenant-এর চিহ্ন আসে কি না। এক tenant নিয়ে চলা test এই শ্রেণির কোনো ভুলই ধরতে পারে না।
৭ · Business logic ত্রুটি — দাম আর পরিমাণ server নিজে হিসাব করে
কিছু ত্রুটিতে অনুমতি ঠিক, প্রবেশও বৈধ, কিন্তু কাজের মান বা ক্রম ব্যবসার নিয়ম ভাঙে। রিনা নিজের cart-ই সাজাচ্ছেন, কিন্তু অনুরোধে দাম ১৮০০-র বদলে অন্য সংখ্যা, বা পরিমাণ ঋণাত্মক এলে কী হবে? এগুলো business logic flaw. কোনো চেনা আকৃতি নেই, কারণ প্রতিটি মান ব্যাকরণে বৈধ। “-1” একটি ঠিকঠাক সংখ্যা, শুধু দোকানের নিয়মে অর্থহীন।
দোকানে ক্রেতা নিজের হাতে মেমো লিখে আনলেন: “পাঞ্জাবি ২টি, দাম ১ টাকা”. দোকানদার মেমো দেখে টাকা নেন না, নিজের দামের তালিকা দেখে হিসাব করেন। ক্রেতার কথা শুধু “কোন জিনিস, কয়টি”; দাম আর যোগফল দোকানের।
নিয়মটি সরল: client জানাবে ইচ্ছা, server ঠিক করবে মূল্য। Hidden field-এর দাম, cart-এর যোগফল, ছাড়ের হার, ডেলিভারি চার্জ, কিছুই client থেকে নেবেন না। নিচের toy checkout দেখুন। টাকা রাখা হয়েছে পয়সায়, পূর্ণসংখ্যায়, কারণ দশমিক সংখ্যায় হিসাব গোলাকার ভুল আনে।
// দাম পয়সায়, server-এর নিজের তালিকায়। কোনো আসল পেমেন্ট নেই
const CATALOG = new Map([['panjabi', 180000], ['saree', 350000]]);
const COUPONS = new Map([['EID10', 10]]); // শতাংশ ছাড়
const MAX_QTY = 10;
function checkout(cart) {
let subtotal = 0;
for (const line of cart.items) {
if (!CATALOG.has(line.sku)) return 'REFUSED unknown item';
const { qty } = line;
if (!Number.isInteger(qty) || qty < 1 || qty > MAX_QTY) {
return `REFUSED bad quantity ${qty}`;
}
subtotal += CATALOG.get(line.sku) * qty; // line.unitPrice ব্যবহার হয়নি
}
const percent = COUPONS.get(cart.coupon) ?? 0;
const total = subtotal - Math.floor((subtotal * percent) / 100);
const flag = cart.clientTotal === total ? '' : ' (LOG price mismatch)';
return `OK charge ${total / 100} taka${flag}`;
}
const carts = [
{ items: [{ sku: 'panjabi', qty: 2 }], clientTotal: 360000 },
{ items: [{ sku: 'panjabi', qty: 2, unitPrice: 100 }], clientTotal: 200 },
{ items: [{ sku: 'saree', qty: 1 }], coupon: 'EID10', clientTotal: 315000 },
{ items: [{ sku: 'saree', qty: 1 }], coupon: 'FAKE99', clientTotal: 35000 },
{ items: [{ sku: 'saree', qty: -1 }], clientTotal: -350000 },
{ items: [{ sku: 'panjabi', qty: 1.5 }], clientTotal: 270000 },
];
carts.forEach((c, i) => console.log(`cart ${i + 1}: ${checkout(c)}`));Output:
cart 1: OK charge 3600 taka
cart 2: OK charge 3600 taka (LOG price mismatch)
cart 3: OK charge 3150 taka
cart 4: OK charge 3500 taka (LOG price mismatch)
cart 5: REFUSED bad quantity -1
cart 6: REFUSED bad quantity 1.5
Cart ২-এ client কম দাম জানিয়েছে, তবু কাটা হবে ৩৬০০ টাকা, কারণ দাম তালিকা থেকে এসেছে। অমিলটি log-এ গেছে: এটি হয় পুরোনো পাতার দাম, নয়তো কারসাজির চেষ্টা। Cart ৩-এ ছাড় ৩৫০০০০-র ১০% = ৩৫০০০ পয়সা, তাই ৩১৫০ টাকা। Cart ৪-এর অচেনা coupon-এ কোনো ছাড় নেই। Cart ৫ আর ৬ প্রত্যাখ্যাত, কারণ ঋণাত্মক বা ভগ্নাংশ পরিমাণ নিয়মে নেই। ঋণাত্মক পরিমাণ না আটকালে যোগফল কমে যেত, অর্থাৎ পরিমাণের যাচাই আসলে দামের যাচাই।
একই পরিবারের আরও কিছু ত্রুটি:
- ধাপ এড়ানো: Payment ধাপ পার না করেই “অর্ডার সম্পন্ন” route-এ যাওয়া। Server-এ অর্ডারের অবস্থা (state) রাখুন, আর শুধু বৈধ পরিবর্তন মানুন।
- পেমেন্ট মিলিয়ে না দেখা: Gateway-র “success” বার্তায় টাকার পরিমাণ, মুদ্রা আর অর্ডার নম্বর মেলানো, আর বার্তাটি সত্যিই gateway-র কি না যাচাই করা।
- একই coupon বারবার: দুটি অনুরোধ একসাথে এলে race condition হতে পারে। Database-এর unique constraint বা atomic update দিয়ে একবার-ব্যবহার নিশ্চিত করুন।
- সীমার যোগফল: প্রতিদিনের transfer সীমা প্রতিটি অনুরোধে আলাদা না দেখে দিনের মোট যোগফলে দেখুন।
৮ · প্রতিরক্ষার নকশা — deny by default আর এক জায়গায় enforcement
প্রতিটি route-এ হাতে যাচাই লেখার ভরসায় থাকলে একদিন একটি বাদ পড়বেই। তাই ভুলকে নিরাপদ দিকে ঠেলে দিতে হবে। লক্ষ্য হলো: যাচাই বাদ পড়লে ফল হবে “প্রবেশ নেই”, “সবার জন্য খোলা” নয়।
একটি অনুরোধের পথে প্রতিরক্ষার স্তর
Everything above the trust boundary is a claim; decisions happen below it
Deny by default আর central enforcement
Deny by default মানে প্রকাশ্য বলে ঘোষণা করা ছাড়া সবকিছু বন্ধ। Enforcement এক জায়গায় থাকলে নিয়ম এক ভাষায় লেখা যায়, পড়া যায়, পরীক্ষা করা যায়। এই বিন্দুকে পর্ব ১৯-এ PEP বলেছি। নিচের ধরনে route বসাতেই policy লাগে:
// policy ছাড়া route বসলে app চালুই হবে না
function route(method, path, { policy, handler }) {
if (typeof policy !== 'function') {
throw new Error(`no policy: ${method} ${path}`);
}
app[method](path, (req, res) => {
if (!policy(req.user, req)) return res.status(403).end();
return handler(req, res);
});
}
// "প্রকাশ্য" হওয়াও একটি স্পষ্ট সিদ্ধান্ত
route('get', '/health', { policy: policies.isPublic, handler: health });
route('post', '/refunds', {
policy: policies.can('refund.create'),
handler: refund,
});এখানে ভুলে policy না দিলে ত্রুটি ধরা পড়ে app চালুর সময়, production-এ নয়। Framework-এ একই কাজ করে global authentication filter বা declarative annotation. যেটিই বেছে নিন, লক্ষ্য একই: খোলা রাখতে হলে সেটি লিখে জানাতে হবে।
মালিকানা query-র ভেতরে, পরে নয়
আগে সারি এনে পরে if দিয়ে মেলানো কাজ করে, কিন্তু query-তে শর্ত বসানো তিন কারণে ভালো:
- ভোলা কঠিন:
current_user.ordersধরনের স্কোপ থেকে শুরু করলে অন্যের সারি হাতেই আসে না। Rails-এর association আর Django-রget_object_or_404(Order, pk=id, owner=request.user)এই ধারার উদাহরণ। - তালিকায় ফাঁস নেই: পাতায় ২০টি সারি এনে পরে ছাঁকলে কোনো পাতা ফাঁকা বা ছোট আসে, আর মোট সংখ্যা অন্যের ডেটার আভাস দেয়।
- লেখার কাজ atomic: যাচাই আর পরিবর্তন এক statement-এ হয়। মাঝখানে অবস্থা বদলের ফাঁক (TOCTOU) থাকে না।
দুর্বল: যাচাই আর কাজ আলাদা
order = db.get_order(order_id)
if order.owner_id != current_user.id:
abort(403)
if order.status == "open":
db.set_status(order_id, "cancelled")ভালো: এক statement-এ
rows = db.execute(
"UPDATE orders SET status = 'cancelled' "
"WHERE id = ? AND owner_id = ? "
"AND status = 'open'",
(order_id, current_user.id)).rowcount
if rows == 0:
abort(404)ব্যতিক্রমও আছে। জটিল নিয়মে, বা যেখানে policy engine বস্তুর গুণ দেখে সিদ্ধান্ত নেয়, আগে সারি পড়তেই হয়। তখনও সিদ্ধান্ত হবে ফল পাঠানোর আগে, আর কোনো লেখা হবে সিদ্ধান্তের পরে।
অনুমান-অযোগ্য ID: বাড়তি স্তর, প্রতিকার নয়
১০৪২-এর বদলে UUID ব্যবহার করলে পরের ID আন্দাজ করা কঠিন হয়, তাই ঠিকানা ধরে একের পর এক খোঁজা অকেজো হয়। এটি ভালো defense in depth, তবে অনুমতি নয়। ID গোপন কিছু নয়: তা URL, ইমেইল, log, browser history আর স্ক্রিনশটে ঘোরে। একবার কারও হাতে গেলে মালিকানা যাচাই ছাড়া আর কোনো বাধা নেই। লকারের নম্বর ছাপা না থাকলে সুবিধা আছে, তবু তালা তো লাগবেই।
আরও গভীরে: OWASP ASVS-এ authorization-এর আলাদা যাচাই-তালিকা আছে, আর OWASP-এর Authorization Cheat Sheet নকশার পরামর্শ দেয়। বড় ব্যবস্থায় নিয়ম আলাদা policy engine-এ যায় (policy as code), কিংবা Google-এর Zanzibar (USENIX ATC, ২০১৯) ধরনের সম্পর্কভিত্তিক সেবায়। Microservice-এ শুধু gateway-তে যাচাই যথেষ্ট নয়: ভেতরের service-কে নিজেও মালিকানা দেখতে হয়, নইলে confused deputy তৈরি হয়। CWE-862 (যাচাই নেই) আর CWE-863 (যাচাই ভুল) আলাদা ত্রুটি; প্রথমটি ছকে ধরা সহজ, দ্বিতীয়টির জন্য নিয়ম বোঝা লাগে।
৯ · পরীক্ষা, log আর alert — যাচাই যে সত্যিই আছে, তার প্রমাণ
নিয়ম লেখা আছে বলে সেটি কাজ করছে, এমন নয়। অনুমোদন-ছক হলো একটি টেবিল: সারিতে route, কলামে ব্যবহারকারীর ধরন, ঘরে প্রত্যাশিত status. এটি পর্ব ১৯-এর access control matrix-এর পরীক্ষাযোগ্য রূপ। ভালো ছকে “নেতিবাচক” ঘর বেশি থাকে: অন্য ব্যবহারকারী, অন্য tenant, login ছাড়া।
route anon rina mitu kamal GET /orders/1042 401 200 404 200 DELETE /orders/1042 401 403 403 200 GET /admin/users 401 403 403 200
route anon rina mitu kamal
GET /orders/1042 401 200 404 200 pass
DELETE /orders/1042 · · · ·
GET /admin/users · · · ·
route anon rina mitu kamal GET /orders/1042 401 200 404 200 DELETE /orders/1042 401 200 · · চাওয়া 403, পাওয়া 200: FAIL
route anon rina mitu kamal GET /orders/1042 401 200 404 200 DELETE /orders/1042 401 200 200 200 দুই customer-ই মুছতে পারছেন
GET /admin/users 401 403 403 200 pass
10/12 passed
CI: RED, merge blocked
DELETE /orders/1042 + requireRole('admin')
DELETE /orders/1042 401 403 403 200 pass
12/12 passed
CI: GREEN
- ছক লেখা হলো। দলের নিয়ম: অর্ডার দেখবেন মালিক আর admin, মুছবেন শুধু admin. চার ব্যবহারকারী আর তিন route মিলে ৪ × ৩ = ১২টি ঘর।
- প্রথম সারি। Test চারজন হয়ে
GET /orders/1042চালাল। সব মিলেছে, মিতুর ঘরে 404-ও। - দ্বিতীয় সারিতে অমিল। login ছাড়া 401 ঠিক আছে। কিন্তু রিনার ঘরে 403 চাওয়া ছিল, এলো 200. Route-টি শুধু login দেখছে।
- আরেকটি অমিল। মিতুর ঘরেও 200, যদিও অর্ডারটি তাঁর নয়। কামালের 200 ঠিক আছে। এই সারিতে মোট ২টি FAIL.
- CI লাল। তৃতীয় সারি পাস করল। মোট ১০/১২। Merge আটকে গেল, ত্রুটি production-এ পৌঁছায়নি।
- সংশোধন আর স্থায়ী পাহারা। Route-এ
requireRole('admin')বসল, ১২/১২ পাস। ছকটি থেকে গেল, তাই ভবিষ্যতে কেউ এই check তুলে দিলে test আবার লাল হবে।
এবার একই ছক কোডে চালান। Test ফাইলের নিচে একটি বাড়তি পরীক্ষাও আছে: app-এর এমন route, যার সারি ছকে নেই। এটি ছাড়া নতুন route চুপচাপ পরীক্ষার বাইরে থেকে যায়। এই ধরনের test প্রতিটি পরিবর্তনে Continuous Integration-এর ধাপে চলে।
// কাল্পনিক app: কোনো আসল server বা network নেই
const owner = { 1042: 'rina' };
const roles = { rina: 'customer', mitu: 'customer', kamal: 'admin' };
const login = (u, next) => (u ? next() : 401);
const app = {
'GET /orders/1042': (u) => login(u, () =>
(u === owner[1042] || roles[u] === 'admin' ? 200 : 404)),
// বাগ: login দেখা হয়েছে, admin পদ দেখা হয়নি
'DELETE /orders/1042': (u) => login(u, () => 200),
'GET /admin/users': (u) => login(u, () => (roles[u] === 'admin' ? 200 : 403)),
'GET /reports/export': (u) => login(u, () => 200), // ছকে সারি নেই
};
const actors = [null, 'rina', 'mitu', 'kamal'];
const matrix = {
'GET /orders/1042': [401, 200, 404, 200],
'DELETE /orders/1042': [401, 403, 403, 200],
'GET /admin/users': [401, 403, 403, 200],
};
let total = 0, failed = 0;
for (const [route, expected] of Object.entries(matrix)) {
actors.forEach((actor, i) => {
const got = app[route](actor);
total += 1;
if (got !== expected[i]) {
failed += 1;
console.log(`FAIL ${route} as ${actor}: want ${expected[i]}, got ${got}`);
}
});
}
const untested = Object.keys(app).filter((r) => !(r in matrix));
untested.forEach((r) => console.log(`UNTESTED ${r}`));
console.log(`${total - failed}/${total} passed, ${untested.length} untested`);
console.log(failed || untested.length ? 'CI: RED, merge blocked' : 'CI: GREEN');Output:
FAIL DELETE /orders/1042 as rina: want 403, got 200
FAIL DELETE /orders/1042 as mitu: want 403, got 200
UNTESTED GET /reports/export
10/12 passed, 1 untested
CI: RED, merge blocked
Test দুটি অনুপস্থিত check ধরেছে: রিনা আর মিতু দুজনই অর্ডার মুছতে পারছেন। ১০টি ঘর পাস করেছে। UNTESTED লাইনটি দ্বিতীয় ধরনের সুরক্ষা: এই route-এর আচরণ ঠিক না ভুল, তা এখনো কেউ বলেনি, তাই CI সেটিকেও আটকেছে। ছকটি কোডে থাকায় code review-তে নিয়ম পরিবর্তন স্পষ্ট দেখা যায়।
নিজের app-এ দুই ব্যবহারকারী হয়ে পরীক্ষা
স্বয়ংক্রিয় test-এর পাশাপাশি হাতের পরীক্ষাও কাজের, বিশেষ করে নতুন feature-এ। Staging পরিবেশে দুটি test account বানান, ক ও খ, প্রয়োজনে দুটি tenant-এ। ক-এর প্রতিটি কাজ করার সময় ঠিকানা আর অনুরোধগুলো লিখে রাখুন। তারপর খ হয়ে একই বস্তুর অনুরোধ দিন। প্রত্যাশা: খ কিছুই পাবেন না। এটি নিজের সিস্টেমে, নিজের নকল ডেটায়। অন্যের সিস্টেমে এমন পরীক্ষা শুধু লিখিত অনুমতিতে, যার নিয়ম পর্ব ৫০-এ আছে।
প্রত্যাখ্যান log করুন, alert দিন
একটি 403 সাধারণ ঘটনা, হয়তো পুরোনো bookmark. কিন্তু একই ব্যবহারকারীর কয়েক মিনিটে অনেকগুলো ভিন্ন বস্তুতে প্রত্যাখ্যান সাধারণ নয়। এটি খুঁজে দেখার মতো সংকেত, যেমন আপনার মহল্লায় কেউ রাত জেগে একের পর এক দরজার হাতল ঘোরালে সন্দেহ হয়। OWASP-ও প্রতিরোধের তালিকায় access control ব্যর্থতা log করা আর admin-কে জানানোর কথা বলে। এর জন্য log-এ ঘরগুলো নির্দিষ্ট থাকা চাই:
{
"time": "2026-10-31T14:02:11Z",
"event": "authz.denied",
"user_id": "u_2041",
"tenant_id": "t_17",
"action": "order.read",
"object_id": "1043",
"route": "GET /orders/:id",
"status": 404,
"source_ip": "203.0.113.24"
}একটি সূক্ষ্ম কথা: query-তে মালিকানা বসালে “অন্যের সারি” আর “সারি নেই” একই ফল দেয়। তাই একটি বস্তু-খোঁজা route-এর 404-ও এই log-এ লিখুন, নইলে যে সংকেত দরকার, সেটিই হারাবে। Sigma-র correlation ধরনে নিয়মটি এমন হতে পারে। সংখ্যাগুলো ধরে নেওয়া; নিজের baseline দেখে ঠিক করবেন।
title: App authorization denied
name: app_authz_denied
status: experimental
logsource:
category: application
product: example-shop
detection:
selection:
event: authz.denied
condition: selection
level: low
---
title: One user denied on many different objects
correlation:
type: value_count
rules:
- app_authz_denied
group-by:
- user_id
timespan: 10m
condition:
field: object_id
gte: 20
level: highAlert এলে কী করবেন: প্রথম প্রশ্ন “চেষ্টা হয়েছে?” নয়, “কোনো অনুরোধ সফল হয়েছে?” একই user-এর সফল অনুরোধে এমন বস্তু খুঁজুন, যা তাঁর নয়। সফল হলে এটি সম্ভাব্য ডেটা ফাঁস: account বা token বন্ধ করা, প্রমাণ রাখা, ত্রুটি সারানো, কার ডেটা গেছে তা হিসাব করা, আর আইন বা চুক্তি অনুযায়ী জানানোর প্রশ্ন আইনজীবীকে করা। Incident response-এর পুরো ধারা পর্ব ৬১-এ পাবেন। Rate limit-ও বসান: এটি প্রতিকার নয়, তবে ক্ষতির গতি কমায়।
পরীক্ষা ও ইন্টারভিউতে:
- Authentication আর authorization-এর পার্থক্য? প্রথমটি পরিচয় যাচাই, দ্বিতীয়টি সেই পরিচয়ের অনুমতি যাচাই।
- IDOR কেন হয়? ব্যবহারকারীর দেওয়া ID দিয়ে সরাসরি বস্তু আনা হয়, কিন্তু মালিকানা মেলানো হয় না।
- UUID কি IDOR সারায়? না; আন্দাজ কঠিন করে, কিন্তু ID ফাঁস হলে যাচাই ছাড়া বাধা থাকে না।
- Client-side check কি যথেষ্ট? না; client ব্যবহারকারীর নিয়ন্ত্রণে। যাচাই server-এ।
- Mass assignment কীভাবে ঠেকাবেন? লেখার ঘরের allowlist বা আলাদা input schema দিয়ে।
১০ · AI-এর যুগে অনুমতি যাচাই
AI খুব দ্রুত CRUD route লিখে দেয়, আর প্রশ্ন না করলে সাধারণত “কাজ করে” এমন সহজ রূপটি লেখে। সেই সহজ রূপে login থাকে, মালিকানা প্রায়ই থাকে না। কারণ বোধগম্য: আপনার ব্যবসার নিয়ম AI জানে না, আর কোডটি নমুনা ডেটায় ঠিকই চলে। তাই এই শ্রেণির ত্রুটি AI-যুগে কমবে, এমন ধরে নেওয়ার কারণ নেই।
| AI-এর সাধারণ ভুল | যেভাবে ধরবেন |
|---|---|
| Route-এ শুধু login, মালিকানা বা পদের শর্ত নেই | প্রতিটি query-তে owner ও tenant খুঁজুন; ছকের test চালান |
...req.body সরাসরি update-এ ঢেলে দেওয়া | Allowlist বা schema চান; অতিরিক্ত ঘরসহ test |
| পদ বা user id অনুরোধের body থেকে নেওয়া | পরিচয়ের উৎস শুধু যাচাই করা session |
| 403 সারাতে check তুলে দেওয়া বা CORS খুলে দেওয়া | “কেন প্রত্যাখ্যান হলো” আগে বুঝুন; diff পর্যালোচনা করুন |
| Test-এ শুধু সফল পথ | অন্য user, অন্য tenant, login ছাড়া, তিনটি নেতিবাচক ঘর চান |
| “OWASP অনুযায়ী” বলে ভুল ধারা বা CVE নম্বর দেওয়া | মূল উৎস খুলে মিলিয়ে নিন; AI প্রায়ই নম্বর বানিয়ে দেয় |
| “কোনো সমস্যা নেই” বলে আত্মবিশ্বাসী উত্তর | AI-এর উত্তরকে সূত্র ধরুন, রায় নয়; নিজের test আর মানুষের review লাগবে |
AI-কে আপনার নিজের কোড পর্যালোচনা করতে দিন, আর নিয়মটি বলে দিন। না বললে সে নিয়ম আন্দাজ করবে। পাঠানোর আগে key, token আর আসল গ্রাহকের ডেটা সরিয়ে নিন।
AI-এর লেখা route হাতে পেলে
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Access control কীভাবে OWASP তালিকার প্রথমে এলো, আর স্বয়ংক্রিয় tool কেন একে পুরোপুরি ধরতে পারে না?
OWASP Top 10-এর ইতিহাস দেখলে বোঝা যায়, সমস্যাটি নতুন নয়। ২০১০-এর দশকের শুরুতে এটি দুই ভাগে ছিল: ২০১৩ সংস্করণে “Insecure Direct Object References” (A4) আর “Missing Function Level Access Control” (A7) আলাদা ছিল। ২০১৭ সংস্করণে দুটি মিলে “Broken Access Control” নামে পঞ্চম স্থানে আসে। ২০২১-এ এটি প্রথম হয়, আর ২০২৫ সংস্করণেও প্রথম, যেখানে SSRF-এর CWE-ও (CWE-918) এই শ্রেণির সঙ্গে যুক্ত। নাম আর গণনা-পদ্ধতি বদলেছে, তাই বছরের তুলনা সাবধানে করতে হয়। তবে দিকটি স্পষ্ট।
Tool-এর সীমা গঠনগত। একটি scanner দেখতে পায় GET /orders/1043 থেকে 200 এসেছে। এটি সঠিক, না ভুল? উত্তর নির্ভর করে একটি নিয়মের ওপর, যা কোডে স্পষ্ট লেখা নেই: এই অর্ডার এই ব্যবহারকারীর কি না। Injection-এ বাইরে থেকে দেখা আচরণেই ত্রুটির চিহ্ন মেলে, এখানে মেলে না, কারণ ত্রুটিপূর্ণ উত্তরটি দেখতে একদম স্বাভাবিক। দুটি test account আর “কে কী পাবে” তালিকা দিলে dynamic tool কিছুটা পারে, আর static tool কিছু নকশা চিহ্নিত করতে পারে। তবু নিয়মটি মানুষকেই লিখতে হয়।
সেজন্য threat modeling, code review আর অনুমোদন-ছক এখানে সবচেয়ে কার্যকর। ছকটিই সেই “কে কী পাবে”-কে যন্ত্রের পড়ার মতো করে লেখে। অনুমোদিত penetration test-ও কাজের, কারণ একজন মানুষ ব্যবসার অর্থ বুঝে প্রশ্ন করেন। তবে সেটি বছরে কয়েকবার হয়, আর কোড বদলায় প্রতিদিন। তাই অনুমোদন-ছকের মতো পাহারা CI-তে রাখা দরকার।
মূল উপলব্ধি: যে ত্রুটি ব্যবসার অর্থের ওপর দাঁড়িয়ে, তা ধরতে অর্থটি লিখে দিতে হয়। নিয়ম লেখা হলে তবেই যন্ত্র পাহারা দিতে পারে।
প্র ০২২০১২ সালের GitHub ঘটনা mass assignment নিয়ে কী শেখায়? দায় কি developer-এর, না framework-এর?
২০১২ সালের মার্চে GitHub জানায়, public key হালনাগাদের form-এর একটি ত্রুটি ব্যবহার করে একজন ব্যবহারকারী Rails সংগঠনের সঙ্গে নিজের key যুক্ত করেন। GitHub-এর প্রকাশিত বিবৃতিতে মূল কারণ বলা হয়, আসা form parameter ঠিকভাবে যাচাই করা হয়নি। অর্থাৎ অনুরোধে যে ঘর আসছে, সেটি বসানোর অনুমতি আছে কি না, তা দেখা হয়নি। GitHub দ্রুত key সরায় আর ত্রুটি সারায়।
ঘটনাটির বিশেষত্ব হলো, এটি ঘটেছিল এমন একটি দলে, যারা Rails খুব ভালো জানত, আর ক্ষতিগ্রস্ত সংগঠনটি Rails নিজেই। Rails-এ তখন model-এ সুরক্ষিত ঘর ঘোষণার ব্যবস্থা ছিল, কিন্তু সেটি ব্যবহার করতে মনে রাখতে হতো। Rails সম্প্রদায় এরপর পথ বদলায়: strong parameters ধারণা এলো, আর Rails 4-এ এটি স্বাভাবিক হলো। এতে controller-এ স্পষ্টভাবে অনুমোদন না করলে parameter বসানোই যায় না।
দায়ের প্রশ্নের উত্তর তাই “দুটিই”, তবে শিক্ষাটি নকশার। যে সুরক্ষা প্রতিবার মনে রাখতে হয়, হাজার developer আর লাখো route-এ সেটি একদিন বাদ পড়বেই। Default বদলালে সবার জন্য ভুলটি বন্ধ হয়। এটি secure by default আর fail-safe defaults-এর একটি বাস্তব উদাহরণ।
আজকের ORM বা AI-লেখা কোডেও update(req.body) দেখলে একই প্রশ্ন করুন। Framework কি আপনাকে বাঁচাচ্ছে, না আপনি নিজেই সুরক্ষাটি পাশ কাটাচ্ছেন? সব framework-এ সুরক্ষা একভাবে চালু থাকে না, তাই নিজের version-এর ডকুমেন্ট দেখে নিন। Spread বা merge করার সুবিধাজনক কোড অনেক সময় সুরক্ষার স্তরটিকে এড়িয়ে যায়।
মূল উপলব্ধি: ভালো দলও মনে রাখার ওপর নির্ভর করা নিয়মে হোঁচট খায়। নিরাপদ পথটিকেই সবচেয়ে সহজ পথ বানান।
প্র ০৩UUID ব্যবহার করলে আন্দাজ প্রায় অসম্ভব। তাহলে মালিকানা যাচাই কেন তবু লাগবে?
হিসাবে UUID v4 সত্যিই শক্তিশালী। এর ১২২ bit এলোমেলো, অর্থাৎ 2122 ≈ ৫.৩ × ১০৩৬টি মান। ১০ কোটি ID থাকলেও একবারের আন্দাজে মেলার সম্ভাবনা প্রায় ১.৯ × ১০−২৯. তবে এখানে একটি শর্ত আছে: UUID v1 সময় আর যন্ত্রের তথ্য থেকে আসে, আর RFC 9562 (২০২৪)-এর UUID v7-এর শুরুতে সময় থাকে। তাই সব UUID সমান এলোমেলো নয়।
আসল সমস্যা আন্দাজ নয়, ফাঁস। ID গোপন রাখার জন্য তৈরি হয়নি: এটি URL-এ বসে, server log, browser history, ইমেইল আর সাপোর্ট টিকিটে যায়, অন্য সাইটে Referer হিসেবেও যেতে পারে। অনুমতি-বদলের প্রশ্নও আছে। কেউ চাকরি ছাড়লেন বা কোনো শেয়ার তুলে নেওয়া হলো, তবু তাঁর কাছে পুরোনো ID রয়ে গেল। যাচাই না থাকলে সেই ID চিরকাল কাজ করবে।
২০১৯ সালের First American Financial ঘটনার প্রকাশিত প্রতিবেদন শিক্ষণীয়। সংবাদ-প্রতিবেদনে প্রায় ৮৮.৫ কোটি রেকর্ডের কথা আসে, আর SEC-এর ২০২১ সালের আদেশে ৮০ কোটির বেশি ছবির কথা বলা হয়। প্রতিবেদন অনুযায়ী, একটি বৈধ link থাকলে login ছাড়াই ঠিকানার সংখ্যা বদলে অন্য নথি দেখা যেত। SEC-এর আদেশ আরেকটি শিক্ষাও দেয়: নিরাপত্তা দল কয়েক মাস আগে দুর্বলতাটি চিহ্নিত করলেও জ্যেষ্ঠ নির্বাহীরা তা জানতেন না। এমন ক্ষতি ঠেকায় প্রতিটি নথিতে অনুমতি যাচাই, শুধু ID-র আকৃতি বদল নয়। অনুমান-অযোগ্য ID থাকলে হয়তো গণহারে খোঁজা কঠিন হতো, কিন্তু ফাঁস হওয়া link তখনো খোলা থাকত।
একটি ব্যতিক্রম আছে: Share link ইচ্ছাকৃতভাবে capability URL, যেখানে link-ই অনুমতি। তখন সেটিকে secret-এর মতো সামলাতে হয়: যথেষ্ট entropy, মেয়াদ, প্রত্যাহার আর Referer নিয়ন্ত্রণ।
মূল উপলব্ধি: অনুমান-অযোগ্যতা আক্রমণের খরচ বাড়ায়, অনুমতির প্রশ্নের উত্তর দেয় না। ID বলে “কোনটি”, অনুমতি বলে “আপনি পারবেন কি না”.
প্র ০৪অন্যের বস্তু চাইলে 403 দেবেন, না 404? আর যাচাই কোথায় বসানো ভালো: gateway, handler, না database?
প্রথম প্রশ্নে কোনো একক সঠিক উত্তর নেই। 403 সৎভাবে বলে “আছে, কিন্তু আপনার জন্য নয়”, আর এতে বস্তুটির অস্তিত্ব ফাঁস হয়। কোনো গোপন প্রকল্পের নাম, বা কোনো email-এ account আছে কি না, এটুকু তথ্যই ক্ষতিকর হতে পারে (account enumeration). 404 অস্তিত্ব লুকায়। GitHub-এর API ডকুমেন্টেও বলা আছে, ব্যক্তিগত repository-র অস্তিত্ব ফাঁস এড়াতে কিছু জায়গায় 403-এর বদলে 404 দেওয়া হয়। আবার 404 ডিবাগ কঠিন করে: অনুমতি না থাকা গ্রাহক ভাবেন জিনিসটি নেই।
একটি কাজের নিয়ম হলো: ব্যক্তিগত বস্তুর অস্তিত্ব গোপন হলে 404, আর পদের কারণে নিষিদ্ধ কাজে 403, যেমন admin পাতা, যার অস্তিত্ব গোপন নয়। Tenant-এর ভেতরে সহকর্মীদের জানা বস্তুতে 403 মানায়। যা-ই বাছুন, log-এ আসল কারণ লিখুন।
দ্বিতীয় প্রশ্নের উত্তর স্তরে স্তরে। Gateway জানে route আর পরিচয়, তাই মোটা নিয়ম ভালো পারে: login আছে কি না, বা এই পথ কোন পদের। Application জানে ব্যবসার অর্থ, তাই মালিকানা আর অবস্থার নিয়ম সেখানে। Database স্তর, যেমন row-level security, শেষ জাল। শুধু gateway-তে ভরসা করলে ভেতরের একটি service সরাসরি ডাকলে সব পার হয়ে যায়, যা zero trust-এর চিন্তার উল্টো। শুধু database-এ রাখলে আবার ব্যবসার নিয়ম SQL-এ ছড়িয়ে পড়ে।
Saltzer ও Schroeder ১৯৭৫ সালেই complete mediation আর fail-safe defaults-এর কথা বলেন। আজকের ভাষায় এর অর্থ: সিদ্ধান্তের একটি স্পষ্ট বিন্দু, যা এড়ানো যায় না, আর সন্দেহে “না”.
মূল উপলব্ধি: যে স্তর যেটুকু জানে, সে সেটুকু যাচাই করবে। Status code-ও একটি তথ্য, তাই সেটিও নকশার অংশ।
প্র ০৫Business logic ত্রুটি WAF বা scanner কেন প্রায়ই ধরে না? ধরার উপায় তাহলে কী?
একটি WAF অনুরোধের আকৃতি দেখে: সন্দেহজনক অক্ষর, চেনা নকশা। কিন্তু "qty": -1 বা একটি অর্ধেক দাম দেখতে একদম সাধারণ JSON. কোনো ক্ষতিকর চিহ্ন নেই। অনুরোধটি ক্ষতিকর হয় শুধু আপনার দোকানের নিয়মের সাপেক্ষে, আর সেই নিয়ম WAF জানে না। একই কারণে সাধারণ scanner-ও চুপ থাকে।
OWASP এই দিকটিকে গুরুত্ব দিয়েছে। Top 10-এর ২০২১ সংস্করণে “Insecure Design” নামে নতুন একটি শ্রেণি আসে, আর API Top 10 (২০২৩)-এর API6 “Unrestricted Access to Sensitive Business Flows” নিয়ে। OWASP Web Security Testing Guide-এ business logic testing-এর আলাদা অংশও আছে। মূল বার্তা: এগুলো কোড-লেখার ভুল যতটা, নকশার ভুল তার চেয়ে বেশি।
ধরার উপায় তিন স্তরে। নকশায়: threat modeling-এর সময় “এই ধাপ কেউ এড়ালে, বারবার করলে, বা উল্টো মান দিলে কী হবে?” জিজ্ঞেস করুন। কাজের প্রবাহকে একটি state machine হিসেবে লিখুন, যেখানে বৈধ পরিবর্তন নির্দিষ্ট। কোডে: মূল নিয়ম domain model-এ রাখুন, যেমন পরিমাণ ১ থেকে ১০, যাতে প্রতিটি পথ একই নিয়ম মানে। OWASP-ও domain model-এ ব্যবসার সীমা প্রয়োগের কথা বলে। চলার সময়ে: ব্যবসার metric দেখুন: হঠাৎ অনেক refund, ছাড়ের হার অস্বাভাবিক, দাম-অমিলের log বাড়া, বিক্রির চেয়ে পেমেন্ট কম। আর হিসাব মেলানো (reconciliation) রাখুন: gateway-র টাকা আর অর্ডারের টাকা দিন শেষে মিলছে কি না। এখানে শুধু যন্ত্র নয়, মানুষের চোখও লাগে।
মূল উপলব্ধি: নিরাপত্তা শুধু কোডের আকৃতি নয়, ব্যবসার অর্থও। তাই business logic ধরার সেরা মানুষ প্রায়ই সেই ব্যক্তি, যিনি ব্যবসা বোঝেন আর হিসাব মেলান।
অনুশীলন
হিসাব, অনুমান, নকশা আর নিজের app-এ পরীক্ষা। অন্য কারও সিস্টেমে কিছু করবেন না।
-
হিসাব করুন: (ক) একটি app-এ ৩০টি route আর ৫ ধরনের ব্যবহারকারী (anonymous, মালিক, অন্য user, অন্য tenant, admin). অনুমোদন-ছকে কয়টি ঘর? প্রতিটি পরীক্ষায় ২০ ms লাগলে মোট সময় কত? (খ) ক্রমিক ID-র ১০ লাখ অর্ডার আছে, আর rate limit প্রতি account-এ সেকেন্ডে ৫টি অনুরোধ। ত্রুটিপূর্ণ route থাকলে সব ID ছুঁতে কত সময় লাগবে? Rate limit কি প্রতিকার?
(ক) ৩০ × ৫ = ১৫০টি ঘর। সময় ১৫০ × ০.০২ s = ৩ সেকেন্ড। অর্থাৎ CI-তে প্রায় কোনো খরচ নেই।
(খ) ১০,০০,০০০ ÷ ৫ = ২,০০,০০০ সেকেন্ড ≈ ৫৫.৬ ঘণ্টা, প্রায় ২.৩ দিন। Rate limit গতি কমায় আর alert দেওয়ার সময় দেয়, কিন্তু ত্রুটি রয়েই যায়। একটি ধীর ফাঁসও ফাঁস, আর কয়েকটি account মিলে সময় আরও কমতে পারে। প্রতিকার মালিকানা যাচাই।
-
Output অনুমান করুন: না চালিয়ে বলুন শেষে কী ছাপা হবে, আর কোথায় ভুল।
const orders = { 7: { owner: 'rina', status: 'open' } }; function cancel(user, id) { const o = orders[id]; o.status = 'cancelled'; if (o.owner !== user) return 403; return 200; } console.log(cancel('mitu', 7), orders[7].status);Output:
403 cancelledমিতু 403 পেলেন, তাই বাইরে থেকে মনে হবে যাচাই কাজ করেছে। কিন্তু অর্ডার ততক্ষণে বাতিল। যাচাই বসেছে পরিবর্তনের পরে। এমন ত্রুটি শুধু status দেখা test-এ ধরা পড়ে না; test-কে ডেটার অবস্থাও দেখতে হবে। সমাধান: যাচাই আগে, অথবা ভালো হয়
WHERE ownerশর্তসহ একটি atomic update. -
নকশা সমালোচনা করুন: একটি কাল্পনিক SaaS হিসাব-সফটওয়্যারে invoice ডাউনলোডের নকশা: (১) ঠিকানা
/api/v2/; (২) billing role না থাকলে বোতাম লুকানো; (৩) role রাখা আছে browser-এর localStorage-এ, আর server সেটি header-এ পড়ে; (৪) PDF cache হয়invoices/ <uuid>/ pdf? tenant=acme invoice:<uuid>key-তে; (৫) ছাড়ের হার form-এর hidden field থেকে আসে। অন্তত পাঁচটি সমস্যা আর প্রতিকার লিখুন।- Tenant URL থেকে: দাবি, প্রমাণ নয়। Tenant নিতে হবে যাচাই করা session থেকে।
- UUID-র ওপর ভরসা: লুকানো ID অনুমতি নয়। Query-তে
tenant_idআর অনুমতির শর্ত লাগবে। - লুকানো বোতাম: শুধু UI. Server-এ
invoice.downloadঅনুমতি যাচাই লাগবে। - Client-এ role: ব্যবহারকারী নিজেই বদলাতে পারেন। পদ আসবে server-এর session বা যাচাই করা token থেকে।
- Cache key: Tenant নেই, আর cache থেকে পড়ার সময় অনুমতি যাচাই হচ্ছে কি না স্পষ্ট নয়। যাচাই cache-এর আগে রাখুন, key-তে tenant যোগ করুন।
- Hidden field-এ ছাড়: Price tampering. ছাড় server-এর নিয়ম থেকে হিসাব হবে।
- বাড়তি:
v2থাকলেv1কি বন্ধ? প্রত্যাখ্যান log হচ্ছে কি? দুই tenant-এর test আছে কি?
-
নিজের app-এ দুই ব্যবহারকারী: আপনার নিজের একটি প্রজেক্ট বা স্থানীয় ডেমো নিন। দুটি test account বানান। তিনটি ব্যক্তিগত বস্তুর route বেছে একটি অনুমোদন-ছক লিখুন, তারপর দ্বিতীয় account দিয়ে প্রথমটির বস্তুগুলো চান। নিজের প্রজেক্ট না থাকলে উপরের
authz-matrix.test.js-এর বাগ সারিয়ে আর/reports/export-এর সারি যোগ করে CI সবুজ করুন।Toy কোডে তিনটি বদল: (১) DELETE route হবে
login(u, () => (roles[u] === 'admin' ? 200 : 403)); (২) export route-এও একই admin শর্ত, যদি নিয়মে তা-ই থাকে; (৩) ছকে যোগ হবে'GET /reports/export': [401, 403, 403, 200]. তখন ১৬/১৬ পাস, ০ untested, আরCI: GREEN.নিজের app-এ ভালো ফল হলো দ্বিতীয় account-এর সব চেষ্টায় 403 বা 404, আর log-এ সেগুলোর চিহ্ন। 200 এলে ত্রুটিটি লিখে রাখুন, query বা middleware সারান, আর সেই ঘরটি স্থায়ী test করুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি নিরাপদ অর্ডার-দেখার অনুরোধের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Session থেকে ব্যবহারকারী আর tenant জানা
- Route-এর policy দেখে পদের অনুমতি যাচাই
- Owner আর tenant শর্তসহ query চালানো
- সারি না মিললে 404 ঠিক করা আর log লেখা
- সারি মিললে অনুমোদিত ঘরগুলো পাঠানো
একটি site-এ login করা গ্রাহক রিনা ঠিকানার /orders/1042-এর সংখ্যা বদলালে মিতুর অর্ডার দেখতে পান। এটি কোন ধরনের সমস্যা?
- Vertical access সমস্যা, কারণ রিনা admin হয়ে গেছেন
- Horizontal সমস্যা, যার নাম IDOR
- Authentication-এর ত্রুটি, কারণ password দুর্বল
- SQL injection, কারণ URL-এ সংখ্যা আছে
একটি দল customer-এর পর্দা থেকে “Refund” বোতাম সরিয়ে দিল, কিন্তু POST /admin/refunds route-এ শুধু login যাচাই আছে। ঠিক কথাটি কোনটি?
- কাজটি সুরক্ষিত, কারণ বোতাম নেই
- Path-এ admin শব্দটি থাকায় যথেষ্ট
- Route-এ পদের যাচাই নেই, তাই এটি function-level ত্রুটি
- HTTPS থাকলে সমস্যা নেই
Profile হালনাগাদের কোড user.update(req.body). এর সবচেয়ে ভালো সংশোধন কোনটি?
- লেখার অনুমতি থাকা ঘরের allowlist বা input schema ব্যবহার
- body থেকে শুধু role ঘরটি মুছে দেওয়া
- Form-এ JavaScript দিয়ে role ঘর আটকানো
- User ID-কে UUID করা
মালিকানার শর্ত query-র ভেতরে রাখা, সারি আনার পরে if দিয়ে মেলানোর চেয়ে ভালো কেন?
- কারণ query সবসময় দশ গুণ দ্রুত হয়
- কারণ তখন login লাগে না
- কারণ এতে ID অনুমান করা অসম্ভব হয়
- কারণ অন্যের ডেটা আসেই না, তালিকা ঠিক থাকে, আর লেখার কাজ atomic হয়
একটি checkout অনুরোধে client জানাল unitPrice: 100, qty: 2. তালিকার দাম ১৮০০ টাকা। Server-এর ঠিক আচরণ কোনটি?
- ২০০ টাকা নেওয়া, কারণ client তা-ই বলেছে
- তালিকা থেকে দাম নিয়ে ৩৬০০ টাকা হিসাব করা আর অমিল log করা
- দামটি hidden field-এ রাখলেই সমস্যা মিটে যায়
- WAF এটি নিজে থেকে ধরবে, তাই app-এ কিছু লাগবে না
৪ ধরনের ব্যবহারকারী আর ১২টি route-এর অনুমোদন-ছকে মোট কয়টি ঘর, আর CI-তে এর মূল উপকার কী?
- ১৬টি; এটি শুধু গতি মাপে
- ৪৮টি; এটি দুর্বল password খুঁজে বের করে
- ৪৮টি; হারানো check merge-এর আগে ধরা পড়ে
- ৪৮টি; এরপর আর কোনো review লাগে না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- অনুরোধের সবকিছু দাবি: পরিচয় আর অনুমতি আসবে server-এর যাচাই করা উৎস থেকে, প্রতিটি অনুরোধে।
- দুই প্রশ্ন আলাদা: এই কাজটি কি এই পদের (vertical), আর এই বস্তুটি কি এই মানুষের (horizontal)?
- লুকানো মানে সুরক্ষিত নয়: বোতাম, hidden field, গোপন ঠিকানা বা UUID অনুমতির বিকল্প নয়।
- নিরাপদ পথকে স্বাভাবিক পথ বানান: Deny by default, এক জায়গায় policy, query-তে owner ও tenant, ঘরের allowlist, server-এ দামের হিসাব।
- প্রমাণ রাখুন: CI-তে অনুমোদন-ছক, দুই ব্যবহারকারীর পরীক্ষা, প্রত্যাখ্যানের log আর alert.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৯ · Injection প্রতিরক্ষাপরবর্তী পর্বParameterized query আর input-এর সীমা।
- পর্ব ১৯ · Authorization modelভিত্তিRBAC, ABAC, ReBAC আর least privilege-এর নকশা।
- সাইবার নিরাপত্তা · কোর্স হোম৮০টি পর্বপুরো কোর্সের রোডম্যাপ এখানে।
- DBMS: database নিরাপত্তাপাশের ট্র্যাকDatabase-স্তরের অনুমতি আর ডেটা সুরক্ষা।
- System Design: নিরাপত্তার নকশাগভীরে যানবড় ব্যবস্থায় authentication আর authorization কোথায় বসে।
- Software Engineering: code reviewপাশের ট্র্যাকঅনুপস্থিত check ধরার জন্য review-এর অভ্যাস।