এই পর্বে যা শিখবেন
- User enumeration ছাড়া signup আর password reset বানাতে, আর সময়ের ফাঁস মাপতে
- NIST-এর বর্তমান password নিয়ম আর ফাঁস-তালিকা মেলানোর পদ্ধতি প্রয়োগ করতে
- Email যাচাইয়ের token নিরাপদে তৈরি, জমা আর ব্যবহার করতে, local SMTP-তে আসল মেইল ধরে
- Email ও ফোন বদল, phone OTP, TOTP আর recovery code নিরাপদে চালাতে
- Redis দিয়ে login rate limit বসাতে, আর account মোছা ও audit trail নকশা করতে
১ · Signup: দরজা খোলা, তথ্য বন্ধ
পর্ব ৪৫ থেকে ৪৮-এ login আর permission দেখেছি। কিন্তু একটি দোকানের account শুধু login নয়। মানুষ নতুন account খোলেন, password ভোলেন, ফোন বদলান, চলেও যান। এই পর্বের বেশিরভাগ বাগ login-এর পাশের এই ছোট পথগুলোতে থাকে, কারণ এগুলো কম চলে আর কম পরীক্ষা হয়।
ব্যাংকের লকার শাখার কথা ভাবুন। লকার ঘরের দরজা খুব মজবুত। কিন্তু রিসেপশনে কেউ জিজ্ঞেস করলেন, “করিম সাহেবের কি এখানে লকার আছে?” রিসেপশনিস্ট “হ্যাঁ” বললেন। লকার ভাঙেনি, তবু একটি গোপন তথ্য বেরিয়ে গেল। হারানো চাবির ফর্মে পরিচয় না দেখে নতুন চাবি দিলে আরও খারাপ। Account-এর কাজ হলো এই রিসেপশন। দরজার মতোই এখানেও নিয়ম লাগে।
প্রথম নিয়মের নাম user enumeration ঠেকানো। পর্ব ২১-এ (বড় form) শব্দটি পেয়েছেন: উত্তর দেখে বোঝা যায় কোন email বা ফোন নম্বর এই সাইটে আছে। এই তালিকা দিয়ে প্রতারক বিশ্বাসযোগ্য phishing পাঠান, আর অন্য সাইটের ফাঁস হওয়া password মিলিয়ে দেখেন, যাকে বলে credential stuffing.
Signup-এ সবচেয়ে সাধারণ ভুল হলো এই উত্তর: 409 email already exists. এর সমাধান: email আগে থেকে থাকুক বা না থাকুক, উত্তর একই থাকবে, আর পরের খবরটি যাবে মেইলে। আসল মালিকই শুধু সেই মেইল পড়তে পারেন।
app.post('/api/signup', async (req, res) => {
const email = norm(req.body.email), pw = req.body.password;
const bad = await policy(pw); // অংশ ২
if (bad) return res.status(422).json({ error: bad });
const hash = await argon2.hash(pw, ARGON); // দুই পথেই একই কাজ
const r = await q(`INSERT INTO users(email,password_hash) VALUES($1,$2)
ON CONFLICT (email) DO NOTHING RETURNING id`, [email, hash]);
if (r.rowCount) {
const t = await createToken(r.rows[0].id, 'verify_email', 60 * 24);
enqueueMail(verifyMail(email, t));
} else {
enqueueMail(alreadyHaveAccountMail(email));
}
res.status(202).json(ok202);
});ON CONFLICT দুটি কাজ করে। আগে SELECT করে পরে INSERT করলে race থাকত, আর দুই পথে ডেটাবেসের কাজও আলাদা হতো। argon2.hash দুই পথেই চলে, কারণ এটি এই route-এর সবচেয়ে ধীর ধাপ। Password hash-এর তত্ত্ব আছে Password সংরক্ষণ পর্বে, আর আমরা OWASP-এর সেই ন্যূনতম মান নিয়েছি: Argon2id, m=19456, t=2, p=1.
সত্যিকারের run · Express 5.3.0, PostgreSQL 18.6, Node.js 24.15 · ~/bazarghar · সব ঠিকানা কল্পিত · কোনো মেইল বাইরে যায়নি · বাংলা বার্তা ছাঁটা
POST signup rahim (new) 202 {"message":"অনুরোধ পেয়েছি। …"}
POST signup rahim (again) 202 {"message":"অনুরোধ পেয়েছি। …"}
MAIL to=rahim@example.com | বাজারঘর: আপনার email যাচাই করুন
MAIL to=rahim@example.com | বাজারঘর: এই email-এ আগেই account আছে
দুটি উত্তর হুবহু এক। পার্থক্য শুধু মেইলের বাক্সে। এই পর্বের সব পথ একটি Express অ্যাপে চলেছে: ডেটা আসল PostgreSQL-এ, session আর limit আসল Redis 8.10.3-এ, আর মেইল ধরেছে নিজের কম্পিউটারের একটি নকল SMTP server. লেখার সময়ের অন্য version: argon2 0.45.1, redis client 6.3.1. পাতার কোড ল্যাবের কোড থেকে ছোট করা, তাই কিছু সাহায্যকারী function-এর নাম আলাদা।
একটি সৎ সীমা। সব পথে এই লুকানো সম্ভব হয় না। ফোন নম্বরে account হলে, অথবা username সবার সামনে দেখানো নিয়ম হলে, “এই নাম নেওয়া হয়েছে” না বলে উপায় নেই। সেখানে লক্ষ্য বদলায়: তালিকা বানানো ধীর আর দামি করা। Rate limit, আর পর্ব ২১-এর সেই সতর্কতা। কোথায় লুকাবেন আর কোথায় সুবিধা দেবেন, তা ব্যবসা আর ঝুঁকির সিদ্ধান্ত।
২ · Password-এর নিয়ম: NIST আসলে কী বলে
অনেক সাইট এখনো বলে “একটি বড় হাতের অক্ষর, একটি সংখ্যা, একটি চিহ্ন”, আর ৯০ দিন পরপর বদলাতে হবে। বর্তমান নির্দেশিকা NIST SP 800-63B-4 (২০২৫) এর উল্টো কথা বলে। নিচের কথাগুলো NIST-এর প্রকাশিত পাতা থেকে মিলিয়ে নেওয়া। এটি মার্কিন সরকারি সংস্থার জন্য লেখা, তবে সারা বিশ্ব একে মানদণ্ড ধরে।
| বিষয় | NIST SP 800-63B-4 | শক্তি |
|---|---|---|
| ন্যূনতম দৈর্ঘ্য | একা password হলে ১৫; শুধু MFA-র অংশ হলে ৮ | SHALL |
| সর্বোচ্চ দৈর্ঘ্য | অন্তত ৬৪ পর্যন্ত নিতে হবে | SHOULD |
| মিশ্র অক্ষরের নিয়ম | আর কোনো গঠনের নিয়ম চাপানো যাবে না | SHALL NOT |
| নিয়মিত বদল | সময় ধরে বদলাতে বলা যাবে না; ফাঁসের প্রমাণ পেলে বদলাতে হবে | SHALL NOT / SHALL |
| Blocklist | সাধারণ, অনুমেয় বা ফাঁস হওয়া password-এর তালিকা মেলাতে হবে, পুরো password ধরে | SHALL |
| অক্ষর গোনা | প্রতিটি Unicode code point একটি অক্ষর | SHALL |
| Password manager | autofill চলতে দিতে হবে; paste চলতে দেওয়া উচিত | SHALL / SHOULD |
| Hint, গোপন প্রশ্ন | চলবে না | SHALL NOT |
কেন দৈর্ঘ্য? মিশ্র অক্ষরের নিয়ম মানুষকে Password1! এর মতো অনুমেয় ছাঁচে ঠেলে দেয়। প্রতিটি নতুন অক্ষর সম্ভাবনাকে গুণ করে বাড়ায়, তাই লম্বা বাক্যাংশ মনে রাখা সহজ, ভাঙা কঠিন। বাংলা বাক্যাংশও ভালো, কিন্তু একটি ফাঁদ আছে: pw.length UTF-16 ইউনিট গোনে, code point নয়। আর একই লেখা ভিন্ন ফোনে ভিন্ন Unicode ক্রমে আসতে পারে, তাই NIST hash করার আগে NFC normalization করা উচিত বলে। আমাদের [...pw.normalize('NFC')].length এই কাজই করে।
ফাঁস-তালিকা, password না পাঠিয়েই
তালিকা মিলাতে গিয়ে পুরো password বাইরে পাঠালে সেটিই নতুন ফাঁস। সমাধানের নাম k-anonymity ভিত্তিক খোঁজ। Password-এর SHA-1 ছাপের প্রথম ৫টি hex অক্ষর পাঠানো হয়। সেবা ওই শুরুর সব ছাপের বাকি অংশ ফেরায়। মিলটি আপনার server নিজে করে, তাই সেবা জানতেই পারে না কোনটি খোঁজা হলো।
হিসাব: ৫টি hex মানে ১৬৫ = ১০,৪৮,৫৭৬টি ঝুড়ি। ধরে নিই তালিকায় ১০০ কোটি ছাপ আছে। তাহলে এক ঝুড়িতে গড়ে ১০০ কোটি ÷ ১০,৪৮,৫৭৬ ≈ ৯৫৪টি। সেবা ৯৫৪টির মধ্যে আপনারটি আলাদা করতে পারে না। এখানে SHA-1 গোপনীয়তার জন্য নয়, শুধু তালিকার সূচি হিসেবে। Password জমা রাখা হচ্ছে Argon2id-তেই। ল্যাবে আমরা কোনো বাইরের সেবায় যাইনি। Local একটি নকল তালিকা-সেবা চালিয়েছি, আর সে কী দেখল তা ছেপেছি।
সত্যিকারের run · নকল তালিকা-সেবা, শুধু local · সব password বানানো
signup pw short-pass-12 (13) 422 {"error":"too_short"}
signup pw password1234567 (15, list) 422 {"error":"breached"}
signup pw AAAAaaaa1111!!!! 202 {"message":"অনুরোধ পেয়েছি। …"}
signup pw (বাংলা বাক্যাংশ) 202 {"message":"অনুরোধ পেয়েছি। …"}
full sha1 of the sample: 648B54E01222585930982009E68112AC270DE1EE
range requests seen by the list service: /range/5CF9B /range/336F6 /range/648B5 /range/2A64F /range/76DC8
তৃতীয় লাইনটি দেখুন। AAAAaaaa1111!!!! দুর্বল, কিন্তু আমাদের ছোট তালিকায় নেই বলে পাস করেছে। এটি নিয়মের সীমা: তালিকা শুধু জানা জিনিস ধরে। NIST তাই প্রত্যাখ্যানের কারণ জানাতে আর ভালো password বাছাইয়ের পরামর্শ দিতেও বলে। এই 422 উত্তরে email-এর কোনো খবর নেই, কারণ policy চলে ডেটাবেস দেখার আগে।
২০০৩ সালে NIST-এর একটি পরিশিষ্টে Bill Burr মিশ্র অক্ষর আর নিয়মিত বদলের পরামর্শ লিখেছিলেন। ২০১৭ সালে একটি পত্রিকার সাক্ষাৎকারে তিনি এ নিয়ে অনুশোচনার কথা বলেন। সে বছরই নতুন SP 800-63B ওই পরামর্শ বাদ দেয়। ২০১৮ সালে Troy Hunt-এর Pwned Passwords সেবা, Cloudflare-এর Junade Ali-র সহযোগিতায়, k-anonymity দিয়ে এই খোঁজ জনপ্রিয় করে। ২০২৫ সালের চতুর্থ সংস্করণে আগের “উচিত” অনেক জায়গায় “বাধ্যতামূলক” হয়েছে।
৩ · Email যাচাই: token, hash আর একবার ব্যবহার
কেউ অন্যের email দিয়ে account খুললে মালিকের বাক্সে আপনার মেইল জমবে, আর পরে সেই মানুষ password reset চাইলে গোলমাল বাধবে। তাই যাচাই লাগে: “এই ঠিকানা আপনার তো?” এর জন্য একটি token মেইলে পাঠানো হয়।
ডাকঘরের রেজিস্ট্রি চিঠির কথা ভাবুন। চিঠিতে একটি গোপন নম্বর, আর ডাকঘরের খাতায় ওই নম্বরের একটি ছাপ। খাতা চুরি হলেও ছাপ থেকে নম্বর পাওয়া যায় না। প্রাপ্ত নম্বরটি একবার দেখালে কাজ শেষ, পরে আর চলে না। Token-এর চারটি নিয়ম এখানেই:
এলোমেলো
randomBytes(32), অর্থাৎ ২৫৬ bit, CSPRNG থেকে। Math.random(), সময় বা user id থেকে নয়।
Hash করে জমা
Database-এ শুধু SHA-256 ছাপ। টেবিল ফাঁস হলে হাতে ছাপ আসে, কাজের token নয়।
hashed at restএকবার
used_at বসিয়ে দেওয়া। দ্বিতীয়বার একই token চলবে না।
মেয়াদ
Email যাচাইয়ে এখানে ২৪ ঘণ্টা, reset-এ ৩০ মিনিট। NIST email-এ পাঠানো recovery code-এ সর্বোচ্চ ২৪ ঘণ্টা রাখে।
expiryPassword-এ Argon2id কিন্তু token-এ সাধারণ SHA-256 কেন? Password মানুষের বানানো, তাই অনুমান করা যায়, আর ধীর hash সেই অনুমানকে দামি করে। ২৫৬ bit এলোমেলো token অনুমানই করা যায় না, তাই ধীর হওয়ার দরকার নেই। দ্রুত hash এখানে ঠিক হাতিয়ার।
Nodemailer আর নিজের নকল SMTP
মেইল পাঠাতে Nodemailer ব্যবহার করব (লেখার সময়ে 10.3.1). আসল ঠিকানায় মেইল পাঠিয়ে শেখা ঠিক নয়। তাই smtp-server package (3.21.3) দিয়ে নিজের কম্পিউটারে একটি SMTP server চালানো হলো, যা মেইল নিয়ে শুধু জমায়। উন্নয়নের সময় Mailpit-এর মতো টুলও এই কাজ করে, তবে এই ল্যাবে সেটি চালানো হয়নি।
import { SMTPServer } from 'smtp-server';
import { simpleParser } from 'mailparser';
import nodemailer from 'nodemailer';
const box = [];
const smtp = new SMTPServer({
authOptional: true, disabledCommands: ['STARTTLS'],
onData(stream, session, cb) {
simpleParser(stream).then((m) => { box.push(m); cb(); }, cb);
},
});
smtp.listen(38566, '127.0.0.1');
const mailer = nodemailer.createTransport({
host: '127.0.0.1', port: 38566, secure: false, ignoreTLS: true, // শুধু local ল্যাবে
});
const info = await mailer.sendMail({ from, to, subject, text });সত্যিকারের run · Nodemailer 10.3.1 আর smtp-server 3.21.3 · মেইল এই কম্পিউটারের বাইরে যায়নি
{
accepted: [ 'rahim@example.com' ],
rejected: [],
response: '250 OK: message queued',
envelope: { from: 'no-reply@bazarghar.example', to: [ 'rahim@example.com' ] }
}
captured: "বাজারঘর" <no-reply@bazarghar.example> -> rahim@example.com
subject : বাজারঘর: আপনার email যাচাই করুন
text : যাচাই করতে: https://bazarghar.example/verify-email?token=SAMPLE
headers : from to subject message-id content-transfer-encoding date mime-version content-type
250 OK মানে শুধু “পরের server নিল”, মালিকের হাতে পৌঁছানো নয়। Production-এ ignoreTLS থাকবে না, TLS আর লগইন লাগবে। SPF, DKIM, DMARC আর transactional mail-এর বিস্তার পর্ব ৬৩-তে আসবে, আর তত্ত্ব আছে DNS ও email নিরাপত্তা পর্বে। এবার পুরো পথটি দেখুন। মানগুলো আসল run-এর, ছাঁটা।
- Signup
POSTক্রেতা - Database
hashশুধু ছাপ - SMTP
tokenআসল token - যাচাই পাতা
POSTক্লিকের পরে
POST /api/signup
{ "email": "rahim@example.com", "password": "…" }
উত্তর: 202 {"message":"অনুরোধ পেয়েছি। …"}
purpose verify_email
token_hash 1e624508a63e1307…
ttl 1 day
used_at null
to: rahim@example.com token: Wf-peFIc… (৪৩ অক্ষর) sha256(token) মিলল db-র সাথে: true
POST /api/login (ঠিক password)
403 {"error":"email_not_verified"}
POST /api/verify-email { token }
UPDATE … SET used_at = now() … RETURNING user_id
200 {"verified":true}
same token 400 {"error":"invalid_or_expired"}
wrong token 400 {"error":"invalid_or_expired"}
expired token 400 {"error":"invalid_or_expired"}
- Signup. রহিম form জমা দিলেন। উত্তরে
202, অর্থাৎ “অনুরোধ নিয়েছি”, “তৈরি করেছি” নয়। মেইল যাবে পেছনে। - শুধু ছাপ জমা। Server একটি এলোমেলো token বানাল, কিন্তু টেবিলে গেল তার SHA-256 ছাপ
1e624508…, মেয়াদ ১ দিন,used_atখালি। - আসল token শুধু মেইলে। ৪৩ অক্ষরের
Wf-peFIc…মেইলের লিংকে আছে। এটি ছাড়া আর কোথাও নেই। Run-এ ছাপ মিলিয়ে দেখা হয়েছে:true. - যাচাইয়ের আগে login. ঠিক password দিলেও
403 email_not_verified. এতে enumeration নেই, কারণ এই উত্তর পেতে ঠিক password জানতে হয়। - লিংকের পাতা, তারপর POST. লিংক একটি পাতা খোলে, আর সেই পাতার বোতাম token পাঠায়
POST-এ। সরাসরিGET-এ যাচাই করলে মেইলের নিরাপত্তা-স্ক্যানার লিংক খুলেই token খরচ করে ফেলতে পারে। সফল হলে200. - একবারই। একই token, ভুল token আর মেয়াদ শেষ, তিনটিতেই হুবহু
400 invalid_or_expired. কেন ব্যর্থ, তা বলার দরকার নেই।
নিচের মডেলে একটি array হলো টেবিল, আর ঘড়িটি নকল, তাই ৩০ মিনিট অপেক্ষা করতে হয় না। crypto আসল। শেষ দুটি লাইনে দেখুন ফাঁস হওয়া ছাপ token হিসেবে চলে কি না।
const { randomBytes, createHash } = require('node:crypto');
let now = 0; // নকল ঘড়ি (মিনিট)
const rows = []; // নকল account_tokens টেবিল
const hash = (t) => createHash('sha256').update(t).digest('hex');
function issue(userId) {
const token = randomBytes(32).toString('base64url');
rows.push({ userId, tokenHash: hash(token), expiresAt: now + 30, usedAt: null });
return token; // শুধু mail-এ যাবে
}
function consume(token) {
const h = hash(token);
const row = rows.find((r) => r.tokenHash === h);
if (!row) return 'invalid';
if (row.usedAt !== null) return 'invalid'; // তিনটি পথেই একই উত্তর
if (row.expiresAt <= now) return 'invalid';
row.usedAt = now;
return 'ok';
}
const a = issue(42);
console.log('token length :', a.length);
console.log('stored :', Object.keys(rows[0]).join(','));
console.log('raw in table :', JSON.stringify(rows).includes(a));
now = 5;
console.log('t=5 use :', consume(a));
now = 6;
console.log('t=6 reuse :', consume(a));
console.log('t=6 garbage :', consume('x'.repeat(43)));
const b = issue(42);
now = 40; // ৩৪ মিনিট পরে
console.log('t=40 expired :', consume(b));
const c = issue(42);
const leaked = rows[2].tokenHash; // ধরুন টেবিল ফাঁস হলো
console.log('t=40 use hash:', consume(leaked));
console.log('t=40 real one:', consume(c));raw in table : false: টেবিলে আসল token নেই। t=6 reuse : invalid: একবার ব্যবহারের পরে শেষ। t=40 expired : invalid: মেয়াদের ৩০ মিনিট পার। আর t=40 use hash: invalid লাইনটি মূল কথা: ছাপটি token হিসেবে দিলে server তার আবার hash করে, আর সেটি কোনো সারির সাথে মেলে না। তাই টেবিল ফাঁস হলেও token চালু হয় না, যেখানে t=40 real one: ok আসল token-এ চলে।
৪ · Password reset: একই উত্তর, একই সময়, একবার
Reset হলো password ছাড়াই account-এ ঢোকার বৈধ পথ। তাই এটি login-এর মতোই গুরুত্বপূর্ণ। প্রথমে উত্তর। জানা আর অজানা email-এ একই লেখা, একই status. নিচের আসল node:http server-এ ভুল আর ঠিক, দুটি পথই আছে।
const http = require('node:http');
const users = new Set(['rahim@example.com']);
let mailsSent = 0, jobs = 0;
const body = JSON.stringify({ message: 'If that address is right, a mail is on its way.' });
function naive(email, res) { // ভুল নকশা
if (!users.has(email)) { res.statusCode = 404; return res.end('{"error":"no such user"}'); }
mailsSent++; res.end(body);
}
function uniform(email, res) { // একই উত্তর, কাজ পরে
jobs++;
setImmediate(() => { if (users.has(email)) mailsSent++; });
res.statusCode = 202; res.end(body);
}
const server = http.createServer((req, res) => {
let raw = ''; req.on('data', (c) => (raw += c));
req.on('end', () => (req.url === '/naive' ? naive : uniform)(JSON.parse(raw).email, res));
});
server.listen(0, '127.0.0.1', async () => {
const url = `http://127.0.0.1:${server.address().port}`;
const ask = async (path, email) => {
const r = await fetch(url + path, { method: 'POST', body: JSON.stringify({ email }) });
return `${r.status} ${await r.text()}`;
};
const list = ['rahim@example.com', 'ghost@example.com'];
for (const path of ['/naive', '/uniform']) {
const [k, u] = [await ask(path, list[0]), await ask(path, list[1])];
console.log(path.padEnd(9), 'same answer:', k === u);
console.log(' known :', k.slice(0, 44));
console.log(' unknown:', u.slice(0, 44));
}
await new Promise((r) => setImmediate(r));
console.log('mails sent:', mailsSent, ' jobs queued:', jobs);
server.close();
});/naive same answer: false: 200 আর 404 সোজা বলে দিল কে আছে। /uniform same answer: true: দুটিই 202, একই লেখা। শেষ লাইন mails sent: 2 jobs queued: 2: job দুটি, মেইল গেছে naive-এর একটি আর uniform-এর একটি মিলিয়ে, অর্থাৎ uniform-এ অজানা ghost-এর জন্য job চলেছে, মেইল যায়নি। Runner-এর সময় ওঠানামা করে, তাই সময় এখানে মাপা হয়নি। আসল মাপ নিচে।
লেখা এক হলেও সময় কথা বলে
উত্তর এক হলেই শেষ নয়। জানা email-এ যদি মেইল পাঠিয়ে তবে উত্তর দেন, সময় বেশি লাগবে। এই পার্থক্য দিয়ে তথ্য বের করাকে বলে timing leak. আমরা একই Express অ্যাপে তিনটি নকশা মাপলাম, প্রতিটিতে ২০০ জোড়া অনুরোধ, পালা করে।
সত্যিকারের run · একটি local কম্পিউটারে একবার · network নেই · এটি সাধারণ ফল নয় · limiter বন্ধ রেখে মাপা
naive login known n=200 p50 21.81 ms p95 27.10 ms
naive login unknown n=200 p50 1.05 ms p95 1.59 ms
p50 gap 20.76 ms
fixed login known n=200 p50 21.48 ms p95 25.21 ms
fixed login unknown n=200 p50 21.36 ms p95 24.89 ms
p50 gap 0.12 ms
naive forgot known n=200 p50 107.10 ms p95 119.91 ms
naive forgot unknown n=200 p50 0.92 ms p95 1.43 ms
p50 gap 106.18 ms
mail later known n=200 p50 0.95 ms p95 1.46 ms
mail later unknown n=200 p50 0.53 ms p95 0.89 ms
p50 gap 0.42 ms
all later known n=200 p50 0.37 ms p95 0.63 ms
all later unknown n=200 p50 0.37 ms p95 0.72 ms
p50 gap 0.01 ms
তিনটি শিক্ষা। Login-এ অজানা email-এ password hash না করলে ২০ ms-এর ফাঁক হয়, যা জানা email-এর ঠিক argon2.verify-এর সময়। সমাধান: অজানা হলেও একটি নকল hash-এর সাথে verify চালানো। Reset-এ SMTP-র অপেক্ষাই ১০৬ ms বাড়িয়েছে। শুধু মেইল পরে পাঠালেও ০.৪২ ms রয়ে গেল, কারণ জানা email-এ দুটি বাড়তি ডেটাবেস লেখা আছে। ফাঁক ছোট, কিন্তু বহু নমুনায় গড় নিলে ছোট ফাঁকও ধরা পড়ে। সবচেয়ে পরিষ্কার সমাধান হলো উত্তরকে অস্তিত্বের ওপর নির্ভরই করতে না দেওয়া।
The response is sent before the code ever looks up the address
উত্তর আগে, খোঁজ পরে
app.post('/api/forgot', (req, res) => {
const email = norm(req.body.email);
later(() => forgotJob(email, req)); // উত্তরের পরে, durable queue-তে (পর্ব ৬২)
res.json(ok202);
});
async function forgotJob(email, req) {
const u = await findActiveUser(email);
const lim = await limiter.hit(`rl:forgot:${acctId(email)}`, 60 * M, 4); // মেইল-বোমা ঠেকায়
if (!u || lim.over) return;
const raw = newToken();
await cancelOldResetTokens(u.id);
await saveToken(u.id, 'reset', hashToken(raw), '30 min');
enqueueMail(resetMail(email, `${APP_URL}/reset-password?token=${raw}`));
}লিংকের ঠিকানা আসে config-এর APP_URL থেকে, অনুরোধের Host header থেকে নয়। Host থেকে বানালে কেউ ভুয়া header পাঠিয়ে আসল মালিকের মেইলে নিজের সাইটের লিংক ঢোকাতে পারতেন, যাকে বলে host header injection. আসল run-এ ভুয়া X-Forwarded-Host দিয়েও মেইলের লিংক ছিল https://bazarghar.example.
forgot known 200 {"message":"অনুরোধ পেয়েছি। …"}
forgot unknown 200 {"message":"অনুরোধ পেয়েছি। …"}
bodies identical: true status equal: true
UPDATE account_tokens SET used_at = now() -- পুরোনো reset
INSERT … purpose = 'reset', expires ৩০ মিনিট
MAIL to=rahim@example.com | বাজারঘর: password নতুন করুন
GET /reset-password?token=…
Referrer-Policy: no-referrer
Cache-Control: no-store
POST /api/reset { token, password }
BEGIN; UPDATE … used_at … RETURNING;
UPDATE users SET password_hash …; COMMIT;
উত্তর: 200 {"ok":true,"sessionsEnded":2}
reset, same token again 400 {"error":"invalid_or_expired"}
GET me, old phone session 401 {"error":"not_signed_in"} GET me, old laptop session 401 {"error":"not_signed_in"}
login with old password 401 {"error":"invalid_credentials"}
login with new password 200 {"ok":true}
MAIL to=rahim@example.com | বাজারঘর: আপনার password বদলেছে
- একই উত্তর। জানা আর অজানা email, দুটিতেই
200আর হুবহু একই লেখা। Run-এ দুটি তুলনাইtrue. - পেছনের কাজ। জানা email-এ আগের সব reset token বাতিল হলো, নতুন একটি তৈরি হলো, ৩০ মিনিট মেয়াদে, মেইল গেল। অজানা email-এ কিছুই হলো না।
- পাতা খোলা। Token URL-এ আছে, তাই পাতা থেকে বাইরের কোনো লিংকে গেলে
Referer-এ ফাঁস হতে পারে।Referrer-Policy: no-referrerসেটি বন্ধ করে, আরno-storecache-এ জমতে দেয় না। - নতুন password. Policy আবার যাচাই, তারপর token খরচ আর password বদল একই transaction-এ। উত্তরে
sessionsEnded: 2. - একবারই। একই token-এ
400. কারণused_atভরে গেছে। - সব session শেষ। Password চুরি হয়েছিল বলে reset হলে দখলকারীর session-ও মরতে হবে। ফোন আর ল্যাপটপ, দুটিই এখন
401. Redis-এusess:<id>set থেকে সব session key পাওয়া যায় বলে এটি সম্ভব। Session-এর গঠন পর্ব ৪৫-এ। - নতুন করে login. পুরোনো password
401, নতুন200. Reset-এর পর নিজে থেকে login করানো হয়নি, OWASP-ও স্বাভাবিক login-এ পাঠাতে বলে। শেষে মালিককে নোটিশ মেইল গেল।
“একবারই” কথাটি race-এও সত্য হতে হবে
অসাবধান কোড আগে SELECT করে দেখে token ঠিক আছে, তারপর ধীর password hash, শেষে used_at বসায়। মাঝের ২০ ms-এ বাকি অনুরোধও পাস করে যায়। এটি পর্ব ৪৩-এর (Application-এ transaction) সেই race condition. একই token-এ ১২টি অনুরোধ একসাথে পাঠিয়ে দেখলাম।
সত্যিকারের run · তিন দফা · local কম্পিউটারে একবার
check-then-act round 1 200: 6 400: 6
check-then-act round 2 200: 11 400: 1
check-then-act round 3 200: 12 400: 0
atomic UPDATE round 1 200: 1 400: 11
atomic UPDATE round 2 200: 1 400: 11
atomic UPDATE round 3 200: 1 400: 11
UPDATE account_tokens SET used_at = now()
WHERE token_hash = $1 AND purpose = $2
AND used_at IS NULL AND expires_at > now()
RETURNING user_id, new_value;যাচাই আর ব্যবহার একই statement-এ, তাই PostgreSQL-এর row lock একজনকেই জিততে দেয়। Hash-এর তুলনা ডেটাবেসের index-এ, আর purpose মেলানোয় email যাচাইয়ের token দিয়ে password reset করা যায় না। মেয়াদ ফুরানো সারি মুছতে একটি নির্ধারিত কাজ রাখুন।
৫ · Email, ফোন বদল আর বাংলাদেশে phone OTP
Account দখলের পর প্রথম কাজ হয় যোগাযোগের ঠিকানা বদলানো, যাতে আসল মালিক reset করতে না পারেন। তাই বদলের পথে চারটি নিয়ম:
- আবার পরিচয়: session থাকলেও বর্তমান password চাইতে হবে, যাকে বলে reauthentication. খোলা ল্যাপটপে কেউ বসলে এতে আটকাবে।
- নতুন ঠিকানায় প্রমাণ: নতুন ঠিকানা token দিয়ে নিশ্চিত হওয়ার আগে কিছু বদলাবে না।
- পুরোনো ঠিকানায় নোটিশ: মালিক দেখতে পাবেন, আর দাবি জানাতে পারবেন।
- বদলের পরে session শেষ: সব ডিভাইস আবার login করবে।
সত্যিকারের run · email বদল · শুধু local SMTP
change, wrong password 401 {"error":"reauth_failed"}
change, right password 202 {"pending":true}
MAIL to=rahim.new@example.com | বাজারঘর: নতুন email নিশ্চিত করুন
MAIL to=rahim@example.com | বাজারঘর: email বদলের অনুরোধ
email now: rahim@example.com <- এখনো পুরোনো
confirm 200 {"ok":true,"sessionsEnded":1}
email now: rahim.new@example.com
Phone OTP: বাস্তব প্রয়োজন, আর তার সীমা
বাংলাদেশে অনেক ক্রেতার কাছে email-এর চেয়ে ফোন নম্বরই প্রধান পরিচয়, তাই OTP প্রায়ই দরকার হয়। নিয়মগুলো আগে দেখুন। NIST ফোন নেটওয়ার্কের OTP-কে restricted বলে, মানে চলতে পারে, কিন্তু ঝুঁকি বিচার করতে হবে আর বিকল্প দিতে হবে। SIM swap এর একটি কারণ। SMS gateway-র দাম, sender ID আর BTRC-র নিয়ম সময়ে বদলাতে পারে, তাই নিজের চুক্তি আর বর্তমান নিয়ম দেখে নেবেন।
| সিদ্ধান্ত | এই ল্যাবে | কেন |
|---|---|---|
| দৈর্ঘ্য | ৬ অঙ্ক, crypto.randomInt | NIST-এর ন্যূনতম ৬ দশমিক অঙ্ক |
| মেয়াদ | ৫ মিনিট | NIST-এর ঊর্ধ্বসীমা ১০ মিনিট |
| ভুল চেষ্টা | ৫ বার | নিজের পছন্দ; নতুন কোড পাঠালেও গণনা শূন্য হবে না |
| আবার পাঠানো | ৬০ সেকেন্ড বিরতি, দিনে ৫ বার | খরচ আর SMS-বোমা ঠেকানো |
| জমা রাখা | HMAC(pepper, ফোন:কোড), Redis-এ TTL সহ | কোড জমা থাকে না |
শেষ সারিটিতে ফাঁদ আছে। ৬ অঙ্কের মাত্র ১০ লাখ মান। সাধারণ SHA-256 ছাপ ফাঁস হলে মুহূর্তেই সব মান চেষ্টা করে কোড বের করা যায়। তাই গোপন key-সহ HMAC, যে key Redis-এর বাইরে থাকে। আর লিংকের token-এর সাথে পার্থক্যটি মনে রাখুন: token-এ ২৫৬ bit, তাই সাধারণ hash যথেষ্ট; OTP-তে ২০ bit-এরও কম, তাই গোপন key আর চেষ্টার সীমা দুটোই লাগে।
সত্যিকারের run · দুটি run-এর লাইন একসাথে · নকল gateway, কোনো SMS যায়নি · নম্বর কল্পিত · কোড ঢেকে দেওয়া
start 202 {"sent":true,"ttl":300,"failsSoFar":0}
resend at once 429 {"error":"wait_before_resend"}
gateway got: 01700000000 বাজারঘর কোড: ******. ৫ মিনিট বৈধ। কাউকে বলবেন না।
redis hash: phone=01700000000 mac=7e5ccd2d2201d684… ttl 300
wrong 1 400 {"error":"invalid_or_expired","triesLeft":4}
right 200 {"ok":true}
… অন্য কোডে পরপর ভুল …
wrong 5 400 {"error":"invalid_or_expired","triesLeft":0}
wrong 6 429 {"error":"too_many_wrong_codes"}
after resend {"sent":true,"ttl":300,"failsSoFar":3} <- ভুলের গণনা থেকে গেল
wrong again {"error":"invalid_or_expired","triesLeft":1}
হিসাব: অনুমান আর খরচ
অনুমানের সম্ভাবনা। একটি কোডে ৫ বার চেষ্টা: ৫ ÷ ১০,০০,০০০ = ০.০০০৫%. দিনে ৫টি কোড ধরলে সর্বোচ্চ ২৫ চেষ্টা, অর্থাৎ ০.০০২৫%. সারা বছর একটি নম্বরে চালিয়ে গেলে ২৫ × ৩৬৫ = ৯,১২৫ চেষ্টা, সম্ভাবনা ১ − (১ − ১০−৬)৯১২৫ ≈ ০.৯১%. সীমা না থাকলে এই সংখ্যা ১০০%-এর দিকে যেত।
খরচ (অনুমান, কোনো gateway-র দাম নয়): ধরে নিই প্রতি SMS ৳০.৫০. প্রতিদিন ২০,০০০ signup, গড়ে ১.৩ বার পাঠানো: ২০,০০০ × ১.৩ × ০.৫০ = ৳১৩,০০০. এখন সীমা নেই এমন endpoint-এ একটি script সেকেন্ডে একটি অনুরোধ করে: ৮৬,৪০০ × ০.৫০ = ৳৪৩,২০০ দিনে, আসল ব্যবসার তিন গুণের বেশি। প্রতি IP-তে ঘণ্টায় ৫টি সীমা দিলে সেই IP থেকে ১২০ × ০.৫০ = ৳৬০. এই ধরনের অপব্যবহারের একটি বড় রূপের নাম SMS pumping. বাংলা SMS Unicode-এ যায়, তাই এক ভাগে ৭০ অক্ষর ধরে, ইংরেজির ১৬০ নয়। লম্বা বাংলা বার্তা দুই ভাগ হয়ে খরচ বাড়তে পারে। ফোনে নিজে কোড পূরণ করতে autocomplete="one-time-code" দিন।
৬ · TOTP আর recovery code
MFA-র তত্ত্ব আছে MFA ও passkey পর্বে, সেখানে দেখবেন কেন passkey সবচেয়ে phishing-প্রতিরোধী। এখানে আমরা সহজে চালু করা যায় এমন TOTP বানাব। Server আর ফোনের app একটি গোপন secret ভাগ করে। দুজনেই বর্তমান সময়কে ৩০ সেকেন্ডের ধাপে ভাগ করে, secret দিয়ে HMAC করে, আর ৬ অঙ্ক বের করে। কোনো network লাগে না। RFC 4226 (২০০৫) HOTP দেয়, আর RFC 6238 (২০১১) তার ওপর সময় বসায়।
const { createHmac } = require('node:crypto');
function hotp(secret, counter, digits, algo) {
const c = Buffer.alloc(8);
c.writeBigUInt64BE(BigInt(counter));
const mac = createHmac(algo, secret).update(c).digest();
const off = mac[mac.length - 1] & 0x0f; // dynamic truncation
const n = mac.readUInt32BE(off) & 0x7fffffff;
return String(n % 10 ** digits).padStart(digits, '0');
}
const totp = (secret, t, digits = 8, algo = 'sha1', step = 30) =>
hotp(secret, Math.floor(t / step), digits, algo);
// RFC 6238 Appendix B-এর test vector
const sha1 = Buffer.from('12345678901234567890');
const sha256 = Buffer.from('12345678901234567890123456789012');
const sha512 = Buffer.from('1234567890'.repeat(6) + '1234');
const v = [
[59, '94287082', '46119246', '90693936'],
[1111111109, '07081804', '68084774', '25091201'],
[1234567890, '89005924', '91819424', '93441116'],
[2000000000, '69279037', '90698825', '38618901'],
[20000000000, '65353130', '77737706', '47863826'],
];
for (const [t, a, b, c] of v) {
const got = [totp(sha1, t), totp(sha256, t, 8, 'sha256'), totp(sha512, t, 8, 'sha512')];
const ok = got[0] === a && got[1] === b && got[2] === c;
console.log(String(t).padEnd(12), got.join(' '), ok ? 'PASS' : 'FAIL');
}
console.log('six digits at t=59 :', totp(sha1, 59, 6));
console.log('t=60 is a new step :', totp(sha1, 60, 6));পাঁচটি সারিতেই PASS: মাত্র কয়েক লাইনের হাতে লেখা কোড RFC-র মানের সাথে মিলে গেল, তিনটি hash-এই। শেষ দুই লাইন 287082 আর 359152 দেখায় t=৫৯ আর t=৬০ আলাদা ধাপে পড়ে, তাই কোড সম্পূর্ণ বদলে যায়। শেখার জন্য হাতে লিখলেন। Production-এ যাচাই করা library ব্যবহার করুন। আমরা otplib 13.6.0 ব্যবহার করেছি, যার ৮ অঙ্কের ফল এই একই RFC মানের সাথে মিলেছে।
এবার আসল অ্যাপে। পাঁচটি জিনিস খেয়াল করুন: secret database-এ AES-256-GCM দিয়ে এনক্রিপ্ট করা, কারণ server-কে আসল secret দরকার, তাই hash চলে না; প্রথম সঠিক কোড না দেওয়া পর্যন্ত TOTP চালু হয় না; recovery code-এর শুধু ছাপ জমে; একই কোড দ্বিতীয়বার চলে না; আর সময়ের ছাড় শুধু আগের এক ধাপ, যা RFC 6238-এর সুপারিশ।
সত্যিকারের run · otplib 13.6.0 · secret কেটে দেখানো · সব account কল্পিত
secret: ZX27O2LK… length 32
uri: otpauth://totp/Bazarghar:rahim.new%40example.com?secret=<secret>&issuer=Bazarghar
in db: kbJOkmBJ7i/2Kjwc… enabled false contains plain secret: false
confirm wrong 400 {"error":"wrong_code"}
confirm right 200 codes 10 sample XXXX-XXXX-XXXX-XXXX
login no code 401 {"error":"totp_required"}
login same code (replay) 401 {"error":"invalid_credentials"}
waiting 12 s for the next 30 s step
login fresh code 200 {"ok":true}
same fresh code again 401 {"error":"invalid_credentials"}
recovery first use 200 ok true replacement issued true
recovery second use 401 {"error":"invalid_credentials"}
recovery_codes: { live: 10, total: 11 }
totp_last_step কলামে শেষ ব্যবহৃত ধাপ থাকে, আর নতুন login-এ UPDATE … WHERE totp_last_step < $2 শর্ত থাকে। NIST আর RFC 6238 দুটোই বলে একই OTP একবারের বেশি নেওয়া যাবে না। ফোন হারালে recovery code কাজে আসে। আমাদের প্রতিটি ৮০ bit, ১৬ অক্ষর, চার-চার ভাগে। NIST-এর শর্ত: অন্তত ৬৪ bit, hash করে জমা, ব্যবহারের পরে বাতিল, আর একটি নতুন code দেওয়া। Run-এর live: 10, total: 11 সেটিই দেখাচ্ছে। Code-গুলো একবারই দেখান, আর ব্যবহার হলে মেইলে জানান।
৭ · Login rate limit: IP, account আর lockout-এর ফাঁদ
পর্ব ২৬-এ (Middleware) rate limit-এর ধারণা, আর পর্ব ৪৪-এ (Cache আর Redis) Redis counter দেখেছেন। Login-এ একটি counter যথেষ্ট নয়, কারণ দুই রকম অপব্যবহার দুই দিক থেকে আসে:
- এক IP, বহু account: একটি সাধারণ password অনেক account-এ চেষ্টা, যাকে বলে password spraying. প্রতি account-এ চেষ্টা কম, তাই account-এর গণনায় ধরা পড়ে না।
- বহু IP, এক account: প্রতি IP-র গণনা কম, তাই IP limit ধরে না। এখানে account-এর গণনা লাগে।
Counter-টি sliding window-এ চলে। সাধারণ fixed window মিনিট বদলের মুহূর্তে শূন্য হয়, তাই সীমানার দুই পাশে দ্বিগুণ চেষ্টা ঢুকে যায়। নিচের মডেল সেটি দেখায়। ঘড়ি নকল, আর Redis-এর বদলে array.
// নকল ঘড়ি: সময় সেকেন্ডে। Redis-এর sorted set-এর বদলে একটি array
function slidingWindow(limit, windowSec) {
const hits = new Map();
return (key, now) => {
const list = (hits.get(key) ?? []).filter((t) => t > now - windowSec);
hits.set(key, list);
if (list.length >= limit) return { ok: false, retry: list[0] + windowSec - now };
list.push(now);
return { ok: true, used: list.length };
};
}
function fixedWindow(limit, windowSec) {
const counts = new Map();
return (key, now) => {
const k = `${key}:${Math.floor(now / windowSec)}`;
const n = (counts.get(k) ?? 0) + 1;
counts.set(k, n);
return { ok: n <= limit };
};
}
const times = [58, 58, 59, 59, 59, 60, 60, 60, 61, 61]; // মিনিটের সীমানায় ১০টি চেষ্টা
const sw = slidingWindow(5, 60), fw = fixedWindow(5, 60);
const count = (fn) => times.filter((t) => fn('acct:42', t).ok).length;
console.log('attempts in 4 seconds :', times.length);
console.log('fixed window allowed :', count(fw));
console.log('sliding window allowed:', count(sw));
const s2 = slidingWindow(3, 60);
for (const t of [0, 10, 20, 30, 59, 61, 70, 71, 82]) {
const r = s2('ip:203.0.113.9', t);
console.log(`t=${String(t).padStart(2)}`, r.ok ? `allowed (${r.used}/3)` : `blocked, wait ${r.retry}s`);
}fixed window allowed : 10: সীমা ৫, কিন্তু ৪ সেকেন্ডে ১০টি ঢুকল, কারণ t=৫৯ আর t=৬০ দুটি আলাদা মিনিট। sliding window allowed: 5: সীমা সত্যিই রইল। নিচের অংশে t=59 blocked, wait 1s: t=০-এর চেষ্টাটি ঠিক t=৬০-এ জানালা থেকে বেরোবে। তাই t=61-এ আবার allowed. প্রতিটি পুরোনো চেষ্টা বের হলে একটি করে জায়গা খোলে, যেমন t=71 blocked, wait 9s আর t=82 allowed.
আসল অ্যাপে একই যুক্তি Redis-এর sorted set-এ, সময়কে score ধরে। পুরো ধাপটি একটি Lua script, তাই দুটি server-এর মাঝে race হয় না, আর সময় আসে Redis-এর TIME থেকে, যাতে সব server একই ঘড়ি দেখে।
local key, window, limit = KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2])
local t = redis.call('TIME')
local now = t[1] * 1000 + math.floor(t[2] / 1000)
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local n = redis.call('ZCARD', key)
if ARGV[3] == 'hit' then
redis.call('ZADD', key, now, now .. ':' .. ARGV[4])
redis.call('PEXPIRE', key, window)
n = n + 1
end
return nএকটি সতর্কতা: req.ip তখনই ঠিক, যখন trust proxy আপনার নিজের proxy পর্যন্তই সেট করা। নইলে যে কেউ X-Forwarded-For বদলে প্রতি অনুরোধে নতুন IP সাজতে পারেন। বাংলাদেশে মোবাইল নেটওয়ার্কে বহু গ্রাহক একই সার্বজনীন IP-তে থাকতে পারেন, তাই খুব কড়া IP limit সৎ ক্রেতাকেও আটকাতে পারে।
Lockout নাকি backoff
পুরোনো সমাধান account lockout: ৫ বার ভুল হলে account ১৫ মিনিট বন্ধ। সমস্যা হলো, চাবি থাকে হামলাকারীর হাতে। যে কেউ আপনার email জানলে ইচ্ছামতো আপনাকে বাইরে রাখতে পারেন। এটি একটি denial of service. তাই account-এ নয়, account আর IP-র জোড়ায় অপেক্ষা বাড়ানো ভালো: ভুলকারী নিজেই ধীর হন, মালিক নন। অপেক্ষা ৩০ সেকেন্ড থেকে দ্বিগুণ হতে হতে সর্বোচ্চ ১ ঘণ্টা, যা NIST-এর দেওয়া উদাহরণের সীমার মধ্যেই।
ভুলকারী IP 203.0.113.9 (password জানেন না) মালিক IP 198.51.100.7 (ঠিক password জানেন) account rahim@example.com
401 401 401 401 401
lockout : 429(wait 900s) 429(wait 900s) 429(wait 900s) backoff : 429(wait 30s) 429(wait 30s) 429(wait 30s)
429 {"error":"too_many_attempts","why":"account_lock"}
Retry-After 900
200 {"ok":true}
401:invalid_credentials 50
403:step_up_required 5 প্রথমটি ৫১তম চেষ্টায়
মালিক: 403 {"error":"step_up_required"}
- দুজন। একজন
203.0.113.9থেকে রহিমের account-এ অনুমান করছেন। রহিম নিজে অন্য IP থেকে ঠিক password নিয়ে আসবেন। IP-গুলো উদাহরণের জন্য সংরক্ষিত ঠিকানা। - প্রথম ৫টি। সব
401. এখনো দুই নকশায় কোনো পার্থক্য নেই। - ৬ নম্বর থেকে। Lockout-এ পুরো account ৯০০ সেকেন্ড বন্ধ। Backoff-এ শুধু এই IP আর account-এর জোড়া ৩০ সেকেন্ড আটকে গেল। ভুলকারীর দেখা উত্তর প্রায় একই।
- Lockout-এ মালিক। ঠিক password দিয়েও
429, ১৫ মিনিট অপেক্ষা। ভুলকারী ৫টি অনুরোধেই মালিককে বাইরে রাখলেন। প্রতি ১৫ মিনিটে আবার করলে মালিক কখনোই ঢুকতে পারবেন না।whyfield শুধু এই ল্যাবের জন্য, আসল উত্তরে এটি থাকবে না। - Backoff-এ মালিক। তাঁর জোড়ায় কোনো ভুল নেই, তাই
200. সফল login-এ ওই জোড়ার গণনাও মুছে যায়, NIST-ও পুরোনো ব্যর্থতা বাদ দিতে বলে। - বহু IP. ৫৫টি IP-র প্রতিটি থেকে একবার। জোড়ার গণনা কাজে আসে না, তাই account-এর গণনা কাজ করে: ঘণ্টায় ৫০-এর পরে বন্ধ নয়, step-up. মালিকও এই উত্তর পান, তবে মেইলের লিংকের মতো বাড়তি ধাপ পেরিয়ে ঢুকতে পারবেন। এই ল্যাব উত্তর পর্যন্তই বানিয়েছে, ওই লিংকটি নয়।
সত্যিকারের run · Redis 8.10.3 · limiter চালু · সব email ও IP কল্পিত
--- one IP, many accounts (35)
status counts { '401': 30, '429': 5 } first 429 at try 31
--- unknown account behaves the same
rahim@example.com 401 401 401 401 401 429 429
ghost@example.com 401 401 401 401 401 429 429
--- redis keys after 3 failures
rl:acct:c9cf28d93acf3625 zset 3 pttl 3599997
rl:ip:203.0.113.60 zset 3 pttl 599996
rl:pair:c9cf28d93acf3625:203.0.113.60 string 3 pttl 3599995
মাঝের দুটি লাইন খুব জরুরি। অজানা ghost-ও ঠিক একই সময়ে 429 পায়। Limit যদি শুধু আসল account-এ বসত, তবে limiter নিজেই enumeration-এর পথ হতো। Key-তে email-এর বদলে keyed hash, তাই Redis-এ ঠিকানা জমে না। NIST-এর ঊর্ধ্বসীমা হলো পরপর ১০০ ব্যর্থতা, এর চেয়ে কম রাখতে বাধা নেই। Login-এর এই limit Redis বন্ধ হলে কী করবে, তা আগে ঠিক করুন। পর্ব ৪৪-এর fail open cache-এ চলে, কিন্তু login-এ পুরো খোলা ছেড়ে দিলে ঝুঁকি। প্রতি process-এর একটি ছোট in-memory সীমা মাঝামাঝি পথ, যা এই ল্যাবে চালানো হয়নি।
CAPTCHA: CAPTCHA একা নিরাপত্তা নয়, শুধু বাড়তি ঘর্ষণ। সবার ওপর না চাপিয়ে সন্দেহ হলে দেখানোই ভালো, যেমন step-up-এর সময়। দৃষ্টি বা শ্রবণ-প্রতিবন্ধী ব্যবহারকারীর সমস্যা, ধীর network আর তৃতীয় পক্ষের script-এর privacy, এই খরচগুলোও হিসাবে রাখুন। NIST বট শনাক্তকরণকে একটি ঐচ্ছিক কৌশল হিসেবে উল্লেখ করে।
৮ · Account মোছা, ডেটা রাখা আর audit trail
“আমার account মুছে দিন” শুনতে একটি DELETE মনে হয়। কিন্তু বাজারঘরে ওই ক্রেতার order আছে, হিসাবের দরকারে যা রাখতে হতে পারে। তাই মোছা দুই ধাপে। প্রথম ধাপে account বন্ধ, session শেষ, আর ফেরার একটি সময় (এই ল্যাবে ৩০ দিন, যা আমাদের নিজের ধরে নেওয়া মান)। দ্বিতীয় ধাপে একটি নির্ধারিত কাজ ব্যক্তিগত তথ্য মুছে দেয়, আর order একটি নামহীন সারির সাথে থাকে। পর্ব ৪১-এর (সম্পর্ক) soft delete এখানে শুধু প্রথম ধাপ। সেটিকেই মোছা বলে দাবি করা ঠিক নয়।
সত্যিকারের run · সময় বদলাতে deleted_at ৩১ দিন পিছিয়ে দেওয়া হয়েছে · সারি ছাঁটা
delete, wrong password 401 {"error":"reauth_failed"}
delete, right password 202 {"scheduled":true}
GET me after delete 401 {"error":"not_signed_in"}
login after delete 401 {"error":"invalid_credentials"}
purge, 30 day grace 200 {"purged":0}
purge, 31 days later 200 {"purged":1}
id | email | password_hash | phone | totp_enabled
1 | deleted+1@invalid | null | null | false
orders: id 1 | user_id 1 | total_paisa 125000
বাংলাদেশের আইন (আইনি পরামর্শ নয়): ABCL TECH-এর Privacy engineering পর্বের ৩১ অক্টোবর ২০২৬-এর যাচাই অনুযায়ী ব্যক্তিগত উপাত্ত সুরক্ষা আইন, ২০২৬ (৬৩ নং) গেজেটে আছে, এর কিছু ধারা পরে কার্যকর হবে, আর ধারা ১৪-তে মোছাকে backup আর test কপিতে ছড়ানোর কথা আছে। আইন বদলায়, আর এই পর্ব প্রতিটি শর্ত ব্যাখ্যা করে না। হিসাব বা করের নথি কত দিন রাখতে হবে, তা অন্য আইনেও ঠিক হতে পারে। আসল অ্যাপে মেয়াদ ঠিক করার আগে বর্তমান লেখা পড়ুন, আর আইনজীবী ও হিসাবরক্ষকের সাথে কথা বলুন। Backup-এর কথাও ভুলবেন না: সেখান থেকে এক সারি মোছা কঠিন, তাই backup-এর মেয়াদ বেঁধে রাখুন।
Audit trail আর ব্যবহারকারীকে জানানো
“কে, কখন, কোথা থেকে, কী করলেন”, এই হিসাবই audit trail. চায়ের দোকানের বাকির খাতার মতো: পাতা ছেঁড়া যায় না, শুধু নতুন লাইন বসে। Log-এর গঠন পর্ব ৩১-এ (Log আর config) দেখেছেন, কিন্তু audit আলাদা: ব্যবসা আর নিরাপত্তার প্রমাণ, তাই আলাদা টেবিল, শুধু যোগ করার অনুমতি। এতে token, password বা কোড কখনো নয়। আমরা user id-র বদলে keyed ছদ্মনাম রেখেছি, যাতে মুছে ফেলার পরেও ঘটনার হিসাব থাকে, কিন্তু সরাসরি মানুষ চেনা না যায়। ছদ্মনামও সাধারণত ব্যক্তিগত তথ্যের মধ্যেই ধরা হয়, তাই এরও মেয়াদ লাগে।
সত্যিকারের run · একই account-এর পুরো জীবন, ঘটনার সংখ্যাসহ · নোটিশের শেষ দুটি লাইন আলাদা run থেকে
signup 1
email_verified 1
login_ok 5
reset_requested 2
password_reset 1
login_failed 1
email_change_requested 1
email_changed 1
phone_changed 1
2fa_enabled 1
recovery_code_used 2
delete_requested 1
purged 1
secrets in audit_log: 0
--- sign-in notice
first time on Laptop/1: [ 'বাজারঘর: নতুন জায়গা থেকে login' ]
second time on Laptop/1: []
নোটিশ পাঠান password, email, ফোন বা MFA বদলে, recovery code ব্যবহারে, আর নতুন জায়গা থেকে login-এ। NIST recovery-র পরে নোটিশ বাধ্যতামূলক করেছে, আর নোটিশে দাবি জানানোর পরিষ্কার পথ দিতে বলে। নতুন ডিভাইস চিনতে এই ল্যাব browser আর IP-র একটি hash রেখেছে, যা সহজ কিন্তু মোবাইল IP বদলালে বাড়তি মেইল দেবে। মেইলে এমন লিংক দেবেন না যা ক্লিক করলেই কিছু ঘটে।
আরও গভীরে: OWASP-এর Forgot Password Cheat Sheet, Authentication Cheat Sheet আর Credential Stuffing Prevention Cheat Sheet এই পর্বের নকশার সাথে মিলিয়ে পড়ুন। NIST-এর ভাষায় reset আসলে “account recovery”, আর Sec. 4.2 recovery code, নোটিশ আর AAL অনুযায়ী শর্ত ঠিক করে। আরও শক্ত পথ হলো password একেবারে বাদ দেওয়া, যেখানে passkey (WebAuthn) ডোমেইনের সাথে বাঁধা থাকে। সময়ের ফাঁসের কোনো নিখুঁত সমাধান নেই, তাই লক্ষ্য হলো ফাঁককে noise-এর নিচে রাখা।
পরীক্ষা ও ইন্টারভিউতে:
- Reset token database-এ hash করা কেন? টেবিল ফাঁস হলেও ছাপ দিয়ে reset করা যায় না।
- Password reset-এর পরে কী করবেন? সব session আর বাকি token বাতিল, আর মালিককে নোটিশ।
- User enumeration কীভাবে ঠেকাবেন? একই লেখা, একই status, একই সময়, আর খবর মেইলে।
- Lockout-এর সমস্যা কী? অন্য কেউ ইচ্ছা করে মালিককে আটকাতে পারেন, যা denial of service.
- Sliding window কেন? Fixed window-র সীমানায় দ্বিগুণ চেষ্টা ঢুকতে পারে।
৯ · AI-এর যুগে account flow
“Forgot password বানিয়ে দাও” বললে AI কয়েক সেকেন্ডে এমন কোড দেয় যা চলে। এখানেই বিপদ: এই পথের ভুলগুলোতে কোনো error আসে না, আর সাধারণ ব্যবহারে সব ঠিক দেখায়।
| AI প্রায়ই যা লেখে | সমস্যা | কীভাবে ধরবেন |
|---|---|---|
404 user not found | User enumeration | দুই email-এ উত্তর মিলিয়ে দেখা |
await sendMail() উত্তরের আগে | সময়ের ফাঁস | ২০০ জোড়া অনুরোধ মাপা |
| Token সরাসরি টেবিলে | ফাঁসেই চালু token | টেবিলের সারি পড়া |
Math.random() দিয়ে token বা OTP | অনুমেয় মান | Diff-এ খোঁজা |
| JWT reset token, বাতিলের উপায় ছাড়া | একবার ব্যবহার নিশ্চিত নয় | একই লিংক দুবার চালানো |
req.headers.host দিয়ে লিংক | Host header injection | ভুয়া header-এ মেইল দেখা |
| Reset-এর পরে session অক্ষত | দখলকারী থেকে যান | পুরোনো cookie-তে অনুরোধ |
বানানো package বা পুরোনো otplib API | v13-এ authenticator নেই, verify object ফেরায় | নিজের version-এর docs আর আসল run |
নিচের প্রম্পটে নিয়মগুলো আগেই বলে দেওয়া আছে, আর শেষে test চাওয়া হয়েছে। আসল secret কখনো দেবেন না।
AI-এর লেখা account কোড হাতে পেলে
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১“বড় হাতের অক্ষর, সংখ্যা, চিহ্ন” নিয়ম দুই দশক চলল, অথচ এখন NIST তা নিষেধ করছে। মাঝখানে কী বদলাল?
বদলেছে প্রমাণ। ২০০৩ সালের পরামর্শ লেখা হয়েছিল তখনকার তাত্ত্বিক হিসাবের ওপর, ধরে নেওয়া হয়েছিল মানুষ অক্ষর এলোমেলোভাবে বেছে নেন। তখন বাস্তবের বড় password তালিকা গবেষকদের হাতে ছিল না। পরে একের পর এক ফাঁসে কোটি কোটি আসল password সামনে আসে, আর গবেষকরা দেখেন মানুষ নিয়ম মানেন অনুমেয় ছাঁচে: শুরুতে বড় হাতের অক্ষর, শেষে একটি সংখ্যা আর একটি !. ৯০ দিনের বদলে Summer2024! হয়ে যায় Autumn2024!. হামলাকারীর টুলগুলো এই ছাঁচ আগে থেকেই জানে।
নিয়মের খরচও ছিল। মানুষ লিখে রাখতেন, এক password সব জায়গায় চালাতেন, সাপোর্টে reset বাড়ত। অর্থাৎ নিরাপত্তা তেমন বাড়েনি, কিন্তু ব্যবহারের কষ্ট বেড়েছে। ২০১৭ সালে NIST এই পরামর্শ বাদ দেয়, আর লেখক Bill Burr নিজেই অনুশোচনার কথা বলেন। ২০২৫ সালের SP 800-63B-4 আরও এগিয়ে গঠনের নিয়ম আর সময় ধরে বদল নিষেধ করেছে, আর একক factor-এ ১৫ অক্ষর বাধ্যতামূলক করেছে।
এখন প্রশ্ন, আপনার প্রতিষ্ঠানের নিরাপত্তা-নীতি বা গ্রাহকের চুক্তি যদি পুরোনো নিয়ম চায়? তখন NIST-এর লেখা দেখিয়ে আলোচনা করুন, তবে চুক্তি ভাঙবেন না। আর শুধু নিয়ম বদলালেই শেষ নয়: ফাঁস-তালিকা মেলানো, ধীর hash, rate limit আর MFA মিলে এই নতুন নীতি কাজ করে। একটি বাদ দিলে অন্যটি দুর্বল হয়।
মূল উপলব্ধি: নিরাপত্তার নিয়মও একটি অনুমান, আর আসল মানুষের আচরণের প্রমাণ এলে তা বদলাতে হয়। যে নিয়ম মানুষকে খারাপ অভ্যাসে ঠেলে দেয়, তা কাগজে শক্ত হলেও বাস্তবে দুর্বল।
প্র ০২Token তো এলোমেলো আর ৩০ মিনিটেই মরে যায়। তাহলে database-এ hash করার ঝামেলা কেন? আর JWT দিয়ে reset করলে তো টেবিলই লাগত না।
Hash না করলে টেবিল পড়ার ক্ষমতা মানেই account দখলের ক্ষমতা। ধরুন একটি SQL injection, একটি ভুল করে ছেড়ে দেওয়া backup, অথবা শুধু পড়ার অধিকার পাওয়া একজন কর্মী। তিনি তখনই চালু reset token পড়ে যেকোনো account নিতে পারেন, password hash ভাঙার কষ্ট ছাড়াই। ৩০ মিনিট মেয়াদ এখানে কম সান্ত্বনা, কারণ হাতে সুযোগ থাকলে তিনি নিজেই reset চেয়ে টেবিল থেকে token পড়ে নিতে পারেন। Hash থাকলে এই আক্রমণ বন্ধ, কারণ ছাপ থেকে token ফেরানো যায় না। খরচ প্রায় শূন্য: একটি SHA-256.
JWT-র আকর্ষণ হলো কোনো সারি লাগে না। কিন্তু তখন “একবার ব্যবহার” কঠিন হয়ে যায়। Token নিজেই বৈধ, তাই ব্যবহার হয়েছে কি না জানতে কোথাও লিখে রাখতেই হবে, অর্থাৎ টেবিল ফিরে আসে। একটি পরিচিত কৌশল আছে: password hash-এর একটি অংশ দিয়ে key বানানো, যাতে password বদলালে token নিজে থেকে মরে যায়। এটি চলে, তবে নতুন reset চাইলে পুরোনোটি বাতিল হয় না, আর signing key ফাঁস হলে সব account-এর token বানানো যায়। Audit-এর হিসাবও থাকে না। টেবিলের নকশায় key ফাঁসের এই ভয় নেই।
এখানে প্রকৌশলীরা একমত নন, আর একটি stateless নকশা সঠিকভাবে করা সম্ভব। তবে শিক্ষানবিশের জন্য একটি opaque token আর একটি সারি প্রায় সব প্রশ্নের সোজা উত্তর দেয়: কে চেয়েছিল, কখন, ব্যবহার হলো কি না, বাতিল করা যায় কি না। পর্ব ৪৬-এর (JWT login) আলোচনাতেও বাতিল করার এই একই সমস্যা দেখেছেন।
মূল উপলব্ধি: নিজেকে জিজ্ঞেস করুন, “এই টেবিলটি একদিন ফাঁস হবে ধরে নিলে কী ঘটবে?” যে মান দিয়ে সরাসরি কিছু করা যায়, তা কখনো সাদা অবস্থায় রাখবেন না।
প্র ০৩User enumeration ঠেকাতে গিয়ে ব্যবহারকারীর অভিজ্ঞতা খারাপ হয়। “এই email আগেই আছে” না বললে মানুষ বিভ্রান্ত হন না?
হন, আর এটি একটি সত্যিকারের বিনিময়। সাধারণ দোকানে একজন ক্রেতা ভুলে আবার signup করলে পাতায় সরাসরি বলে দিলে তাঁর দুই মিনিট বাঁচে। আমাদের নকশায় তাঁকে মেইল খুলতে হয়। ধীর network-এ বা email কম ব্যবহার করেন এমন মানুষের কাছে এই ধাপটি বড়। তাহলে প্রশ্ন হলো, ফাঁসের দাম কত?
দাম নির্ভর করে সাইটের ধরনের ওপর। স্বাস্থ্য, ঋণ, ডেটিং বা রাজনৈতিক সংগঠনের সাইটে “এই মানুষ এখানে আছেন” তথ্যটিই স্পর্শকাতর। সেখানে লুকানো আবশ্যক। বাজারঘরের মতো সাধারণ দোকানে তথ্যটি নিজে তত গোপন নয়, কিন্তু এটি দিয়ে তালিকা বানিয়ে credential stuffing আর phishing চালানো যায়। অনেক বড় সাইট বিভিন্ন সময়ে বিভিন্ন সিদ্ধান্ত নিয়েছে, আর নিরাপত্তা গবেষকরাও এক কথা বলেন না।
একটি কার্যকর মাঝামাঝি পথ আছে। Reset আর login-এ সবসময় লুকান, কারণ সেখানে খরচ প্রায় শূন্য। Signup-এ মেইলের নকশা ব্যবহার করুন, অথবা সুবিধা বেশি দরকার হলে সরাসরি জানান কিন্তু কড়া rate limit আর bot শনাক্তকরণ বসান। ফোন নম্বরে account হলে OTP পাঠিয়ে পরে জানানো যায়। যা-ই বেছে নিন, লিখে রাখুন কেন নিয়েছেন। আর সব পথ একসাথে দেখুন: login-এ লুকিয়ে signup-এ বলে দিলে login-এর লুকানো অর্থহীন হয়ে যায়। হামলাকারী দুর্বলতম পথটিই ব্যবহার করবেন। তাই একটি পথের উত্তর বদলানোর আগে বাকি সব পথের উত্তরও পাশাপাশি রেখে মিলিয়ে দেখুন।
মূল উপলব্ধি: নিরাপত্তার সিদ্ধান্ত হ্যাঁ বা না নয়, দাম আর লাভের হিসাব। কিন্তু হিসাবটি পুরো সিস্টেম ধরে করতে হয়, একটি পাতা ধরে নয়।
প্র ০৪NIST যদি ফোনের OTP-কে restricted বলে, তবু বাংলাদেশের সাইটগুলোতে কেন এটি চলে? আপনার সাইটে কী করবেন?
ইতিহাসটি শিক্ষণীয়। ২০১৬ সালে NIST-এর খসড়ায় SMS-কে deprecated বলা হয়, আর শিল্পে আলোড়ন ওঠে। ২০১৭ সালের চূড়ান্ত লেখায় শব্দটি নরম হয়ে “restricted” হয়। ২০২৫ সালের সংস্করণেও PSTN পথটি restricted, শর্ত হলো ঝুঁকি বিচার, একটি বিকল্প authenticator, আর SIM বদল বা নম্বর সরানোর মতো সংকেত দেখা। অর্থাৎ নিষেধ নয়, দুর্বল বলে চিহ্নিত।
দুর্বলতার কারণ তিনটি। SMS ফোন নেটওয়ার্ক দিয়ে যায়, যা এই কাজের জন্য বানানো হয়নি। SIM swap বা সামাজিক প্রকৌশলে নম্বর অন্যের হাতে যেতে পারে। আর phishing পাতায় মানুষ নিজেই কোড লিখে দিতে পারেন, যা TOTP-তেও সম্ভব। তবে শুধু passkey আর নিজের ডোমেইনের সাথে বাঁধা পদ্ধতি এই শেষটি ঠেকায়।
তাহলে কেন চলে? কারণ এটি প্রায় সবার কাছে পৌঁছায়। একটি সাধারণ ফোনেও SMS আসে, কোনো app লাগে না, আর বাংলাদেশে অনেক মানুষের জন্য নম্বরই প্রধান পরিচয়। সবার হাতে smartphone আর অভ্যাস থাকবে, এই ধারণা বাস্তবসম্মত নয়। কিছুই না থাকার চেয়ে OTP অনেক ভালো। আবার খরচ, পৌঁছাতে দেরি আর অপব্যবহারের ঝুঁকিও এর সঙ্গে আসে।
বাজারঘরের জন্য একটি যুক্তিসংগত পরিকল্পনা: শুরুতে OTP ঠিকমতো (হিসাব, সীমা, HMAC), সাথে TOTP আর passkey ঐচ্ছিক। ঝুঁকিপূর্ণ কাজে, যেমন ফোন বদল বা টাকা তোলায়, মেইলে নোটিশ আর বাড়তি ধাপ। বার্তায় স্পষ্ট লিখুন কোডটি কাউকে বলা যাবে না। Bank বা payment-এর জন্য নিজের নিয়ন্ত্রকের নিয়ম আলাদা হতে পারে, সেগুলো দেখে নিন।
মূল উপলব্ধি: মানদণ্ড একটি দিক দেখায়, কিন্তু আপনার ব্যবহারকারী কে, তা-ও নকশার অংশ। দুর্বল পদ্ধতি বেছে নিতে হলে দুর্বলতা জেনে, শক্ত বিকল্পসহ বেছে নিন।
প্র ০৫ব্যবহারকারী “সব মুছে দিন” বললেন, আবার নিরাপত্তা বলে audit log অপরিবর্তনীয় রাখতে হবে। দুটি কীভাবে মেলাবেন?
দুটি দাবিই ন্যায্য, তাই একটি জিতবে না, নকশা দিয়ে মেলাতে হবে। প্রথম ধাপ হলো ডেটাকে উদ্দেশ্য ধরে ভাগ করা। Profile, ঠিকানা, ফোন: সেবা শেষ হলে এগুলোর উদ্দেশ্যও শেষ, তাই মুছতে হবে। Order আর টাকার হিসাব: হিসাব বা কর-সংক্রান্ত প্রয়োজনে রাখতে হতে পারে, তবে নাম আর ফোন ছাড়া রাখা সম্ভব কি না দেখুন। Audit: নিরাপত্তা আর বিবাদের প্রমাণ, যা একটি নির্দিষ্ট সময় রাখা যায়।
দ্বিতীয় ধাপ হলো শুরু থেকেই audit-এ ব্যক্তিগত তথ্য কম রাখা। আমাদের ল্যাবে user_ref একটি keyed ছদ্মনাম, email নয়। আরও শক্ত নকশায় প্রতি ব্যবহারকারীর আলাদা key থাকে, আর সেটি মুছে দিলে সংযোগ কেটে যায়, যাকে বলে crypto-shredding. এতে অপরিবর্তনীয় log না ছুঁয়েই মানুষটির সাথে তার সম্পর্ক ভাঙা যায়। তবে এটি নিখুঁত নয়: IP, সময় আর ঘটনার ধরন মিলিয়ে কখনো মানুষ চেনা যেতে পারে, আর ছদ্মনামকে আইনে সাধারণত ব্যক্তিগত তথ্যই ধরা হয়। ৬৩ নং আইনের ধারা ১৪-তে একদিকে মোছাকে backup-এ ছড়ানোর কথা, অন্যদিকে পরিবর্তনের হিসাব অপরিবর্তনীয় লেজারে রাখার কথাও আছে বলে আমাদের Privacy engineering পর্ব জানায়। এই জোড়া ঠিক এই প্রশ্নটিই তোলে।
তৃতীয় ধাপ হলো মেয়াদ। প্রতিটি শ্রেণির জন্য একটি লেখা নিয়ম, আর একটি কাজ যা সত্যিই মোছে। ভুলে যাওয়া সহজ জায়গাগুলো: backup, search index, analytics, মেইল-সেবার তালিকা, cache, আর log. ব্যবহারকারীকে সৎভাবে বলুন কী মোছা হলো আর কী কেন রইল। একটি ডেটা তালিকা ছাড়া এই কাজ সম্ভব নয়।
মূল উপলব্ধি: মোছা একটি বোতাম নয়, ডেটার পুরো জীবনচক্রের নকশা। শুরুতে কম ডেটা নিলে শেষে কম সমস্যা থাকে। এ ধরনের সিদ্ধান্তে আইনি পরামর্শ নিন।
অনুশীলন
প্রথমটি হিসাব, দ্বিতীয়টি নকশা-সমালোচনা, তৃতীয়টি ফল অনুমান, চতুর্থটি হাতে-কলমে।
-
Numerical: OTP ৪ অঙ্কের, প্রতি কোডে ৩ চেষ্টা, দিনে ৪টি কোড। (ক) একটি কোডে অনুমানের সম্ভাবনা কত? (খ) ৩০ দিনে মোট চেষ্টা কত, আর আনুমানিক সম্ভাবনা কত? (গ) ৬ অঙ্কে করলে (খ) কত হয়? (ঘ) প্রতিটি Argon2id hash-এ ২০ ms লাগলে ১টি CPU core সেকেন্ডে কতটি login সামলাবে?
(ক) ৩ ÷ ১০,০০০ = ০.০৩%. (খ) ৩ × ৪ × ৩০ = ৩৬০ চেষ্টা। সম্ভাবনা ১ − (১ − ১০−৪)৩৬০ ≈ ৩.৫৪%. (গ) ৩৬০ ÷ ১০,০০,০০০ = ০.০৩৬%. প্রায় ১০০ গুণ কম। (ঘ) ১০০০ ÷ ২০ = ৫০টি। আট core-এ আদর্শভাবে ৪০০টি। তাই login endpoint-এ limit শুধু অনুমান নয়, CPU-কেও বাঁচায়: সেকেন্ডে কয়েকশো নকল অনুরোধই server-কে ব্যস্ত রাখতে পারে।
-
নকশা-সমালোচনা: একজন সহকর্মী লিখেছেন:
const u = await User.findOne({email}); if (!u) return res.status(404).send('No user'); const t = Date.now().toString(36); u.resetToken = t; await u.save(); await mail(email, `${req.headers.origin}/reset?t=${t}`); res.send('sent');. কতগুলো সমস্যা পান? প্রতিটির সমাধান বলুন।404: enumeration. একই উত্তর দিন।Date.now(): অনুমেয়।randomBytes(32)নিন।- Token সাদা অবস্থায় জমা: SHA-256 ছাপ রাখুন।
- মেয়াদ নেই:
expires_atযোগ করুন। await mail: সময়ের ফাঁস। Queue-তে পাঠান।originheader থেকে লিংক:APP_URLconfig নিন।- সীমা নেই: মেইল-বোমা। প্রতি email-এ ও IP-তে limit বসান।
- একবার ব্যবহারের ব্যবস্থা নেই:
used_atআর এককUPDATE.
-
ফল অনুমান: Backoff নকশায় একটি IP আর account-এর জোড়ায় ৫টি ভুলের পর ৩০ সেকেন্ড, ৬টির পর ৬০, ৭টির পর ১২০ সেকেন্ড অপেক্ষা (
30 × 2^(n−5), সর্বোচ্চ ৩,৬০০)। একই IP থেকে ক্রম: t=০ থেকে t=৪-এ ৫টি ভুল; t=১০-এ ঠিক password; t=৪০-এ ভুল; t=৫০-এ ঠিক password. প্রতিটির ফল কী? t=৫০-এর পরের গণনা কত?t=০–৪: পাঁচটি
401, n=৫, শেষ ভুল t=৪-এ। t=১০: অপেক্ষা t=৩৪ পর্যন্ত, তাই ঠিক password হলেও429; password দেখাই হয় না। t=৪০: অপেক্ষা শেষ, ভুল,401, n=৬, এবার অপেক্ষা ৬০ সেকেন্ড, t=১০০ পর্যন্ত। t=৫০: আবার429. গণনা এখনো ৬. মালিক এই IP-তেই আটকে আছেন, অন্য IP-তে আটকাতেন না। শিক্ষা:Retry-Afterদেখিয়ে অপেক্ষা করতে বলুন, আর ভুলের পর ভুল করলে সময় বাড়ে। -
হাতে-কলমে: নিজের কম্পিউটারে
smtp-serverআর Nodemailer দিয়ে অংশ ৩-এর মতো মেইল ধরুন। তারপর একটি reset route বানিয়ে জানা ও অজানা email-এ ২০০ জোড়া অনুরোধের p50 মাপুন। আগেawait sendMailরেখে, পরে সরিয়ে। আপনার ফাঁক কত?আমাদের একটি run-এ
awaitসহ ফাঁক ১০৬.১৮ ms, আর সব কাজ উত্তরের পরে নিলে ০.০১ ms. আপনার সংখ্যা আলাদা হবে, কারণ CPU, SMTP আর load আলাদা। অনুরোধ পালা করে পাঠান, একই ধরনের সব পরপর নয়, নইলে ডেটাবেস cache একটি পক্ষকে সুবিধা দেবে। কোনো আসল ঠিকানায় মেইল পাঠাবেন না,hostঅবশ্যই127.0.0.1রাখুন, আর পরীক্ষা শুধু নিজের অ্যাপে করুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি নিরাপদ password reset-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- সবার জন্য একই উত্তর পাঠানো
- পেছনের কাজে email খোঁজা, পুরোনো token বাতিল
- নতুন token-এর ছাপ জমা, আসল token মেইলে
- একই statement-এ token যাচাই ও ব্যবহার, password বদল
- সব session শেষ করে মালিককে নোটিশ
NIST SP 800-63B-4 অনুযায়ী একক factor হিসেবে ব্যবহৃত password নিয়ে কোন কথাটি ঠিক?
- অন্তত ৮ অক্ষর, বড় হাতের অক্ষর ও চিহ্ন বাধ্যতামূলক
- অন্তত ১৫ অক্ষর, গঠনের নিয়ম নয়, blocklist মেলানো বাধ্যতামূলক
- ৯০ দিন পরপর বদলাতে হবে
- ভুলে গেলে সাহায্যের জন্য hint রাখতে হবে
৩২ byte CSPRNG-এর reset token database-এ রাখতে কোনটি সবচেয়ে উপযুক্ত?
- সাদা অবস্থায়, কারণ মেয়াদ কম
- Argon2id দিয়ে, যাতে যত ধীর হয় তত ভালো
- SHA-256 ছাপ, কারণ এলোমেলো মান অনুমান করা যায় না
- Base64 করে
একটি reset route-এ জানা email-এ p50 ১০৭ ms, অজানা email-এ ১ ms, যদিও উত্তরের লেখা এক। সবচেয়ে ভালো সমাধান কোনটি?
- কাজটি queue-তে দিয়ে আগে উত্তর ফেরানো, খোঁজ পরে
- অজানা email-এ এলোমেলো ঘুম যোগ করা
- Network-এর noise-এ এমনিতেই ঢেকে যাবে, কিছু লাগবে না
- অজানা email-এ 404 দেওয়া
একই reset token-এ ১২টি অনুরোধ একসাথে এলে ঠিক একটিই সফল হওয়া নিশ্চিত করে কোনটি?
- SELECT-এ দেখে নিয়ে পরে used_at বসানো
- Node.js single-threaded, তাই আলাদা কিছু লাগে না
- Frontend-এ বোতাম বন্ধ করা
- WHERE used_at IS NULL সহ একটি UPDATE … RETURNING
৬ অঙ্কের OTP Redis-এ রাখার সবচেয়ে ভালো উপায় কোনটি?
- সাদা অবস্থায়, TTL সহ
- গোপন key-সহ HMAC, TTL সহ
- সাধারণ SHA-256, salt ছাড়া
- Base64 করে, TTL ছাড়া
প্রতি account-এ ৫টি ভুলে পুরো account ১৫ মিনিট বন্ধ, এই নিয়মের প্রধান দুর্বলতা কী?
- Redis-এ বানানো যায় না
- অনুমান একটুও ধীর করে না
- অন্য কেউ ইচ্ছা করে আসল মালিককে বাইরে রাখতে পারেন
- NIST কোনো rate limit চায় না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Signup, login আর reset-এ লেখা, status আর সময়, তিনটিই একই রাখুন। খবর যাবে মেইলে।
- Password-এ দৈর্ঘ্য আর ফাঁস-তালিকা, মিশ্র অক্ষরের নিয়ম নয়, সময় ধরে বদল নয়।
- Token: এলোমেলো, hash করে জমা, একবার, মেয়াদসহ। যাচাই আর ব্যবহার একই statement-এ।
- Reset বা ঠিকানা বদলের পরে সব session শেষ, মালিককে নোটিশ। OTP-তে HMAC আর চেষ্টার সীমা, TOTP-তে replay বন্ধ।
- Lockout নয়, IP আর account-এর জোড়ায় backoff; মোছা দুই ধাপে, audit-এ কোনো secret নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫০ · অ্যাপের নিরাপত্তা তালিকাপরবর্তী পর্বCORS, CSRF, XSS, header আর dependency.
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের রোডম্যাপ।
- Password সংরক্ষণতত্ত্বSalt, Argon2id, pepper আর ফাঁসের ক্ষতির হিসাব।
- MFA আর passkeyগভীরে যানSMS, TOTP, push আর WebAuthn-এর তুলনা।
- Privacy engineeringপাশের ট্র্যাকডেটা কমানো, মুছে ফেলা আর বাংলাদেশের উপাত্ত আইন।
- NIST SP 800-63B-4মূল উৎসAuthentication আর authenticator ব্যবস্থাপনা।
- OWASP Forgot Password Cheat Sheetমূল উৎসReset-এর নিয়মের সংক্ষিপ্ত তালিকা।