এই পর্বে যা শিখবেন
- Trust boundary আর least privilege দিয়ে যেকোনো প্রোগ্রামের ঝুঁকির জায়গা চেনা
- Allow-list, ধরন, সীমা আর দৈর্ঘ্য দিয়ে input যাচাই করা
- SQL, OS command, কোড, path আর HTML — পাঁচ রকম injection-এর নিরাপদ সমাধান
- Secret কোডের বাইরে রাখা, আর পাসওয়ার্ড সংরক্ষণের সঠিক পাইপলাইন
- bandit, ruff, pip-audit, Dependabot দিয়ে নিয়মিত পরীক্ষা, আর ছোট প্রজেক্টের একটি নিরাপত্তা-চেকলিস্ট
১ · নিরাপত্তা মানে বিশ্বাসের সীমানা চেনা
সুমাইয়া একটি কোচিং সেন্টারের জন্য ছোট একটি ওয়েবসাইট বানিয়েছেন। শিক্ষার্থীরা রোল নম্বর লিখলে মডেল টেস্টের ফল দেখায়, শিক্ষকেরা লগইন করে PDF নোট আপলোড করেন, আর একটি বাইরের SMS সেবার API key দিয়ে ফলের খবর পাঠানো হয়। সাইটটি দিব্যি চলছে। কিন্তু একদিন একজন অভিভাবক জানালেন, রোলের ঘরে অদ্ভুত কিছু লেখায় পাতাটি ভেঙে লাল error দেখাচ্ছে — আর সেই error-এ database-এর টেবিলের নাম, ফাইলের পুরো পথ, সব দেখা যাচ্ছে।
কোনো ক্ষতি হয়নি, তবে সতর্ক হওয়ার মতো ঘটনা। সুমাইয়ার কোড “ঠিকঠাক” কাজ করে — যতক্ষণ সবাই ঠিকঠাক ইনপুট দেয়। নিরাপদ কোডের প্রশ্ন ঠিক উল্টো: কেউ ভুল করে বা ইচ্ছা করে অপ্রত্যাশিত কিছু দিলে কী হয়?
এই পর্বের পুরো চিন্তা একটি বাক্যে ধরা যায়: বাইরে থেকে যা আসে, তা ভুল বা ক্ষতিকর হতে পারে — তাই দরজায় যাচাই করুন, secret কোডের বাইরে রাখুন, আর নিজে না বানিয়ে পরীক্ষিত নিরাপদ যন্ত্র ব্যবহার করুন। “বাইরে” মানে শুধু ওয়েব ফর্ম নয়: ফাইল, নেটওয়ার্ক, environment, command-line argument, এমনকি আপনার ইনস্টল করা package-ও। আপনার কোড আর এই বাইরের জগতের মাঝের রেখাটির নাম trust boundary.
মেট্রো রেলের স্টেশনের কথা ভাবুন। প্ল্যাটফর্মে ঢোকার আগে একটিমাত্র জায়গায় ব্যাগ স্ক্যান হয়, কার্ড যাচাই হয়। ভেতরে ঢোকার পর ট্রেনের প্রতিটি দরজায় আবার তল্লাশি হয় না, কারণ দরজাটি একটিই এবং সেখানে কড়া পাহারা। আবার ট্রেন চালকের কেবিনে যাত্রীর কার্ড কাজ করে না — যার যতটুকু দরকার, ততটুকুই অধিকার। প্রথমটি trust boundary-তে যাচাই, দ্বিতীয়টি least privilege.
নিচের ডায়াগ্রামে সুমাইয়ার সাইটের একটি অনুরোধ বাইরে থেকে database পর্যন্ত কীভাবে যায়, ধাপে ধাপে দেখুন:
- বাইরের জগৎ
ফর্ম · ফাইল · APIবিশ্বাস করা যায় না - দরজা
validate()ধরন, দৈর্ঘ্য, allow-list - অ্যাপের যুক্তি
int, str, Pathপরিষ্কার ধরনের মান - নিরাপদ যন্ত্র
execute(sql, (x,))ডেটা আর নির্দেশ আলাদা - Database · ফাইল
ন্যূনতম অধিকারশুধু যা দরকার
roll = "230101" ← প্রত্যাশিত roll = " 230101\n" ← বাড়তি ফাঁকা জায়গা roll = "২৩০১০১" ← বাংলা অঙ্ক roll = (১০ লাখ অক্ষরের লেখা) roll = (SQL-এর চিহ্নমেশানো লেখা)
roll = roll.strip()
if not (roll.isascii() and roll.isdigit()
and len(roll) == 6):
reject("অবৈধ রোল নম্বর")
roll: str # ঠিক ৬টি ASCII অঙ্ক — প্রমাণিত # ভেতরের ফাংশনগুলো আর অদ্ভুত ইনপুট নিয়ে ভাবে না
db.execute("SELECT name, gpa FROM result WHERE roll = ?",
(roll,))
ফল দেখানোর অংশ → result টেবিলে শুধু SELECT
নোট আপলোড → শুধু uploads/ ফোল্ডারে লেখা
ভুল হলেও ক্ষতির পরিধি ছোট থাকে
- বাইরের জগৎ। ফর্মের ঘর, আপলোড করা ফাইল, অন্য সার্ভারের API-র উত্তর, command-line argument — সবই আপনার নিয়ন্ত্রণের বাইরে। এগুলো বেশিরভাগ সময় ঠিক থাকে, কিন্তু ভুলভাবে বা ইচ্ছা করে অদ্ভুত হতে পারে। নিয়ম: বাইরের ডেটাকে ধরে নিন “অজানা”, যতক্ষণ না প্রমাণ হয়।
- দরজায় যাচাই। সীমানা পেরোনোর ঠিক মুহূর্তে একবার কড়া যাচাই: ধরন ঠিক আছে কি না, দৈর্ঘ্য সীমার ভেতরে কি না, আর মানটি অনুমোদিত তালিকার মধ্যে পড়ে কি না। যা চেনা নয়, তা ফেরত — ঠিক করার চেষ্টা নয়।
- ভেতরে পরিষ্কার ধরন। যাচাই পেরোনো মানে এখন একটি প্রমাণিত মান। ভেতরের কোড প্রতিটি লাইনে আবার সন্দেহ করে না — তাই কোডও সরল থাকে। বড় প্রজেক্টে এই কাজ pydantic-এর মতো library করে।
- নিরাপদ যন্ত্র। যাচাইয়ের পরেও মানটিকে কখনো SQL বা shell-এর লেখার ভেতরে জোড়া হয় না। Parameterized query মানকে আলাদা পথে পাঠায়, তাই সেটি কখনো নির্দেশ হয়ে ওঠে না। যাচাই আর নিরাপদ যন্ত্র — দুটি আলাদা স্তর; একটিতে ভুল হলে অন্যটি রক্ষা করে।
- ন্যূনতম অধিকার। ফল দেখানোর অংশটি যে database অ্যাকাউন্টে চলে, তার শুধু পড়ার অধিকার থাকলে কোনো বাগ থাকলেও ডেটা মুছে যাওয়ার সুযোগ নেই। প্রতিটি অংশকে তার কাজের জন্য যতটুকু দরকার, ঠিক ততটুকু অধিকার — এটিই least privilege.
এক স্তরের ওপর ভরসা নয়। দরজায় যাচাই, নিরাপদ API, ন্যূনতম অধিকার, পরিষ্কার error, আর নিয়মিত টুল-পরীক্ষা — কয়েকটি স্তর মিলে সুরক্ষা। একে বলে defense in depth. একটি স্তরে ফাঁক থাকবেই; লক্ষ্য হলো সব স্তরে একই জায়গায় ফাঁক যেন না থাকে।
২০০১ সালে শুরু হওয়া উন্মুক্ত সংগঠন OWASP (Open Worldwide Application Security Project) ২০০৩ সাল থেকে কয়েক বছর পরপর ওয়েব অ্যাপের সবচেয়ে সাধারণ দশটি ঝুঁকির তালিকা “OWASP Top 10” প্রকাশ করে। প্রতিটি সংস্করণে injection তালিকায় জায়গা পেয়েছে, আর ২০১০, ২০১৩ ও ২০১৭ সালে ছিল একেবারে প্রথম স্থানে — কারণটি আমরা অংশ ৩-এ দেখব। এই পর্বে আক্রমণের কৌশল নয়, শুধু প্রতিরোধের অভ্যাস শেখানো হবে।
C/C++-এ নিরাপত্তার বড় অংশ মেমরি নিয়ে — buffer overflow, use-after-free, undefined behavior. Python নিজেই সীমা যাচাই আর মেমরি সামলায়, তাই সেই শ্রেণির বাগ সাধারণ Python কোডে প্রায় থাকে না। কিন্তু injection, ফাঁস হওয়া secret, দুর্বল পাসওয়ার্ড-সংরক্ষণ আর dependency-র ঝুঁকি দুই জগতেই একই। মেমরির দিকটি দেখতে C++ কোর্সের UB ও নিরাপদ C++ আর C কোর্সের নিরাপদ কোডিং পর্ব পড়ুন।
২ · দরজায় যাচাই — allow-list, ধরন, সীমা আর দৈর্ঘ্য
Input validation মানে বাইরের মানকে ব্যবহারের আগে চারটি প্রশ্ন করা:
| প্রশ্ন | উদাহরণ | Python-এ |
|---|---|---|
| ধরন কি ঠিক? | বয়স একটি পূর্ণসংখ্যা | int(text), ব্যর্থ হলে ValueError |
| পরিসর কি ঠিক? | নম্বর ০ থেকে ১০০ | 0 <= mark <= 100 |
| দৈর্ঘ্য কি সীমায়? | নাম সর্বোচ্চ ৬০ অক্ষর | len(name) <= 60 |
| মান কি অনুমোদিত? | বিভাগ শুধু বিজ্ঞান/মানবিক/ব্যবসায় | dept in ALLOWED, বা re.fullmatch |
Allow-list বনাম deny-list
দুইভাবে যাচাই করা যায়। Deny-list: খারাপ জিনিসের তালিকা বানিয়ে সেগুলো আটকানো — যেমন “', ;, .. থাকলে বাতিল”। Allow-list: যা অনুমোদিত, শুধু সেটির নিয়ম লেখা — যেমন “ঠিক ৬টি অঙ্ক”। দ্বিতীয়টি প্রায় সবসময় ভালো। কারণ খারাপ জিনিসের তালিকা কখনো সম্পূর্ণ হয় না — নতুন এনকোডিং, Unicode-এর দেখতে একই রকম অক্ষর, বা আপনার না-ভাবা কোনো রূপ ঠিকই ফাঁক গলে ঢুকে যায়। অন্যদিকে “ঠিক ৬টি অঙ্ক”-এর বাইরে কোনো কিছুই ঢুকতে পারে না।
বিয়েবাড়ির গেটে দুই রকম পাহারাদার ভাবুন। একজনের হাতে “এরা ঢুকবে না” তালিকা — তালিকার বাইরের যেকোনো অচেনা মানুষ ঢুকে পড়ে। অন্যজনের হাতে নিমন্ত্রিতদের তালিকা — তালিকায় নাম না থাকলে কেউ ঢুকতে পারে না। নিরাপত্তায় দ্বিতীয় পাহারাদারই আপনার চাই।
Python-এ allow-list লেখার কয়েকটি পরিচিত উপায়: ছোট নির্দিষ্ট মানের জন্য একটি set বা Enum; গঠনের জন্য re.fullmatch (পর্ব ২৮ — re.match নয়, কারণ সেটি শুধু শুরুতে মেলায়, শেষে বাড়তি কিছু থাকলেও খুশি থাকে); সংখ্যার জন্য int() রূপান্তর আর পরিসর। রূপান্তর ব্যর্থ হলে ValueError ধরুন আর ব্যবহারকারীকে পরিষ্কার বার্তা দিন (পর্ব ২৪)।
Unicode-এর একটি ফাঁদ: "২৩০১০১".isdigit() Python-এ True! str.isdigit() বাংলা, আরবি, দেবনাগরী — সব লিপির অঙ্ককেই অঙ্ক মানে, আর int("২৩০১০১")-ও সফলভাবে 230101 দেয়। এটি বাংলাভাষী ব্যবহারকারীর জন্য সুবিধা হতে পারে, আবার যেখানে শুধু ASCII অঙ্ক প্রত্যাশিত, সেখানে চমকও। তাই যা চান, ঠিক সেটিই লিখুন: s.isascii() and s.isdigit(), বা re.fullmatch(r"[0-9]{6}", s). (মনে রাখুন, regex-এ \d-ও ডিফল্টভাবে সব লিপির অঙ্ক মেলায়।)
আরও কয়েকটি অভ্যাস:
- আগে স্বাভাবিক রূপ, তারপর যাচাই।
strip(), প্রয়োজনেcasefold()বা Unicode normalization (পর্ব ০৯) আগে করুন, যাচাই পরে — নইলে যাচাই করা রূপ আর ব্যবহার করা রূপ আলাদা হয়ে যায়। - দৈর্ঘ্যের সীমা সবসময়। ১০ লাখ অক্ষরের একটি “নাম” আপনার regex বা database-কে অকারণে ধীর করতে পারে। সার্ভারে আপলোডের আকারেও সীমা রাখুন।
- যাচাই সার্ভারে। ব্রাউজারের JavaScript-এর যাচাই শুধু ব্যবহারকারীর সুবিধার জন্য; সেটি পাশ কাটিয়ে সরাসরি অনুরোধ পাঠানো যায়। আসল যাচাই হয় আপনার Python কোডে।
- বড় কাঠামোয় pydantic. FastAPI (পর্ব ৪৮) ভেতরে pydantic ব্যবহার করে: আপনি একটি class-এ ঘরের ধরন আর সীমা লেখেন (
roll: str = Field(pattern=...)), আর ভুল ইনপুট আপনার ফাংশনে পৌঁছানোর আগেই পরিষ্কার error-সহ ফেরত যায়।
৩ · Injection — যখন ডেটা নির্দেশ হয়ে যায়
প্রায় সব injection-এর মূল একটিই: প্রোগ্রাম একটি লেখা বানায়, যা অন্য কোনো “ভাষার” interpreter চালাবে — SQL, shell, Python, HTML — আর সেই লেখার ভেতরে বাইরের ডেটা সরাসরি জুড়ে দেয়। তখন ডেটার কোনো অংশ সেই ভাষার চোখে নির্দেশ হয়ে যেতে পারে। সমাধানও তাই একটিই নীতিতে দাঁড়িয়ে: ডেটা আর নির্দেশকে আলাদা পথে পাঠানো, যাতে ডেটা কখনো নির্দেশ হিসেবে পড়া না যায়।
ব্যাংকের চেকের কথা ভাবুন। টাকার অঙ্ক লেখার নির্দিষ্ট ঘর আছে, আর অঙ্কের পরে দাগ টেনে দেওয়া হয় যাতে কেউ পাশে কিছু যোগ করতে না পারে। খালি কাগজে “অমুককে … টাকা দিন” লিখে দিলে পরে যে কেউ বাক্যটি বাড়িয়ে দিতে পারত। Parameterized query হলো সেই ছাপানো চেক — মান বসে নির্দিষ্ট ঘরে, বাক্যের গঠন বদলাতে পারে না।
SQL — সবসময় parameterized query
এই কাজটি কখনো করবেন না: f-string, % বা + দিয়ে ব্যবহারকারীর মান SQL লেখার ভেতরে জোড়া — যেমন f"... WHERE roll = '{roll}'". মানের ভেতরে একটি ' চিহ্ন থাকলেই SQL-এর গঠন বদলে যায়। নিরীহ ক্ষেত্রে শুধু error আসে (“D'Costa” নামটিই এমন ভাঙন ঘটায়), কিন্তু একই ফাঁক দিয়ে কেউ ইচ্ছা করে query-র অর্থই বদলে দিতে পারে — এটিই SQL injection. ফলে অন্যের ডেটা দেখা, বদলানো বা মুছে ফেলার মতো গুরুতর ক্ষতি হতে পারে।
সঠিক পথ: SQL লেখায় মানের জায়গায় ? placeholder, আর মানগুলো আলাদা একটি tuple-এ। sqlite3 মডিউল তখন মানকে শুধু ডেটা হিসেবে database-কে দেয় — এর ভেতরে যা-ই থাকুক, SQL-এর গঠনের অংশ হয় না। সুমাইয়ার ফল দেখানোর ফাংশনটি নিরাপদভাবে, allow-list যাচাইসহ:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE result (roll TEXT PRIMARY KEY, name TEXT, gpa REAL)")
db.executemany("INSERT INTO result VALUES (?, ?, ?)",
[("230101", "Nusrat Jahan", 5.0), ("230102", "Rina D'Costa", 4.75)])
def find_result(roll: str) -> str:
roll = roll.strip()
# allow-list: ঠিক ৬টি ASCII অঙ্ক — এর বাইরে কিছুই database পর্যন্ত যাবে না
if not (roll.isascii() and roll.isdigit() and len(roll) == 6):
return "অবৈধ রোল নম্বর"
row = db.execute("SELECT name, gpa FROM result WHERE roll = ?", (roll,)).fetchone()
return f"{row[0]} — GPA {row[1]}" if row else "ফল পাওয়া যায়নি"
for asked in ["230101", " 230102 ", "2301O1", "২৩০১০১", "999999"]:
print(f"{asked!r:<10} -> {find_result(asked)}")":memory:" মানে database শুধু মেমরিতে, কোনো ফাইল তৈরি হয় না। পাঁচটি অনুরোধের ফল: '230101' → Nusrat Jahan — GPA 5.0; ' 230102 ' → ফাঁকা জায়গা strip()-এ বাদ, তাই Rina D'Costa — GPA 4.75; '2301O1' (মাঝে ইংরেজি O অক্ষর) → অবৈধ রোল নম্বর; '২৩০১০১' → isdigit() সত্য হলেও isascii() মিথ্যা, তাই অবৈধ রোল নম্বর; '999999' → বৈধ রূপ কিন্তু টেবিলে নেই, তাই ফল পাওয়া যায়নি. খেয়াল করুন, “D'Costa” নামের ' চিহ্নটি executemany-এর ? দিয়ে ঢোকানোয় কোনো সমস্যাই হয়নি — string জুড়ে লিখলে এই নামটিই SQL ভেঙে দিত। (roll,)-এর কমাটি জরুরি: এটি একটি উপাদানের tuple.
একটি সূক্ষ্ম বিষয়: placeholder শুধু মানের জন্য। টেবিল বা কলামের নাম ? দিয়ে বসানো যায় না। ব্যবহারকারী যদি “কোন কলাম অনুযায়ী সাজাবে” বেছে নেন, তাহলে নামটি একটি নির্দিষ্ট allow-list থেকে নিন: {"name", "gpa"}-এর মধ্যে না থাকলে বাতিল। SQLAlchemy-র মতো ORM বা query builder ব্যবহার করলেও ভেতরে এই parameterized পথই চলে — তবে সেখানেও কাঁচা SQL-এর ফাংশনে string জুড়লে একই ঝুঁকি ফিরে আসে।
OS command — list-এ argument, shell=True নয়
পর্ব ৪৯-এ subprocess দিয়ে অন্য প্রোগ্রাম চালানো দেখেছেন। নিরাপদ নিয়মটি সরল: কমান্ড আর তার argument-গুলো একটি list হিসেবে দিন, আর ব্যবহারকারীর মান থাকলে shell=True কখনো নয়। list দিলে Python সরাসরি প্রোগ্রামটি চালায়, প্রতিটি উপাদান একটি আলাদা argument হয় — মাঝে কোনো shell নেই, তাই shell-এর বিশেষ চিহ্ন (;, |, &, $( )) কোনো অর্থ পায় না। shell=True-এ পুরো string-টি shell পড়ে, আর মানের ভেতরের চিহ্ন বাড়তি কমান্ড হয়ে যেতে পারে — এটিই command injection.
import subprocess
# ✗ করবেন না: ব্যবহারকারীর মান shell-এর লেখায়
# subprocess.run(f"zip backup.zip {folder}", shell=True)
# ✓ নিরাপদ: list, shell নেই; "--" এর পরে সব কিছু ফাইলের নাম
subprocess.run(["zip", "-r", "backup.zip", "--", folder], check=True, timeout=60)সাথে আরও তিনটি অভ্যাস: "--" দিয়ে option শেষ জানান, যাতে - দিয়ে শুরু হওয়া কোনো ফাইলের নাম option হিসেবে পড়া না যায়; সম্ভব হলে বাইরের প্রোগ্রাম না ডেকে Python-এর নিজস্ব যন্ত্র ব্যবহার করুন (shutil.make_archive, shutil.copy, pathlib) — সবচেয়ে নিরাপদ কমান্ড হলো যে কমান্ড চালাতেই হয় না; আর os.system() এড়িয়ে চলুন, কারণ এটি সবসময় shell ব্যবহার করে।
Code — eval/exec কখনো বাইরের ডেটায় নয়
eval() একটি লেখাকে Python কোড হিসেবে চালায়। ব্যবহারকারীর ইনপুট এতে দিলে আপনি আসলে তাঁকে আপনার সার্ভারে যেকোনো Python কোড চালানোর অধিকার দিচ্ছেন — ফাইল পড়া, মুছে ফেলা, সবই। eval(text, {"__builtins__": {}})-এর মতো “খাঁচা” বানানোর চেষ্টা বহুবার ব্যর্থ প্রমাণিত হয়েছে; Python-এর ডকুমেন্টেশনও স্পষ্ট বলে, এটি নিরাপদ খাঁচা নয়। প্রয়োজন অনুযায়ী বিকল্প:
| আপনি আসলে যা চান | নিরাপদ পথ |
|---|---|
| লেখা থেকে সংখ্যা | int(text), float(text), Decimal(text) |
"[1, 2, 3]" বা "{'a': 1}"-এর মতো সরল literal | ast.literal_eval — শুধু string, সংখ্যা, tuple, list, dict, set, bool, None বোঝে; কোনো ফাংশন ডাকে না |
| প্রোগ্রামের মধ্যে ডেটা আদান-প্রদান | json.loads (পর্ব ২৬) |
| ব্যবহারকারী একটি কাজ বেছে নেবেন | dict-এ নাম থেকে ফাংশন: ACTIONS[name](), নাম allow-list-এ না থাকলে বাতিল |
| ক্যালকুলেটরের মতো সূত্র হিসাব | নিজের ছোট parser, বা পরিচিত নিরাপদ library — eval নয় |
ast.literal_eval-ও সীমাহীন নয় — খুব বড় বা গভীরভাবে নেস্ট করা ইনপুটে মেমরি বা সময় বেশি লাগতে পারে, তাই আগে দৈর্ঘ্যের সীমা দিন। আর ডেটা আদান-প্রদানের জন্য json-ই প্রথম পছন্দ।
HTML — ওয়েব পাতায় দেখানোর আগে escape
ব্যবহারকারীর লেখা মন্তব্য বা নাম যদি সরাসরি HTML পাতায় বসানো হয়, তাহলে লেখার ভেতরের < আর > চিহ্ন ব্রাউজারের চোখে HTML ট্যাগ হয়ে যেতে পারে — এমনকি অন্য দর্শকের ব্রাউজারে চলা script-ও। একে বলে cross-site scripting (XSS). সমাধান হলো দেখানোর মুহূর্তে escape করা: html.escape(text) <-কে <, &-কে &, আর উদ্ধৃতিচিহ্নকে নিরাপদ রূপে বদলে দেয়, ফলে ব্রাউজার সেটিকে শুধু লেখা হিসেবে দেখায়। বাস্তবে Jinja2-র মতো template engine (Flask ও অনেক ফ্রেমওয়ার্কে) HTML template-এ autoescape চালু রাখে — ঝুঁকি আসে তখনই, যখন কেউ |safe বা Markup() দিয়ে সেটি বন্ধ করেন।
SQL:
? · Shell: list · Python: literal_eval/json · HTML: escape৪ · ফাইলের পথ আর deserialization — সীমানার আরও দুটি দরজা
সুমাইয়ার সাইটে শিক্ষার্থীরা নোট নামাতে পারেন: /download?name=physics-ch3.pdf. সার্ভারে সব নোট থাকে একটি নির্দিষ্ট ফোল্ডারে। সরল কোড হতো open(BASE / name). কিন্তু name-এ যদি ../ থাকে, তাহলে পথটি নোটের ফোল্ডার থেকে এক ধাপ ওপরে উঠে যায়, আর অনুমোদিত ফোল্ডারের বাইরের ফাইল — কনফিগ, পাসওয়ার্ডের ফাইল — চলে আসতে পারে। একে বলে path traversal. Absolute path দিলেও একই সমস্যা: BASE / "/etc/hosts"-এর ফল শুধু /etc/hosts, কারণ pathlib-এ ডান দিকে absolute path থাকলে বাঁ দিকটি বাদ পড়ে (পর্ব ২৫)।
সমাধান: পথ জোড়ার পর resolve() দিয়ে আসল, পূর্ণ পথ বের করুন (.. আর symlink খুলে), তারপর যাচাই করুন সেটি অনুমোদিত ফোল্ডারের ভেতরে কি না — is_relative_to() দিয়ে (Python 3.9 থেকে)। ধাপে ধাপে দেখুন, একটি বৈধ আর একটি ../-যুক্ত নামের ক্ষেত্রে কী ঘটে:
1BASE = Path("/srv/notes").resolve()2def safe_path(name: str) -> Path:3 target = (BASE / name).resolve()4 if not target.is_relative_to(BASE):5 raise PermissionError("নিষিদ্ধ পথ")6 return target
| name | |
| BASE / name | |
| target (resolve) | |
| BASE-এর ভেতরে? |
- ভিত্তি ঠিক করা। অনুমোদিত ফোল্ডারটি একবারই, শুরুতে
resolve()করে রাখা হলো। এতে তুলনার দুই পাশেই পূর্ণ, স্বাভাবিক পথ থাকবে — নইলে symlink বা আপেক্ষিক পথের কারণে তুলনা ভুল হতে পারে। - বৈধ নাম জোড়া।
physics-ch3.pdfভিত্তির সাথে জোড়া হলো, আরresolve()-এর পরেও পথটি একই থাকল — কোনো..বা symlink নেই। - যাচাই পাশ।
is_relative_to(BASE)সত্য, কারণ পথটি/srv/notes-এর ভেতরে। ফাংশন পথটি ফেরত দেয়, ফাইল পাঠানো হবে। - এবার
../-যুক্ত নাম। জোড়ার পরে লেখাটি দেখতে এখনো/srv/notes/...দিয়ে শুরু হয় — শুধু string-এর শুরু মিলিয়ে দেখলে (startswith) এটি পাশ করে যেত! এজন্যই জোড়া লেখার ওপর ভরসা নয়। - resolve আসল চেহারা দেখায়।
..মানে এক ধাপ ওপরে, তাই আসল পথ/srv/config.ini— নোটের ফোল্ডারের বাইরে। symlink থাকলে resolve সেটিও খুলে আসল গন্তব্য দেখায়। - যাচাই ব্যর্থ, অনুরোধ বাতিল।
is_relative_toমিথ্যা, তাইPermissionError. ব্যবহারকারীকে শুধু একটি সাধারণ “ফাইল নেই” উত্তর দিন; ভেতরের পথ বা কারণ জানানোর দরকার নেই। চাইলে নামের জন্য আগে একটি allow-list-ও দিন (যেমন শুধু[a-z0-9-]+\.pdf) — দুই স্তর।
startswith দিয়ে পথ যাচাই করবেন না। str(target).startswith("/srv/notes")-এর দুটি সমস্যা: .. resolve না করলে সেটি ধরা পড়ে না, আর /srv/notes-old/...-এর মতো পাশের ফোল্ডারও “মিলে যায়”। is_relative_to পথকে অংশ ধরে ধরে তুলনা করে, তাই দুটি সমস্যাই এড়ায়। আরও সহজ নকশা হলো ব্যবহারকারীকে ফাইলের নামই না দেওয়া — database-এ একটি id রাখুন, আর id থেকে সার্ভার নিজে পথ বের করুক।
pickle শুধু বিশ্বস্ত ডেটায় — বাইরের জন্য json
পর্ব ২৬-এ দেখেছেন, pickle প্রায় যেকোনো Python object সংরক্ষণ করতে পারে। এই ক্ষমতাই তার বিপদ: pickle ফাইল খোলার (pickle.load) সময় ফাইলের ভেতরের নির্দেশ অনুযায়ী যেকোনো ফাংশন ডাকা সম্ভব। তাই অবিশ্বস্ত উৎসের pickle খোলা মানে অচেনা কারও কোড চালানো — arbitrary code execution. Python-এর ডকুমেন্টেশনের pickle পাতার শুরুতেই এই সতর্কবার্তা আছে।
- নেটওয়ার্ক, ব্যবহারকারীর আপলোড বা অন্য কারও ফাইল:
json. json শুধু ডেটা (সংখ্যা, লেখা, list, dict) বোঝে; খোলার সময় কোনো কোড চলে না। - শুধু নিজের প্রোগ্রামের নিজের cache: pickle চলতে পারে — যদি নিশ্চিত হন অন্য কেউ ফাইলটি বদলাতে পারে না।
- মেশিন লার্নিং মডেল: অনেক পুরোনো মডেল-ফাইল ভেতরে pickle ব্যবহার করে। তাই অচেনা উৎসের মডেল খোলার আগে সাবধান; ওজন রাখার জন্য safetensors-এর মতো শুধু-ডেটা ফরম্যাট তৈরি হয়েছে।
- YAML: PyYAML-এ সবসময়
yaml.safe_load. পুরোনোyaml.loadসাধারণ ডেটার বাইরে Python object বানাতে পারত; PyYAML 6.0 থেকে Loader উল্লেখ না করলে এটি চলেই না। - XML: অবিশ্বস্ত XML-এর ক্ষেত্রে Python-এর ডকুমেন্টেশনই সতর্ক করে — বিশেষভাবে বানানো XML বিপুল মেমরি খেতে পারে। বাইরের XML পড়তে defusedxml package-এর নিরাপদ parser ব্যবহার করুন।
৫ · Secret — কোডের বাইরে, log-এর বাইরে, git-এর বাইরে
সুমাইয়ার প্রথম সংস্করণে SMS সেবার key কোডের ভেতরেই লেখা ছিল: API_KEY = "sk_live_...". কোডটি GitHub-এ গেলে key-ও গেল — আর পাবলিক repository স্বয়ংক্রিয় প্রোগ্রাম দিয়ে নিয়মিত খোঁজা হয়, ফাঁস হওয়া key খুব দ্রুত অপব্যবহার হতে পারে। API key, database-এর পাসওয়ার্ড, token — এগুলো সবই secret, আর এদের প্রথম নিয়ম: secret কখনো কোডে নয়।
প্রচলিত পথ হলো secret-কে environment variable-এ রাখা, আর কোড শুধু নাম ধরে পড়বে। না পেলে প্রোগ্রাম শুরুতেই পরিষ্কার বার্তা দিয়ে থামবে — অর্ধেক কাজ করে মাঝপথে রহস্যময়ভাবে ভাঙবে না:
import os
import sys
def require_env(name: str) -> str:
value = os.environ.get(name, "").strip()
if not value:
sys.exit(f"কনফিগ ভুল: {name} environment variable সেট করা নেই। "
f".env.example দেখে সেট করুন।")
return value
SMS_API_KEY = require_env("SMS_API_KEY") # মানটি কখনো print বা log নয়খেয়াল করুন, error বার্তায় শুধু variable-এর নাম আছে, মান নয়। নিজের কম্পিউটারে প্রতিবার environment variable সেট করা ঝামেলা, তাই প্রজেক্ট-ফোল্ডারে একটি .env ফাইল রাখা হয় (SMS_API_KEY=...), আর python-dotenv-এর মতো package বা টুল সেটি পড়ে environment-এ বসায়। নিয়মগুলো:
.envসবসময়.gitignore-এ (পর্ব ২৩-এ দেখেছেন)। git-এ রাখুন শুধু.env.example— নামগুলোসহ, মান ফাঁকা — যাতে সহকর্মী জানেন কী কী লাগবে।- সার্ভারে secret আসে হোস্টিং-সেবার “environment/secrets” সেটিং, CI-এর secret store (যেমন GitHub Actions secrets) বা ক্লাউডের secret manager থেকে।
- প্রতিটি পরিবেশের আলাদা key: নিজের কম্পিউটারের পরীক্ষার key আর আসল সার্ভারের key এক নয়; আর প্রতিটি key-কে সেবার সেটিং থেকে ন্যূনতম অধিকার দিন — least privilege আবার।
Log-এ secret নয়
পর্ব ৪০-এ logging শিখেছেন। ডিবাগের সময় logger.debug(f"config: {settings}") লিখে ফেলা খুব সহজ — আর তাতে পুরো config, key-সহ, log ফাইলে চলে যায়। log ফাইল প্রায়ই অনেক মানুষ ও টুল পড়ে, বহুদিন জমা থাকে। তাই: পাসওয়ার্ড, token, পুরো অনুরোধের header বা body log করবেন না; দরকার হলে শুধু শেষ চারটি অক্ষর (...a9f2); আর secret রাখার class-এর __repr__ এমনভাবে লিখুন যাতে ছাপলে মান না দেখায় (pydantic-এর SecretStr ঠিক এটিই করে)।
Key ফাঁস হলে — মুছে ফেলা যথেষ্ট নয়
ভুল করে key git-এ চলে গেলে পরের commit-এ লাইনটি মুছে দিলেও key পুরোনো commit-এর ইতিহাসে থেকে যায়, আর যে কেউ ইতিমধ্যে দেখে বা কপি করে থাকতে পারে। তাই প্রথম কাজ: সেবার ড্যাশবোর্ড থেকে key বাতিল করে নতুন key তৈরি — একে বলে key rotation. তারপর নতুন key environment-এ বসান, আর সেবার ব্যবহারের ইতিহাসে অস্বাভাবিক কিছু হয়েছে কি না দেখুন। ইতিহাস থেকে মুছে ফেলা (git filter-repo-এর মতো টুল দিয়ে) পরের কাজ, বিকল্প নয়।
প্রতিরোধের জন্য আছে secret scanning: GitHub পরিচিত ধরনের key-র নকশা চিনে সতর্ক করে, আর “push protection” চালু থাকলে key-সহ commit push করার মুহূর্তেই আটকে দেয় — ২০২৪ সাল থেকে পাবলিক repository-তে এটি ডিফল্টভাবে চালু। নিজের কম্পিউটারে commit-এর আগে চালানোর জন্য gitleaks বা detect-secrets-এর মতো টুলও আছে।
Secret বানাতে secrets, random নয়
পর্ব ২৭-এ দেখেছেন, random module-এর সংখ্যা পরিসংখ্যানের দিক থেকে এলোমেলো, কিন্তু নিরাপত্তার জন্য নয়: এর ভেতরের Mersenne Twister যথেষ্ট আউটপুট দেখে পরের মান অনুমানযোগ্য। Password-reset-এর লিংক, OTP, session token বা API key বানাতে সবসময় secrets module (Python 3.6 থেকে), যা অপারেটিং সিস্টেমের নিরাপদ উৎস (CSPRNG) ব্যবহার করে: secrets.token_urlsafe(32), secrets.token_hex(16), আর OTP-র জন্য secrets.randbelow(10**6).
৬ · পাসওয়ার্ড — hashing, encryption আর encoding-এর পার্থক্য
সুমাইয়ার শিক্ষকদের লগইন আছে। পাসওয়ার্ড কীভাবে রাখবেন? প্রথমে তিনটি শব্দ আলাদা করা দরকার, কারণ এগুলো প্রায়ই গুলিয়ে যায় — আর AI-এর লেখা কোডেও:
🔤 Encoding
শুধু রূপ বদল, কোনো গোপনীয়তা নেই। যে কেউ উল্টো করতে পারে, কোনো key লাগে না। base64, UTF-8, URL-encoding এর উদাহরণ।
base64 এনক্রিপশন নয়!🔐 Encryption
Key দিয়ে তালা দেওয়া; key থাকলে উল্টো করে আসল লেখা ফেরত পাওয়া যায়। যে ডেটা পরে পড়তে হবে — যেমন সংরক্ষিত ফোন নম্বর বা অন্য সেবার token — তার জন্য encryption.
যেমন: cryptography package-এর Fernet🧬 Hashing
একমুখী: ইনপুট থেকে ছাপ বানানো যায়, ছাপ থেকে ইনপুট ফেরত নয়। পাসওয়ার্ড আসলে ফেরত পড়ার দরকার নেই — শুধু মিলিয়ে দেখতে হয়। তাই পাসওয়ার্ডের জন্য hashing.
cryptographic hashকিন্তু সাধারণ hash (MD5, SHA-1, এমনকি SHA-256) পাসওয়ার্ডের জন্য যথেষ্ট নয়। দুটি কারণ:
- এগুলো খুব দ্রুত। ফাইলের ছাপ নেওয়ার জন্য দ্রুত হওয়াই ভালো। কিন্তু database কোনোভাবে ফাঁস হলে আক্রমণকারী নিজের যন্ত্রে সেকেন্ডে কোটি কোটি অনুমান মিলিয়ে দেখতে পারে, আর সাধারণ পাসওয়ার্ডগুলো দ্রুত বেরিয়ে যায়।
- salt ছাড়া একই পাসওয়ার্ডের একই ছাপ। দুজন শিক্ষকের পাসওয়ার্ড এক হলে hash-ও এক — একটি ভাঙলে দুটিই। আর আগে থেকে হিসাব করে রাখা বিশাল তালিকা (rainbow table) দিয়ে মিলিয়ে দেখা সহজ হয়।
সমাধান: প্রতিটি ব্যবহারকারীর জন্য আলাদা random salt, আর ইচ্ছা করে ধীর একটি password-hashing ফাংশন — যাকে বলে key derivation function (KDF). আসল ব্যবহারকারীর লগইনে একবার মেলাতে কয়েক মিলিসেকেন্ড বেশি লাগলে কেউ টের পান না; কিন্তু কোটিবার অনুমানের খরচ বিপুল হয়ে যায়। পুরো পাইপলাইনটি দেখুন:
- পাসওয়ার্ড
"Padma@2026"শুধু মেমরিতে, কিছুক্ষণ - Random salt
token_bytes(16)প্রতি ব্যবহারকারী আলাদা - ধীর hash
scrypt / Argon2ইচ্ছা করে ব্যয়বহুল - সংরক্ষণ
salt + hashপাসওয়ার্ড নয় - লগইনে মেলানো
compare_digestসমান সময়ে তুলনা
password = form["password"] # log-এ নয়, database-এ নয় if len(password) < 12: reject("অন্তত ১২ অক্ষর")
salt = secrets.token_bytes(16) # ১২৮ বিট, প্রতি ব্যবহারকারী নতুন একই পাসওয়ার্ড + ভিন্ন salt → সম্পূর্ণ ভিন্ন hash
digest = hashlib.scrypt(password.encode(), salt=salt,
n=2**14, r=8, p=1, dklen=32)
username | password_hash
nasrin | scrypt$16384$8$1$<salt hex>$<hash hex>
candidate = scrypt(typed.encode(), salt=stored_salt, ...)
ok = hmac.compare_digest(candidate, stored_hash)
- পাসওয়ার্ড আসে। নিবন্ধনের সময় পাসওয়ার্ড শুধু অল্প সময়ের জন্য মেমরিতে থাকে। এটি কখনো database, log বা ইমেইলে যায় না। এখানে যাচাইও হয়: খুব ছোট বা অতি পরিচিত পাসওয়ার্ড (যেমন “12345678”) বাতিল — দৈর্ঘ্যই সবচেয়ে বড় শক্তি।
- প্রতিটি ব্যবহারকারীর জন্য নতুন random salt.
secrets.token_bytes(16). salt গোপন নয় — hash-এর পাশেই খোলাখুলি রাখা হয়। এর কাজ শুধু প্রতিটি hash-কে অনন্য করা, যাতে একই পাসওয়ার্ডের দুটি hash আলাদা হয় আর আগে থেকে বানানো তালিকা অকেজো হয়ে যায়। - ধীর hash. scrypt-এর
nঠিক করে কতটা মেমরি আর সময় লাগবে। ফলে প্রতিটি অনুমানের জন্য আক্রমণকারীকেও সেই খরচ দিতে হয়, আর বিশেষ হার্ডওয়্যারে লাখো অনুমান একসাথে চালানো কঠিন হয়। Argon2 ও bcrypt-এর পেছনেও একই চিন্তা। - সংরক্ষণ — salt, hash আর প্যারামিটার একসাথে। একটি ঘরে অ্যালগরিদমের নাম, প্যারামিটার, salt আর hash রাখলে ভবিষ্যতে প্যারামিটার কঠিন করা যায়: পুরোনো ব্যবহারকারী পরের বার সফলভাবে লগইন করলে নতুন প্যারামিটারে আবার hash করে রাখা। bcrypt আর Argon2-এর library এই ফরম্যাট নিজেরাই বানায়।
- লগইনে মেলানো। টাইপ করা পাসওয়ার্ডকে সংরক্ষিত salt আর প্যারামিটার দিয়ে আবার hash করে তুলনা।
==নয় —hmac.compare_digest, যা প্রথম অমিলে থেমে না গিয়ে সবসময় সমান সময় নেয়। ফলে উত্তর আসতে কত সময় লাগল, তা থেকে কোনো তথ্য বেরোয় না। এই ধরনের সময় মেপে তথ্য বের করাকে বলে timing attack.
Python-এর standard library দিয়েই পুরো পাইপলাইনটি লেখা যায় — hashlib.scrypt (Python 3.6 থেকে, OpenSSL-এর ওপর নির্ভর করে) বা hashlib.pbkdf2_hmac, আর hmac.compare_digest. নিচের কোডে ফল মেলানোর সুবিধার জন্য salt নির্দিষ্ট রাখা হয়েছে; আসল কোডে প্রতিবার secrets.token_bytes(16):
import hashlib
import hmac
import secrets
def hash_password(password: str, salt: bytes) -> bytes:
# scrypt ইচ্ছা করে ধীর ও মেমরি-খেকো: একবার মেলাতে সমস্যা নেই, কোটিবার অনুমান ব্যয়বহুল
return hashlib.scrypt(password.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32)
def verify(password: str, salt: bytes, stored: bytes) -> bool:
return hmac.compare_digest(hash_password(password, salt), stored)
salt_a, salt_b = b"demo-salt-000001", b"demo-salt-000002" # বাস্তবে token_bytes(16)
stored = hash_password("Padma@2026", salt_a) # database-এ যায় salt + hash
print("hash-এর দৈর্ঘ্য:", len(stored), "byte")
print("সঠিক পাসওয়ার্ড:", verify("Padma@2026", salt_a, stored))
print("এক অক্ষর ভিন্ন:", verify("padma@2026", salt_a, stored))
print("ভিন্ন salt-এ একই hash?", hash_password("Padma@2026", salt_b) == stored)
token = secrets.token_urlsafe(32) # মানটি কখনো ছাপবেন না
print("নতুন API token-এর দৈর্ঘ্য:", len(token), "অক্ষর")ফল পাঁচ লাইনে: hash-এর দৈর্ঘ্য: 32 byte (dklen=32 চাওয়া হয়েছে); সঠিক পাসওয়ার্ড: True; এক অক্ষর ভিন্ন: False — শুধু P ছোট হাতের হওয়াতেই সম্পূর্ণ ভিন্ন hash; ভিন্ন salt-এ একই hash? False — একই পাসওয়ার্ড, ভিন্ন salt, ভিন্ন ফল, ঠিক যা salt-এর কাজ; আর নতুন API token-এর দৈর্ঘ্য: 43 অক্ষর — token_urlsafe(32) ৩২ byte random ডেটাকে URL-নিরাপদ base64-এ লেখে, তাই ৪৩ অক্ষর। token-এর মান ইচ্ছা করে ছাপা হয়নি: random মান প্রতিবার বদলায়, আর secret ছাপার অভ্যাসই রাখা উচিত নয়। কোডটি চলতে এক মুহূর্ত বেশি লাগলে খেয়াল করুন — ওটাই scrypt-এর ইচ্ছাকৃত ধীরতা।
বাস্তব প্রজেক্টে নিজে এই কোড না লিখে পরিচিত library নিন: argon2-cffi (Argon2id — বর্তমানে OWASP-এর প্রথম পছন্দ), bcrypt, বা Django-র মতো ফ্রেমওয়ার্কের নিজস্ব পাসওয়ার্ড ব্যবস্থা (Django ডিফল্টভাবে PBKDF2 ব্যবহার করে)। এগুলো salt, প্যারামিটার, ফরম্যাট আর ভবিষ্যতে প্যারামিটার আপডেট — সব নিজেই সামলায়। standard library-র বাইরে যেতে না চাইলে উপরের scrypt বা যথেষ্ট বেশি iteration-সহ pbkdf2_hmac. যা কখনো নয়: সাদা লেখায় পাসওয়ার্ড, MD5/SHA-1/SHA-256 একবার চালানো, base64 “এনক্রিপশন”, আর নিজের বানানো “গোপন” অ্যালগরিদম।
বার্তার সত্যতা যাচাইয়ে HMAC: কোনো সেবা (যেমন পেমেন্ট গেটওয়ে) আপনার সার্ভারে webhook পাঠালে সাধারণত সাথে একটি স্বাক্ষর দেয়: গোপন key আর বার্তা দিয়ে বানানো hmac.new(key, body, hashlib.sha256).hexdigest(). আপনি নিজে একই হিসাব করে hmac.compare_digest দিয়ে মিলিয়ে দেখেন বার্তাটি সত্যিই সেই সেবার, পথে কেউ বদলায়নি। এখানেও == নয়।
৭ · Dependency আর টুল — নিজের চোখের পাশাপাশি যন্ত্রের চোখ
আপনার প্রজেক্টের বেশিরভাগ কোড আপনি লেখেননি — সেগুলো dependency (পর্ব ২৩)। তাদের কোনোটিতে নিরাপত্তা-ত্রুটি ধরা পড়লে আপনার প্রজেক্টও ঝুঁকিতে। এমন প্রকাশ্য ত্রুটিগুলো একটি সাধারণ নম্বর পায় — CVE (যেমন CVE-2024-xxxxx) — আর ডেটাবেসে জমা হয়। আপনার কাজ চারটি অভ্যাস:
- নাম যাচাই করে ইনস্টল। Typosquatting আর AI-এর বানানো নাম (পর্ব ২৩) — pypi.org-এ নাম, বয়স, repo দেখে নিন।
- Pin আর lock. Pinning করা থাকলে নতুন কোনো version আপনার অজান্তে আসে না; hash-সহ lock file থাকলে নামানো ফাইল বদলানোও ধরা পড়ে (
pip install --require-hashes). - নিয়মিত audit.
pip-auditআপনার ইনস্টল করা version-গুলো জানা ত্রুটির ডেটাবেসের সাথে মেলায়। - আপডেট রাখা — Python নিজেও। প্রতিটি Python version নির্দিষ্ট সময় পর্যন্ত নিরাপত্তা-সংশোধন পায়; মেয়াদ শেষ হওয়া version (যেমন Python 3.8, ২০২৪ সালে শেষ) আর কোনো সংশোধন পায় না।
Pinning আর আপডেটের মধ্যে টানাপোড়েন আছে: বেঁধে রাখলে স্থিতি, কিন্তু পুরোনো ত্রুটিও বেঁধে থাকে। সমাধান হলো আপডেটকে একটি নিয়মিত, পরীক্ষিত প্রক্রিয়া বানানো। একটি ছোট দলের প্রক্রিয়া ধাপে ধাপে:
- Pin করা তালিকা
requirements.txtহুবহু version - Audit
pip-auditCI-তে প্রতিটি push-এ - আপডেটের প্রস্তাব
Dependabot PRনতুন version - টেস্ট
pytestকিছু ভাঙল কি? - Merge
নতুন pinআবার audit পরিষ্কার
fastapi==0.115.0
jinja2==3.1.3
requests==2.31.0
$ pip-audit -r requirements.txt
Found 1 known vulnerability in 1 package
Name Version ID Fix Versions
-------- ------- --------------- ------------
requests 2.31.0 (একটি CVE ID) 2.32.0
Bump requests from 2.31.0 to 2.32.x - requests==2.31.0 + requests==2.32.x Release notes · Changelog · Compatibility score
ruff check . ✓ bandit -r src -q ✓ pytest ✓ 84 passed
$ pip-audit -r requirements.txt
No known vulnerabilities found
- Pin করা তালিকা। প্রজেক্টে ঠিক কোন version চলছে, তা লেখা আছে — এটিই audit-এর ভিত্তি। তালিকা না থাকলে “আমরা কি ঝুঁকিতে?” প্রশ্নের উত্তরই দেওয়া যায় না।
- স্বয়ংক্রিয় audit. pip-audit প্রতিটি package-এর version Python-এর প্রকাশ্য ত্রুটি-ডেটাবেসের সাথে মেলায়, আর কোনটিতে ত্রুটি, কোন version-এ ঠিক হয়েছে, তা বলে। CI-তে (পর্ব ৩৯) প্রতিটি push-এ চালালে নতুন ঘোষিত ত্রুটিও দ্রুত চোখে পড়ে। (আউটপুটটি উদাহরণ; আসল ID আর version আপনার সময়ে ভিন্ন হবে।)
- আপডেটের প্রস্তাব। GitHub-এর Dependabot (বা Renovate) নতুন version বা নিরাপত্তা-সংশোধন এলে নিজেই একটি pull request খোলে, release notes-এর লিংকসহ। আপনাকে খুঁজতে হয় না, শুধু পর্যালোচনা করতে হয়।
- টেস্ট আগে, merge পরে। নতুন version-এ কিছু ভাঙল কি না, তা বলে আপনার টেস্ট। এখানেই pytest আর static টুলের মূল্য: টেস্ট না থাকলে প্রতিটি আপডেট ভয়ের, তাই মানুষ আপডেটই করে না — আর পুরোনো ত্রুটি থেকে যায়।
- নতুন pin, পরিষ্কার audit. Merge-এর পর তালিকায় নতুন version, audit আবার পরিষ্কার। চক্রটি চলতেই থাকে — নিরাপত্তা একবারের কাজ নয়, অভ্যাস।
কোডের জন্য নিরাপত্তা-linter
নিজের কোডের পরিচিত ঝুঁকিপূর্ণ নকশা ধরতে আছে static analysis টুল — এরা কোড না চালিয়ে পড়ে:
- bandit: শুরুতে OpenStack প্রজেক্টে তৈরি, এখন PyCQA দলের রক্ষণাবেক্ষণে।
eval,pickle.loads,shell=True, কোডে বসানো পাসওয়ার্ডের মতো দেখতে string,yaml.load,assert, MD5 — এমন নকশা খুঁজে সতর্ক করে। - ruff-এর
Sনিয়ম: পর্ব ৪৪-এ দেখা ruff-এ flake8-bandit-এর নিয়মগুলোSউপসর্গে আছে (যেমনS608— string দিয়ে বানানো SQL-এর সন্দেহ)।pyproject.toml-এselect-এ"S"যোগ করলেই প্রতিটি সংরক্ষণে পরীক্ষা। - CodeQL, Semgrep: আরও গভীর বিশ্লেষণ — বাইরের ডেটা কোন পথে গিয়ে বিপজ্জনক জায়গায় পৌঁছায়, তা অনুসরণ করে।
python -m pip install bandit pip-audit
bandit -r src/ # নিজের কোডে ঝুঁকিপূর্ণ নকশা
pip-audit # dependency-তে জানা ত্রুটি
ruff check --select S src/ # ruff-এর নিরাপত্তা-নিয়মএই টুলগুলো false positive দেয় — যেমন পরীক্ষার কোডে assert, বা এমন subprocess যেখানে কোনো বাইরের ডেটা নেই। ভেবে দেখে নিশ্চিত হলে সেই লাইনে # nosec (bandit) বা # noqa: S603 (ruff) দিয়ে কারণসহ চুপ করান — পুরো নিয়ম বন্ধ করবেন না। আর মনে রাখুন, টুল পরিষ্কার বললেই কোড নিরাপদ নয়; টুল শুধু চেনা নকশা ধরে, নকশার ভুল (যেমন ভুল জায়গায় অধিকার যাচাই) ধরে না।
৮ · Python-এর নিজস্ব ফাঁদ, error-এর ভাষা আর ছোট প্রজেক্টের চেকলিস্ট
assert দিয়ে নিরাপত্তা-যাচাই নয়
assert user.is_admin, "অনুমতি নেই" দেখতে যুক্তিসংগত। কিন্তু Python-কে -O flag দিয়ে (বা PYTHONOPTIMIZE environment variable দিয়ে) চালালে সব assert কোড থেকে বাদ পড়ে — যাচাইটিই আর থাকে না। assert হলো প্রোগ্রামারের নিজের ধারণা পরীক্ষার জন্য (পর্ব ৩৯-এর টেস্টে), নিরাপত্তার জন্য নয়। অধিকার যাচাই সবসময় সাধারণ if আর raise দিয়ে:
# ✗ python -O দিলে এই লাইন মুছে যায়
assert user.is_admin, "অনুমতি নেই"
# ✓ সবসময় চলে
if not user.is_admin:
raise PermissionError("এই কাজের অনুমতি নেই")Error বার্তা — ব্যবহারকারীকে সংক্ষেপে, log-এ বিস্তারিত
সুমাইয়ার সাইটের মূল সমস্যাটি মনে করুন: error পাতায় traceback, টেবিলের নাম, ফাইলের পথ। এগুলো ডিবাগে কাজের, কিন্তু বাইরের মানুষের কাছে আপনার সিস্টেমের মানচিত্র — এটি information disclosure. নিয়ম:
- আসল সার্ভারে debug mode বন্ধ (Flask-এ
debug=False, Django-তেDEBUG = False). Debug পাতা শুধু নিজের কম্পিউটারে। - ব্যবহারকারী পাবেন সাধারণ বার্তা আর একটি ছোট রেফারেন্স নম্বর: “কিছু একটা ভুল হয়েছে (রেফ: 7f3a)”. পুরো traceback যাবে log-এ (
logger.exception), সেই নম্বরসহ — যাতে আপনি খুঁজে পান। - লগইনে “এই ইউজার নেই” আর “পাসওয়ার্ড ভুল” আলাদা না বলে একটিই বার্তা: “ইউজারনেম বা পাসওয়ার্ড ভুল”। নইলে কোন অ্যাকাউন্ট আছে, তা বাইরে থেকে জানা যায়। সাথে বারবার ভুল চেষ্টায় গতি কমানো (rate limiting).
- সব exception চুপচাপ গিলে ফেলাও (
except: pass) সমাধান নয় — তাহলে আপনি নিজেই সমস্যা দেখবেন না (পর্ব ২৪)।
আরও কয়েকটি Python-নির্দিষ্ট অভ্যাস
- অস্থায়ী ফাইল:
tempfile.NamedTemporaryFile(),tempfile.mkstemp()বাTemporaryDirectory()— এগুলো অনুমান-অযোগ্য নামে, শুধু আপনার পড়ার-লেখার অনুমতিসহ ফাইল বানায়। নিজে/tmp/myapp.txt-এর মতো নির্দিষ্ট নাম ব্যবহার, বা বহু আগে বাতিল ঘোষিতtempfile.mktemp()নয়। - ফাইলের অনুমতি: secret-যুক্ত ফাইল (যেমন
.env) Linux সার্ভারে শুধু মালিক পড়তে পারুক (chmod 600). প্রোগ্রাম root বা admin হিসেবে চালাবেন না — least privilege. - বাইরের অনুরোধে timeout: নেটওয়ার্ক অনুরোধে (পর্ব ৪৭) সবসময়
timeout=দিন, আর আপলোড/ইনপুটের আকারে সীমা — নইলে একটি ধীর বা বিশাল অনুরোধেই প্রোগ্রাম আটকে যেতে পারে। - ব্যবহারকারীর দেওয়া URL: আপনার সার্ভার ব্যবহারকারীর দেওয়া যেকোনো URL থেকে ডেটা আনলে সেটি ভেতরের নেটওয়ার্কের ঠিকানাও হতে পারে। শুধু অনুমোদিত ডোমেইনের allow-list থেকে আনুন।
- SQL injection কীভাবে ঠেকাবেন? parameterized query —
?placeholder আর মান আলাদা tuple-এ; কখনো f-string দিয়ে SQL নয়। - পাসওয়ার্ড কেন SHA-256 দিয়ে hash করা যথেষ্ট নয়? এটি খুব দ্রুত, তাই অনুমান মেলানো সস্তা; দরকার random salt আর ধীর KDF (Argon2, bcrypt, scrypt).
randomআরsecrets-এর পার্থক্য? random অনুমানযোগ্য, সিমুলেশন ও খেলার জন্য; secrets OS-এর নিরাপদ উৎস থেকে, token ও পাসওয়ার্ডের জন্য।eval-এর নিরাপদ বিকল্প? সরল literal-এর জন্যast.literal_eval, ডেটার জন্যjson.loads, সংখ্যার জন্যint()/float().- encoding, encryption আর hashing-এর পার্থক্য? encoding শুধু রূপ বদল (key ছাড়াই উল্টো করা যায়), encryption key দিয়ে উল্টো করা যায়, hashing একমুখী।
ছোট প্রজেক্টের নিরাপত্তা-চেকলিস্ট
বড় প্রতিষ্ঠানের নিরাপত্তা-দল আছে; আপনার ছোট প্রজেক্ট বা ক্যাপস্টোনের জন্য এই দশটি প্রশ্নই যথেষ্ট শুরু:
৯ · AI-এর যুগে — AI-এর লেখা কোডে নিরাপত্তার ফাঁক খোঁজা
AI খুব দ্রুত “কাজ করে” এমন কোড দেয়। কিন্তু AI শিখেছে ইন্টারনেটের বহু বছরের কোড থেকে, যার বড় অংশ টিউটোরিয়াল আর ছোট উদাহরণ — যেখানে নিরাপত্তা প্রায়ই বাদ দেওয়া হয় “সরলতার জন্য”। তাই AI-এর কোডে কিছু নির্দিষ্ট ভুল বারবার দেখা যায়। এগুলো চোখে পড়লেই থামুন:
| AI-এর কোডে যা দেখবেন | সমস্যা | নিরাপদ রূপ |
|---|---|---|
f"SELECT ... WHERE id = {user_id}" | SQL injection-এর দরজা | execute("... WHERE id = ?", (user_id,)) |
API_KEY = "sk-..." কোডের ভেতরে | git-এ গেলেই ফাঁস | os.environ + .env + .gitignore |
eval(input(...)) | ব্যবহারকারী যেকোনো কোড চালাতে পারেন | int(), ast.literal_eval, json |
নেটওয়ার্ক থেকে আসা ডেটায় pickle.loads | অচেনা কোড চলতে পারে | json |
hashlib.md5(password) | দ্রুত, salt নেই | Argon2/bcrypt library, বা salt-সহ scrypt |
assert user.is_admin | -O-এ মুছে যায় | if not ...: raise PermissionError |
subprocess.run(f"... {name}", shell=True) | command injection | list argument, shell=False |
random.randint দিয়ে OTP বা token | অনুমানযোগ্য | secrets |
requests.get(url, verify=False) | সংযোগের পরিচয় যাচাই বন্ধ | verify চালু রাখা, সাথে timeout |
AI-কে নিরাপত্তা-রিভিউয়ের সহকারী হিসেবেও ব্যবহার করা যায় — অনেক সময় বেশ ভালো কাজ করে। কিন্তু রিভিউও AI-এর আরেকটি উত্তর: কিছু বাদ পড়তে পারে, কিছু ভুল সতর্কতাও আসতে পারে। তাই নিয়ম হলো AI-এর রিভিউ + টুলের যাচাই + নিজের বোঝা. প্রম্পটে কাঠামো দিন, যাতে উত্তর যাচাইযোগ্য হয়:
নিজের লেখা বা AI-এর দেওয়া কোডের নিরাপত্তা-রিভিউ চাইতে এমন একটি প্রম্পট দিন:
AI-এর রিভিউয়ের পরে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো নিরাপত্তার “কী করব” থেকে “কেন এভাবে” পর্যন্ত নিয়ে যাবে।
প্র ০১OWASP Top 10 বিশ বছরের বেশি সময় ধরে প্রকাশ হচ্ছে — তবু injection কেন বারবার তালিকার ওপরের দিকে থাকে?
OWASP একটি অলাভজনক, উন্মুক্ত সম্প্রদায়, ২০০১ সালে শুরু। ২০০৩ সালে তারা প্রথম “Top 10” প্রকাশ করে — ওয়েব অ্যাপের সবচেয়ে গুরুতর ও সাধারণ দশটি ঝুঁকির একটি সচেতনতা-তালিকা। পরে ২০০৪, ২০০৭, ২০১০, ২০১৩, ২০১৭, ২০২১ আর ২০২৫ সালে নতুন সংস্করণ আসে, প্রতিবার আরও বেশি বাস্তব তথ্যের ভিত্তিতে। ২০১০, ২০১৩ আর ২০১৭-তে injection ছিল এক নম্বরে। ২০২১-এর সংস্করণে এটি তৃতীয় স্থানে নামে — আর সেখানে cross-site scripting-কেও injection-এর অংশ ধরা হয়; ২০২৫-এর সংস্করণেও এটি প্রথম পাঁচের মধ্যে আছে। অর্থাৎ নাম আর ক্রম বদলালেও মূল সমস্যাটি দুই দশক ধরে শীর্ষের কাছাকাছি।
কেন? প্রথম কারণ গঠনগত। সফটওয়্যার প্রতিনিয়ত এক ভাষায় অন্য ভাষার লেখা বানায়: Python থেকে SQL, Python থেকে shell কমান্ড, server থেকে HTML, JavaScript, LDAP query, log-লাইন। যতদিন মানুষ এই লেখাগুলো string জোড়া দিয়ে বানাবে, ততদিন ডেটা আর নির্দেশ মিশে যাওয়ার সুযোগ থাকবে। নতুন প্রযুক্তি এলে নতুন “ভাষা”ও আসে — যেমন আজকের LLM-ভিত্তিক অ্যাপে prompt injection, যেখানে বাইরের লেখা মডেলের নির্দেশ হয়ে উঠতে পারে। সমস্যার রূপ বদলায়, মূল একই থাকে।
দ্বিতীয় কারণ মানবিক। string জোড়া দেওয়ার পথটি সবচেয়ে সহজ ও স্বাভাবিক মনে হয়, আর সাধারণ পরীক্ষায় সেটি দিব্যি চলে — কারণ সাধারণ ব্যবহারকারী বিশেষ চিহ্ন দেন না। ভুলটি দেখা যায় না যতক্ষণ না কেউ খোঁজে। পুরোনো টিউটোরিয়াল আর কপি-পেস্ট সেই অভ্যাস ছড়িয়ে দেয়, আর আজ AI-ও সেই উদাহরণ থেকেই শেখে।
তৃতীয় কারণ পুরোনো কোড। নতুন ফ্রেমওয়ার্কগুলো ডিফল্টভাবে নিরাপদ পথ দেয় — ORM, parameterized query, autoescape template — আর তাতে ঘটনা অনেক কমেছে। কিন্তু পৃথিবীতে বিপুল পরিমাণ পুরোনো কোড এখনো চলছে, আর নতুন কোডেও কেউ “একটু দ্রুত” কাঁচা SQL লিখে ফেলেন।
এখান থেকে প্রকৌশলের একটি গভীর শিক্ষা: সমস্যাটি সবচেয়ে ভালোভাবে কমেছে মানুষকে “সাবধান হও” বলে নয়, নিরাপদ পথকে সবচেয়ে সহজ পথ বানিয়ে। যে API-তে ভুল করা কঠিন, সেটিই সবচেয়ে কার্যকর সুরক্ষা।
মূল উপলব্ধি: যেখানেই এক ভাষার ভেতরে অন্য ভাষার লেখা বানানো হয়, সেখানেই injection-এর ঝুঁকি। প্রতিরোধের স্থায়ী উপায় হলো এমন যন্ত্র ব্যবহার করা, যা ডেটা আর নির্দেশকে গঠনগতভাবেই আলাদা রাখে।
প্র ০২কম্পিউটার যেখানে সবকিছু দ্রুত করতে চায়, সেখানে পাসওয়ার্ডের hash ইচ্ছা করে ধীর বানানো হয় কেন? bcrypt, scrypt আর Argon2 কীভাবে এল?
একটি ভাবনা-পরীক্ষা দিয়ে শুরু করি। ধরুন কোনো সাইটের database ফাঁস হলো, আর সেখানে পাসওয়ার্ডের SHA-256 hash আছে। আধুনিক GPU সেকেন্ডে শত কোটি SHA-256 হিসাব করতে পারে। মানুষের বেছে নেওয়া পাসওয়ার্ডের বড় অংশ কয়েকটি পরিচিত নকশায় পড়ে — নাম, জন্মসাল, “123”, “@”। ফলে দ্রুত hash-এ সাধারণ পাসওয়ার্ডগুলো খুব অল্প সময়ে বেরিয়ে আসে। এখানে গতিই শত্রু। আসল ব্যবহারকারী দিনে একবার লগইন করেন; তাঁর জন্য ১০০ মিলিসেকেন্ড অদৃশ্য। কিন্তু কোটি অনুমানের জন্য সেই ১০০ মিলিসেকেন্ড গুণ হয়ে বিশাল সময় আর খরচ হয়ে দাঁড়ায়।
পুরোনো পথ: Unix-এর পুরোনো crypt (১৯৭০-এর দশক) salt ব্যবহার করত আর ইচ্ছা করে কয়েকবার হিসাব চালাত — ধারণাটি নতুন নয়। ২০০০ সালে PKCS #5 মানকে (RFC 2898) PBKDF2 আসে: একটি hash হাজার হাজার বা লাখো বার পরপর চালানো, iteration-সংখ্যা বাড়িয়ে খরচ বাড়ানো যায়।
bcrypt (১৯৯৯): Niels Provos ও David Mazières USENIX সম্মেলনে bcrypt উপস্থাপন করেন, যা Blowfish সাইফারের ধীর key-প্রস্তুতির ওপর দাঁড়ানো। এর বড় অবদান একটি “cost” প্যারামিটার: হার্ডওয়্যার দ্রুত হলে cost এক ধাপ বাড়ালে খরচ দ্বিগুণ। পঁচিশ বছরের বেশি পরেও এটি ব্যাপক ব্যবহৃত।
scrypt (২০০৯): Colin Percival তাঁর Tarsnap ব্যাকআপ সেবার জন্য scrypt বানান। তিনি লক্ষ করেন, শুধু সময়-খরচ যথেষ্ট নয় — GPU বা বিশেষ চিপে হাজারো ছোট হিসাব একসাথে সস্তায় চালানো যায়। তাই scrypt প্রতিটি হিসাবে প্রচুর মেমরিও চায়; মেমরি সমান্তরালভাবে বাড়ানো ব্যয়বহুল। একে বলে memory-hard ফাংশন।
Argon2 (২০১৫): এরপর একটি উন্মুক্ত প্রতিযোগিতা হয় — Password Hashing Competition (২০১৩–২০১৫), অনেকটা AES আর SHA-3 নির্বাচনের ধাঁচে। বহু প্রস্তাবের মধ্যে বিজয়ী হয় লুক্সেমবার্গ বিশ্ববিদ্যালয়ের Alex Biryukov, Daniel Dinu ও Dmitry Khovratovich-এর Argon2. এতে সময়, মেমরি আর সমান্তরালতা — তিনটিই আলাদাভাবে ঠিক করা যায়। পরে এটি RFC 9106 হিসেবে মানক হয়, আর Argon2id রূপটি আজ সাধারণভাবে প্রথম পছন্দ।
মূল উপলব্ধি: নিরাপত্তায় “দক্ষতা” মাপতে হয় আক্রমণকারীর খরচ দিয়ে। পাসওয়ার্ড-hash ধীর ও মেমরি-খেকো হয় যাতে আসল ব্যবহারকারীর জন্য খরচ নগণ্য, কিন্তু অনুমান-যন্ত্রের জন্য অসহনীয় থাকে — আর প্যারামিটার বাড়িয়ে ভবিষ্যতের দ্রুত হার্ডওয়্যারের সাথে তাল রাখা যায়।
প্র ০৩“নিজের ক্রিপ্টো নিজে বানাবেন না” — Kerckhoffs-এর নীতি কী, আর একজন Python প্রোগ্রামারের কাছে এর মানে কী?
১৮৮৩ সালে ডাচ বংশোদ্ভূত ভাষাবিদ ও ক্রিপ্টোগ্রাফার Auguste Kerckhoffs ফরাসি সামরিক বাহিনীর জন্য লেখা “La cryptographie militaire” প্রবন্ধে কয়েকটি নীতি দেন। এর মধ্যে বিখ্যাতটি আজ Kerckhoffs-এর নীতি নামে পরিচিত: একটি গোপন-লিখন ব্যবস্থা এমন হওয়া উচিত যে ব্যবস্থাটি শত্রুর হাতে পড়লেও কোনো ক্ষতি নেই — গোপন থাকবে শুধু key. পরে Claude Shannon প্রায় একই কথা বলেন: “শত্রু ব্যবস্থাটি জানে”।
এর বিপরীত চিন্তার নাম “security through obscurity” — অ্যালগরিদম গোপন রাখলেই নিরাপদ। সমস্যা হলো, গোপন অ্যালগরিদম কোনো একদিন ফাঁস হয় — কোড decompile করে, কর্মী চাকরি বদলে, বা নিছক বিশ্লেষণে। তখন পুরো ব্যবস্থা একসাথে ভেঙে পড়ে, আর বদলানো যায় না। অন্যদিকে key ফাঁস হলে শুধু key বদলালেই চলে। তাই AES, SHA-256, Argon2 — আধুনিক সব মানক অ্যালগরিদম সম্পূর্ণ প্রকাশ্য, বছরের পর বছর বিশ্বের গবেষকেরা ভাঙার চেষ্টা করেছেন, আর সেই প্রকাশ্য পরীক্ষাই তাদের বিশ্বাসযোগ্যতার ভিত্তি।
“নিজের ক্রিপ্টো বানাবেন না” কথাটি এখান থেকেই। নতুন অ্যালগরিদম বানানো তো দূরের কথা, পরিচিত অ্যালগরিদম সঠিকভাবে জোড়া দেওয়াও কঠিন: ভুল mode, পুনর্ব্যবহৃত nonce, যাচাই না করা ciphertext, == দিয়ে তুলনা, দুর্বল random উৎস — প্রতিটিই পুরো ব্যবস্থাকে অকেজো করতে পারে, আর পরীক্ষায় সব “ঠিকঠাক” দেখায়। ক্রিপ্টোগ্রাফির ভুল সাধারণ বাগের মতো error দেয় না; চুপচাপ দুর্বল থাকে।
Python প্রোগ্রামারের জন্য এর ব্যবহারিক অর্থ: পাসওয়ার্ডের জন্য argon2-cffi, bcrypt বা ফ্রেমওয়ার্কের নিজস্ব ব্যবস্থা; এনক্রিপশনের জন্য cryptography package-এর উচ্চ-স্তরের API (যেমন Fernet), যা সঠিক জোড়া নিজেই করে; random-এর জন্য secrets; তুলনায় hmac.compare_digest; আর সংযোগে TLS-এর যাচাই চালু রাখা। base64 বা XOR দিয়ে “লুকানো” এনক্রিপশন নয়। নিজের পরিবর্তন যোগ করে “আরও শক্ত” বানানোর চেষ্টাও নয় — প্রায়ই তা উল্টো ফল দেয়।
মূল উপলব্ধি: নিরাপত্তা আসে প্রকাশ্যে পরীক্ষিত নকশা আর গোপন key থেকে, গোপন নকশা থেকে নয়। আপনার কাজ নতুন তালা বানানো নয় — পরীক্ষিত তালা সঠিকভাবে লাগানো আর চাবি সামলে রাখা।
প্র ০৪Python নিজে কীভাবে নিরাপত্তা-সমস্যা সামলায় — PSRT, CVE, আর ২০১২ সালের hash randomization থেকে কী শেখার আছে?
Python-এর মতো বিশাল প্রকল্পেও নিরাপত্তা-ত্রুটি ধরা পড়ে — standard library-র কোনো parser-এ, কোনো দুর্লভ ইনপুটে। প্রশ্ন হলো, সেগুলো কীভাবে সামলানো হয়। এজন্য আছে Python Security Response Team (PSRT) — অভিজ্ঞ core developer-দের একটি ছোট দল। কেউ ত্রুটি খুঁজে পেলে প্রকাশ্য issue-তে না লিখে গোপনে জানান (Python-এর নিরাপত্তা-নীতির পাতায় পথ দেওয়া আছে)। দলটি সমস্যা যাচাই করে, সংশোধন তৈরি করে, আর সমর্থিত সব version-এ একসাথে প্রকাশ করে — যাতে সংশোধন আসার আগেই ত্রুটির খবর ছড়িয়ে না পড়ে। একে বলে দায়িত্বশীল প্রকাশ (coordinated disclosure).
প্রকাশিত ত্রুটি একটি CVE নম্বর পায়, যাতে সারা পৃথিবীর টুল আর মানুষ একই সমস্যাকে একই নামে চেনে। ২০২৩ সালে Python Software Foundation নিজেই একটি CVE Numbering Authority হয়, অর্থাৎ Python ও pip-এর ত্রুটির CVE নিজেরাই দিতে পারে। একই বছর PSF একজন পূর্ণকালীন Security Developer-in-Residence নিয়োগ দেয়। প্রতিটি Python version কয়েক বছর নিরাপত্তা-সংশোধন পায়; তারপর “end of life”, আর কোনো সংশোধন নয় — এজন্যই সমর্থিত version ব্যবহার জরুরি।
Hash randomization-এর গল্প: ২০১১ সালের শেষে গবেষক Alexander Klink ও Julian Wälde একটি সম্মেলনে দেখান, অনেক ভাষার hash table-এ (PHP, Java, Python, Ruby…) একটি নকশাগত দুর্বলতা আছে: string-এর hash যেহেতু সবসময় একই, তাই কেউ আগে থেকে হিসাব করে একই ঘরে পড়া অনেক চাবি পাঠালে dict-এর কাজ খুব ধীর হয়ে যায়, আর একটি অনুরোধেই সার্ভারের CPU আটকে যেতে পারে। এটি denial of service — তথ্য চুরি নয়, সেবা অচল।
Python-এর প্রতিক্রিয়া ছিল সতর্ক। ২০১২ সালে পুরোনো version-গুলোতে (2.6.8, 2.7.3, 3.1.5, 3.2.3) একটি ঐচ্ছিক -R option আসে, কারণ হঠাৎ ডিফল্ট বদলালে dict-এর ক্রমের ওপর নির্ভরশীল পুরোনো কোড ভাঙত। Python 3.3-এ (২০১২) এটি ডিফল্টভাবে চালু হয়: প্রতিবার প্রোগ্রাম শুরুতে একটি গোপন random বীজ hash-এ মেশে। পরে PEP 456 (Python 3.4) hash-এর জন্য SipHash বেছে নেয়, যা এই কাজের জন্যই নকশা করা। এই কারণেই set-এর ছাপার ক্রম প্রতিবার আলাদা হতে পারে, আর PYTHONHASHSEED দিয়ে পরীক্ষার জন্য নির্দিষ্ট করা যায়।
মূল উপলব্ধি: নিরাপত্তা শুধু কোড নয়, প্রক্রিয়াও — গোপনে জানানোর পথ, সমন্বিত সংশোধন, সাধারণ নাম (CVE) আর পুরোনো কোডের সামঞ্জস্য রেখে ধীরে ধীরে ডিফল্ট বদলানো। আর সবচেয়ে কার্যকর সমাধান প্রায়ই সেটি, যা ব্যবহারকারীকে কিছু না করতে দিয়েই সবাইকে রক্ষা করে।
প্র ০৫Package ecosystem-এর supply-chain ঘটনাগুলো থেকে কী শেখা গেল, আর trusted publishing-এর মতো ব্যবস্থা কোন সমস্যার উত্তর?
আধুনিক সফটওয়্যার একটি দীর্ঘ সরবরাহ-শৃঙ্খলের শেষ মাথা: আপনার কোড দাঁড়িয়ে আছে শত শত package-এর ওপর, সেগুলো তৈরি হয় অন্য কারও কম্পিউটারে বা CI-তে, আসে একটি ভাণ্ডার (PyPI, npm) থেকে। এই শৃঙ্খলের যেকোনো একটি কড়ি দুর্বল হলে নিচের সবাই ঝুঁকিতে। গত এক দশকে বিভিন্ন ecosystem-এ কয়েক ধরনের ঘটনা বারবার দেখা গেছে — এখানে শুধু তাদের শ্রেণি আর শিক্ষাটুকু।
- বানানের ফাঁদ: জনপ্রিয় নামের কাছাকাছি নামে ক্ষতিকর package (পর্ব ২৩-এ typosquatting). শিক্ষা: নাম যাচাই, কম dependency.
- রক্ষণাবেক্ষণকারীর অ্যাকাউন্ট দখল: দুর্বল বা পুনর্ব্যবহৃত পাসওয়ার্ড, বা মেয়াদোত্তীর্ণ ইমেইল-ডোমেইনের মাধ্যমে আসল package-এর অ্যাকাউন্ট অন্যের হাতে যাওয়ার ঘটনা ঘটেছে — তখন বিশ্বস্ত নামেই খারাপ version আসে। শিক্ষা: 2FA আর pin.
- বিশ্বাসের হাতবদল: পরিত্যক্ত প্রকল্পের দায়িত্ব নতুন কেউ নিয়ে পরে ক্ষতিকর বদল আনা — npm-এ ২০১৮ সালের event-stream ঘটনা পরিচিত উদাহরণ; ২০২৪ সালে xz-utils-এর ঘটনা দেখায়, বছরের পর বছর ধৈর্য নিয়েও এমন চেষ্টা হতে পারে, আর একজন প্রকৌশলীর সতর্ক চোখেই সেটি সময়মতো ধরা পড়ে।
- Build বা CI-র ফাঁক: প্রকাশের জন্য রাখা দীর্ঘস্থায়ী token ফাঁস হলে যে কেউ প্রকাশকের নামে আপলোড করতে পারে।
ভাণ্ডারের প্রতিক্রিয়া: PyPI ২০২২ সালে গুরুত্বপূর্ণ প্রকল্পের জন্য, আর ২০২৪ সালের শুরু থেকে সব আপলোডকারী অ্যাকাউন্টের জন্য two-factor authentication বাধ্যতামূলক করে। ২০২৩ সালে আসে trusted publishing: প্রকাশের জন্য কোথাও স্থায়ী token রাখতে হয় না; GitHub Actions-এর মতো CI নির্দিষ্ট repository আর workflow থেকে চলছে, তা প্রমাণ করে কয়েক মিনিটের একটি token পায়। চুরি করার মতো দীর্ঘস্থায়ী চাবিই থাকে না। এরপর PEP 740-এর মাধ্যমে প্রকাশিত ফাইলের সাথে যাচাইযোগ্য উৎস-প্রমাণ (attestation) যুক্ত করার পথ তৈরি হয়েছে — ফাইলটি কোন repository আর কোন build থেকে এসেছে, তা মিলিয়ে দেখা যায়।
আপনার ভূমিকা: প্রকাশক হলে trusted publishing আর 2FA; ব্যবহারকারী হলে নাম যাচাই, pin, hash-সহ lock file, pip-audit, আর নতুন dependency যোগের আগে “এটা কি সত্যিই দরকার?” প্রশ্ন।
মূল উপলব্ধি: supply chain-এর নিরাপত্তা এক স্তরের কাজ নয় — ভাণ্ডার, প্রকাশক, টুল আর ব্যবহারকারী, সবার স্তরে বিশ্বাসকে যাচাইযোগ্য করে তোলাই লক্ষ্য। “আমি package-টি চিনি” থেকে “আমি প্রমাণ মিলিয়ে দেখেছি” — এটিই দিক।
অনুশীলন
বেশিরভাগ কাজ ভাবনা ও রিভিউয়ের। নিজে উত্তর লিখে তারপর মিলিয়ে নিন।
-
AI-এর কোড রিভিউ: একটি AI সহকারী এই লগইন-কোড দিয়েছে। অন্তত পাঁচটি নিরাপত্তা-সমস্যা খুঁজুন আর প্রতিটির সমাধান এক লাইনে লিখুন:
DB_PASSWORD = "admin123" def login(username, password): row = db.execute(f"SELECT pw FROM users WHERE name = '{username}'").fetchone() assert row, "user not found" if hashlib.md5(password.encode()).hexdigest() == row[0]: logger.info(f"login ok {username} {password}") return True- কোডে secret (
DB_PASSWORD) → environment variable,.envgit-এর বাইরে; ইতিমধ্যে git-এ গিয়ে থাকলে পাসওয়ার্ড বদলান। - f-string-এ SQL →
execute("... WHERE name = ?", (username,)). assertদিয়ে যাচাই →-O-এ মুছে যায়;if row is None: return False.- MD5, salt নেই → argon2/bcrypt library, বা salt-সহ scrypt.
==দিয়ে hash তুলনা →hmac.compare_digest.- log-এ পাসওয়ার্ড → পাসওয়ার্ড কখনো log নয়; শুধু ইউজারনেম আর ফল।
বাড়তি: ব্যবহারকারী না থাকলে আর পাসওয়ার্ড ভুল হলে একই বার্তা দিন; বারবার ভুল চেষ্টায় rate limiting; আর ফাংশনটি ব্যর্থতায় স্পষ্টভাবে
Falseফেরত দিক (এখন ভুল পাসওয়ার্ডেNoneআসে)। - কোডে secret (
-
কোনটি কোন কাজের: প্রতিটি ক্ষেত্রে বলুন hashing, encryption, encoding — কোনটি লাগবে, আর কেন। (ক) শিক্ষকদের লগইন-পাসওয়ার্ড সংরক্ষণ; (খ) ছবি JSON-এর ভেতরে পাঠানো; (গ) অভিভাবকের ফোন নম্বর সংরক্ষণ, যাতে পরে SMS পাঠানো যায়; (ঘ) নামানো ফাইল পথে বদলেছে কি না যাচাই।
- (ক) Hashing — ধীর, salt-সহ (Argon2/bcrypt/scrypt). পাসওয়ার্ড কখনো ফেরত পড়ার দরকার নেই, শুধু মেলাতে হয়।
- (খ) Encoding — base64. JSON-এ byte রাখা যায় না, তাই লেখায় রূপান্তর। এতে কোনো গোপনীয়তা নেই।
- (গ) Encryption — পরে আসল নম্বর লাগবে, তাই একমুখী hash চলবে না। key রাখুন কোড আর database-এর বাইরে।
- (ঘ) Hashing — দ্রুত cryptographic hash (SHA-256) এখানে ঠিক আছে; ফাইলের ছাপ মিলিয়ে দেখা। এখানে ধীর hash-এর দরকার নেই, কারণ গোপন কিছু অনুমান করা হচ্ছে না।
-
নিজে লিখুন — মোবাইল নম্বরের allow-list:
validate_mobile(text)ফাংশন লিখুন, যা বাংলাদেশের মোবাইল নম্বর গ্রহণ করবে:01দিয়ে শুরু, তৃতীয় অঙ্ক 3 থেকে 9, মোট ১১টি ASCII অঙ্ক; শুরুর বা শেষের ফাঁকা জায়গা বাদ দেবে, আর বৈধ না হলেValueErrorদেবে।"01712345678"," 01912345678 ","01212345678","০১৭১২৩৪৫৬৭৮"দিয়ে পরীক্ষা করুন।import re MOBILE = re.compile(r"01[3-9][0-9]{8}") def validate_mobile(text: str) -> str: number = text.strip() if len(number) != 11 or not MOBILE.fullmatch(number): raise ValueError("সঠিক মোবাইল নম্বর দিন (যেমন 017XXXXXXXX)") return numberপ্রথম দুটি বৈধ (দ্বিতীয়টি
strip()-এর পরে)। তৃতীয়টি বাতিল — তৃতীয় অঙ্ক 2. চতুর্থটি বাতিল, কারণ[0-9]শুধু ASCII অঙ্ক মেলায় (\dলিখলে বাংলা অঙ্কও মিলে যেত)। বাংলা অঙ্ক গ্রহণ করতে চাইলে আগে সচেতনভাবে রূপান্তর করুন, তারপর একই যাচাই।fullmatchজরুরি —matchশেষে বাড়তি অক্ষর থাকলেও মেনে নিত। আগে দৈর্ঘ্য পরীক্ষা খুব লম্বা ইনপুটে regex চালানোই এড়ায়। -
Key ফাঁস হয়ে গেছে: রাফি ভুল করে SMS সেবার API key-সহ
config.pyএকটি পাবলিক GitHub repository-তে push করেছেন, এক ঘণ্টা পরে টের পেলেন। কী কী করবেন, সঠিক ক্রমে? আর ভবিষ্যতে এড়াতে কোন দুটি ব্যবস্থা নেবেন?- সঙ্গে সঙ্গে key বাতিল (revoke) করে নতুন key তৈরি — সেবার ড্যাশবোর্ড থেকে। এটিই সবচেয়ে জরুরি, কারণ পাবলিক repository দ্রুত স্ক্যান হয়; এক ঘণ্টা অনেক সময়।
- নতুন key environment variable/
.env-এ বসানো, কোড বদলেos.environথেকে পড়া;.envযোগ.gitignore-এ। - সেবার ব্যবহারের ইতিহাস ও বিল দেখা — ওই সময়ে অচেনা ব্যবহার হয়েছে কি না; হলে সেবাদাতাকে জানানো।
- git ইতিহাস থেকে key সরানো (
git filter-repoইত্যাদি) — তবে মনে রাখুন, এটি key বাতিলের বিকল্প নয়; কেউ আগেই কপি করে থাকতে পারে।
ভবিষ্যতের জন্য: GitHub-এর secret scanning ও push protection চালু রাখা, আর commit-এর আগে gitleaks বা detect-secrets-এর মতো টুল (pre-commit hook). সাথে key-কে ন্যূনতম অধিকার আর খরচের সীমা দেওয়া, যাতে ফাঁস হলেও ক্ষতি সীমিত থাকে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি ওয়েব অ্যাপে ব্যবহারকারীর পাসওয়ার্ড নিরাপদে সংরক্ষণ আর পরে যাচাইয়ের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- নিবন্ধনে পাসওয়ার্ড নেওয়া আর দৈর্ঘ্য যাচাই করা
secrets.token_bytes(16)দিয়ে নতুন random salt বানানো- পাসওয়ার্ড আর salt দিয়ে ধীর hash (যেমন scrypt) হিসাব করা
- salt, প্যারামিটার আর hash database-এ রাখা
- লগইনে সংরক্ষিত salt দিয়ে আবার hash করে
hmac.compare_digest-এ তুলনা করা
Python-এ sqlite3 দিয়ে ব্যবহারকারীর দেওয়া রোল নম্বর অনুযায়ী ডেটা খোঁজার সবচেয়ে নিরাপদ উপায় কোনটি?
db.execute(f"SELECT * FROM result WHERE roll = '{roll}'")db.execute("SELECT * FROM result WHERE roll = ?", (roll,))roll.replace("'", "")করে তারপর f-string দিয়ে SQL বানানোdb.execute("SELECT * FROM result WHERE roll = '%s'" % roll)
ব্যবহারকারীর দেওয়া "[1, 2, 3]" ধরনের লেখাকে Python list-এ রূপান্তরের নিরাপদ উপায় কোনটি?
eval(text)exec("result = " + text)ast.literal_eval(text)pickle.loads(text.encode())
একটি ডাউনলোড ফাংশনে ব্যবহারকারীর দেওয়া ফাইলের নাম অনুমোদিত ফোল্ডার BASE-এর ভেতরে আছে কি না যাচাইয়ের সঠিক উপায় কোনটি?
(BASE / name).resolve().is_relative_to(BASE.resolve())str(BASE / name).startswith(str(BASE))"../" not in name(BASE / name).exists()
পাসওয়ার্ড-রিসেটের লিংকের জন্য অনুমান-অযোগ্য token বানাতে Python-এর কোনটি ব্যবহার করা উচিত?
random.randintদিয়ে বড় সংখ্যা- বর্তমান সময়ের
hashlib.md5 - ব্যবহারকারীর id-কে base64 করা
secrets.token_urlsafe(32)
Python কোডে assert user.is_admin দিয়ে অধিকার যাচাই করা কেন বিপজ্জনক?
- assert খুব ধীর, তাই সার্ভার ধীর হয়ে যায়
- Python
-Oদিয়ে চালালে assert বাদ পড়ে, ফলে যাচাইটিই থাকে না - assert ব্যর্থ হলে কোনো exception আসে না
- assert-এ object-এর attribute ব্যবহার করা যায় না
একটি API key ভুল করে পাবলিক git repository-তে push হয়ে গেছে। সবচেয়ে জরুরি প্রথম কাজ কোনটি?
- পরের commit-এ লাইনটি মুছে দেওয়া
- repository-টি private করে দেওয়া
- সেবার ড্যাশবোর্ড থেকে key বাতিল করে নতুন key তৈরি করা
- key-কে base64 করে আবার push করা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Trust boundary-তে যাচাই: বাইরের সব ডেটা allow-list, ধরন, সীমা আর দৈর্ঘ্য দিয়ে দরজায় পরীক্ষা; প্রতিটি অংশ ন্যূনতম অধিকারে।
- ডেটা কখনো নির্দেশ নয়: SQL-এ
?, subprocess-এ list,eval-এর বদলেliteral_eval/json, HTML-এ escape, path-এresolve()+is_relative_to(), বাইরের ডেটায় pickle নয়। - Secret কোড, git আর log-এর বাইরে: environment variable আর
.env(.gitignore-এ); ফাঁস হলে আগে key বাতিল; token বানাতেsecrets. - পাসওয়ার্ড = random salt + ধীর hash: Argon2/bcrypt/scrypt, তুলনা
compare_digest-এ। base64 এনক্রিপশন নয়, নিজের ক্রিপ্টো নয়। - যন্ত্রের চোখ নিয়মিত: pin, pip-audit, bandit/ruff S, Dependabot;
assertদিয়ে নিরাপত্তা নয়; ব্যবহারকারীকে সংক্ষিপ্ত error, log-এ বিস্তারিত।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫১ · ক্যাপস্টোন — একটা আসল Python সিস্টেমপরবর্তী পর্বপুরো কোর্সের জ্ঞান এক প্রজেক্টে — নকশা, টেস্ট, নিরাপত্তা আর প্রকাশ।
- Python প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- C++ প্রোগ্রামিং · UB ও নিরাপদ C++তুলনা করুনমেমরির দিক থেকে নিরাপত্তা — undefined behavior, sanitizer আর নিরাপদ অভ্যাস।
- C প্রোগ্রামিং · নিরাপদ কোডিংগভীরে যানbuffer overflow থেকে fuzzing — নিচের স্তরে নিরাপত্তা কেমন দেখায়।