Injection Attacks — কোড আর ডেটা যখন একই স্ট্রিং
Injection Attacks
SQL injection, command injection, আর SSTI — তিনটাই আসলে একই ভুলের তিনটা পোশাক: untrusted ডেটাকে সরাসরি একটা interpreter-এর CODE/SYNTAX string-এ বসিয়ে দেওয়া; সমাধানও একটাই — ডেটা আর syntax-কে কখনো একই চ্যানেলে মিশতে না দেওয়া।
আগে এটা বুঝি
আগের লেসনে authorization-এর প্রশ্ন ছিল “এই user-টা কি এই কাজ করার অনুমতিপ্রাপ্ত”। আজকের প্রশ্নটা আরও গোড়ার — “যা user পাঠিয়েছে, সিস্টেম সেটাকে ঠিক কী হিসেবে পড়ছে”। আর উত্তরটা যখন ভুল হয়, তখনই জন্ম নেয় ইতিহাসের সবচেয়ে দীর্ঘজীবী, সবচেয়ে ব্যাপক আক্রমণ-শ্রেণি: injection।
ধরুন একটা লগইন ফর্ম। ব্যাকএন্ড ডেভেলপার একটা প্রশ্ন বানাতে চান ডাটাবেসকে — “এই username আর password-এর মিল আছে কিনা দেখাও”। স্বাভাবিক তাড়াহুড়ায় যা লেখা হয়:
query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"এই এক লাইনেই বিপদটা লুকানো — আর সেটা কোনো syntax error না, কোনো typo না।
সমস্যাটা ধারণাগত: username ভেরিয়েবলটা আসলে ডেটা হওয়ার কথা ছিল, অথচ
সেটা সরাসরি বসে যাচ্ছে সেই একই string-এর ভেতরে যেটা ডাটাবেসকে কোড
(SQL syntax) হিসেবে পাঠানো হবে। যদি username-এ কেউ এমন কিছু পাঠায় যাতে
একটা ' (single quote) থাকে, সেই ব্যক্তি হঠাৎ ডেটা-পাঠানোর জায়গা থেকে
বেরিয়ে নিজের SQL syntax লেখার ক্ষমতা পেয়ে যায় — আর ডাটাবেস কোনোভাবেই
বুঝতে পারে না কোনটা ডেভেলপারের লেখা query, কোনটা attacker-এর ঢুকিয়ে দেওয়া
নতুন নির্দেশ। দুটোই একই string, একই channel দিয়ে গেছে।
আজকের লেসনে আমরা এই একটা ধারণা তিনটা ভিন্ন প্রেক্ষাপটে সত্যিই চালিয়ে দেখব — একটা real sqlite3 ডাটাবেসে authentication bypass, একটা real শেল কমান্ডে injection, আর একটা toy template ইঞ্জিনে remote-code-execution-এর মতো আচরণ। তিনটাতেই একই প্যাটার্ন ফিরে আসবে, আর একই সমাধান।
মূল ধারণা
Trust boundary — যেখানে DATA context CODE context-এ ঢুকে পড়ে
প্রতিটা interpreter — SQL ইঞ্জিন, শেল, টেমপ্লেট রেন্ডারার, এমনকি একটা regex engine — একটা ইনপুট স্ট্রিং পড়ে দুই ধরনের জিনিস বের করে: syntax (কোন অপারেশন করতে হবে, কোন keyword, কোন delimiter) আর data (আসল মান, যেটার ওপর অপারেশন চলবে)। একটা নিরাপদ সিস্টেমে এই দুইটা সবসময় আলাদা চ্যানেলে interpreter-এর কাছে পৌঁছায়। Injection ঘটে যখন ডেভেলপার — না জেনেই — এই দুইটা চ্যানেলকে একটাতে মিশিয়ে ফেলেন: user-controlled ডেটাকে সরাসরি সেই স্ট্রিং-এর ভেতরে বসিয়ে দেন যেটা পরে syntax হিসেবে পার্স হবে।
| Injection class | Interpreter | Syntax-এর উদাহরণ | Attacker যা ঢোকায় |
|---|---|---|---|
| SQL injection | ডাটাবেস query engine | SELECT, ', OR, -- | নতুন WHERE-শর্ত, নতুন statement |
| Command injection | শেল (/bin/sh) | ;, |, &&, ` | নতুন কমান্ড |
| SSTI (template injection) | টেমপ্লেট রেন্ডারার | {{ }} expression syntax | নির্বিচারে expression evaluation |
| LDAP injection | LDAP filter parser | *, (, ), | | filter logic বদলে দেওয়া |
| Log injection | log parser/viewer | newline, ANSI escape | ভুয়া log entry, terminal exploit |
টেবিলের প্রতিটা row-এর গঠন একই — শুধু interpreter আর তার syntax-token বদলেছে। এই লেসনে আমরা প্রথম তিনটা গভীরভাবে দেখব (SQL flagship হিসেবে, command আর SSTI একই প্যাটার্নের প্রতিধ্বনি হিসেবে), শেষ দুইটা শুধু টেবিলে থাকল যাতে বোঝা যায় প্যাটার্নটা এখানেই থামে না।
ভেতরে কী ঘটছে
SQL injection-এর মেকানিজম — precedence আর comment-এর খেলা
সবচেয়ে চেনা পেলোড: ' OR '1'='1। কিন্তু এই পেলোড একা সবসময় কাজ করে
না — নিচের কোডটা আসলেই চালানো, আর সেটাই দেখায় কেন:
# precedence.py — কেন শুধু "' OR '1'='1'" যথেষ্ট নয়, '--' কেন দরকার
import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (username TEXT, password TEXT)")
conn.execute("INSERT INTO users VALUES ('admin', 'S3cr3t!Root99')")
conn.commit()
def run(username, password):
query = f"SELECT username FROM users WHERE username = '{username}' AND password = '{password}'"
print(f" query : {query}")
return conn.execute(query).fetchall()
print("== variant A: '1'='1' যোগ করা, কিন্তু কোনো comment ছাড়া ==")
rows = run("' OR '1'='1", "totally-wrong-password")
print(f" ফলাফল: {rows} (বাইপাস হলো: {len(rows) > 0})")
print("== variant B: একই payload, কিন্তু '--' দিয়ে বাকি query কমেন্ট-আউট করা ==")
rows = run("' OR '1'='1' -- ", "this-part-never-even-parsed")
print(f" ফলাফল: {rows} (বাইপাস হলো: {len(rows) > 0})")প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):
== variant A: '1'='1' যোগ করা, কিন্তু কোনো comment ছাড়া ==
query : SELECT username FROM users WHERE username = '' OR '1'='1' AND password = 'totally-wrong-password'
ফলাফল: [] (বাইপাস হলো: False)
== variant B: একই payload, কিন্তু '--' দিয়ে বাকি query কমেন্ট-আউট করা ==
query : SELECT username FROM users WHERE username = '' OR '1'='1' -- ' AND password = 'this-part-never-even-parsed'
ফলাফল: [('admin',)] (বাইপাস হলো: True)দুইটা variant-এর পার্থক্যই আজকের সবচেয়ে গুরুত্বপূর্ণ সূক্ষ্মতা। Variant
A ব্যর্থ হয় কারণ SQL-এ AND, OR-এর চেয়ে বেশি precedence পায় — তাই
query আসলে পার্স হয় username='' OR ('1'='1' AND password='totally-wrong-password')
হিসেবে, আর ডান পাশটা password ভুল বলে পুরোটাই False। Variant B
সফল হয় কারণ -- SQL-এ একটা comment-marker — তার পরের সবকিছু (বাকি
AND password = '...' অংশ) parser-এর কাছে নিছক একটা মন্তব্য, কখনো
execute-ই হয় না। Attacker password check-টাকেই বাস্তবে মুছে ফেলেছে,
ভাঙেনি।
এইখানেই root-cause-টা সবচেয়ে স্পষ্ট: ডেভেলপার যে string-টা লিখেছিলেন
(SELECT ... WHERE username = '...' AND password = '...'), সেটা আর
সেই স্ট্রিং নেই যেটা ডাটাবেস আসলে পার্স করছে — attacker-এর ইনপুট
নিজেই নতুন syntax টুকরো হয়ে সেই string-এর অর্থ পুনর্লিখন করে ফেলেছে।
Command injection — একই প্যাটার্ন, শেল-এর ভাষায়
শেল কমান্ড বানানোর সময় হুবহু একই ভুল হয়। ধরুন একটা ফিচার যেটা user-এর দেওয়া hostname দিয়ে কিছু একটা করে, আর ডেভেলপার লেখেন:
cmd = f"echo Checking host: {hostname}"
subprocess.run(cmd, shell=True, ...)শেলের কাছে ;, |, &&, backtick — এগুলো সবই syntax-token, ঠিক SQL-এর
' আর ---এর মতোই। hostname-এ যদি ; echo INJECTED বসানো যায়, শেল
সেটাকে দুইটা আলাদা কমান্ড হিসেবে পড়ে — কোনো “hostname ছিল যার ভেতরে
সেমিকোলন ছিল” ধারণা শেলের কাছে নেই, সে শুধু syntax-token দেখে দুইটা
statement আলাদা করে। পুরো verified demonstration নিচের Build অংশে।
SSTI — যখন interpreter-টা একটা template engine
তৃতীয় উদাহরণ, সংক্ষেপে। টেমপ্লেট ইঞ্জিন (Jinja2, Twig, Freemarker-জাতীয়)
{{ expr }} -স্টাইল syntax দিয়ে templates-এর ভেতরে expression
বসানোর সুযোগ দেয়। যদি user-এর ইনপুট (যেমন “নাম”) সরাসরি টেমপ্লেট-string-
এর ভেতরে বসিয়ে তারপর render করা হয় — ঠিক SQL আর শেলের মতো একই ভুল —
attacker নিজের নাম-এর জায়গায় {{ 7*7 }} পাঠালে templating
engine সেটাকে টেক্সট না ভেবে expression হিসেবে execute করে ফেলে।
নিচের কোড টুকরোটা আসলেই চালানো, একটা toy engine দিয়ে (Jinja2-জাতীয়
real engine ঠিক এই একই নীতিতে চলে, শুধু sandboxing-স্তর বেশি):
# ssti.py — টেমপ্লেট ইঞ্জিনের ভেতরে code/data মিশ্রণ (toy engine)
import re
def render(template, context):
return re.sub(r"\{\{\s*(.*?)\s*\}\}", lambda m: str(eval(m.group(1), {}, context)), template)
print("== naive: user-এর নাম সরাসরি টেমপ্লেট-STRING-এর ভেতরে বসানো ==")
naive_template = "Hello, World! Today is {{ 2 + 2 }} days away."
print(f" render output : {render(naive_template, {})!r}")
print("== attacker নিজের 'নাম'-এর জায়গায় টেমপ্লেট-syntax পাঠায় ==")
payload = "{{ 7*7 }}"
malicious_template = f"Hello, {payload}! Today is {{{{ 2 + 2 }}}} days away."
print(f" render output : {render(malicious_template, {})!r}")
print("== fix: user input টেমপ্লেট-STRING-এ না বসিয়ে, শুধু context DATA হিসেবে ==")
safe_template = "Hello, {{ name }}! Today is {{ 2 + 2 }} days away."
print(f" render (payload as data): {render(safe_template, {'name': payload})!r}")প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):
== naive: user-এর নাম সরাসরি টেমপ্লেট-STRING-এর ভেতরে বসানো ==
render output : 'Hello, World! Today is 4 days away.'
== attacker নিজের 'নাম'-এর জায়গায় টেমপ্লেট-syntax পাঠায় ==
render output : 'Hello, 49! Today is 4 days away.'
== fix: user input টেমপ্লেট-STRING-এ না বসিয়ে, শুধু context DATA হিসেবে ==
render (payload as data): 'Hello, {{ 7*7 }}! Today is 4 days away.'লক্ষ্য করুন সবচেয়ে গুরুত্বপূর্ণ লাইনটা — attacker “নাম” হিসেবে 7*7
পাঠাল, output-এ এলো 49, World না — মানে তার ইনপুট evaluate হয়েছে,
শুধু display হয়নি। বাস্তব SSTI-তে 7*7-এর জায়গায় ফাইল-পড়া বা
কমান্ড-execute করার মতো expression বসানো সম্ভব, নির্ভর করে কোন ইঞ্জিন আর
কতটা sandbox করা। শেষ ধাপে fix হুবহু আগের দুইটার মতোই — user input আর
টেমপ্লেট-syntax কখনো একই string-এ না মেশা।
উদাহরণ
তিনটা ক্লাসের পাশাপাশি তুলনা
একই root-cause, তিনটা ভিন্ন interpreter — নিচের টেবিলে প্যাটার্নটা এক নজরে:
| SQL injection | Command injection | SSTI | |
|---|---|---|---|
| Interpreter | ডাটাবেস engine | শেল (/bin/sh) | টেমপ্লেট renderer |
| ভাঙা boundary | query string | কমান্ড string | template string |
| চেনা syntax-token | ', --, OR | ;, |, `, && | {{ }} expression grammar |
| আজকের payload | ' OR '1'='1' -- | ; echo INJECTED | {{ 7*7 }} |
| Structural fix | parameterized query | argument-list exec (shell=False) | user input শুধু context-DATA, template-string না |
| Fix-এর সাধারণ নাম | data/code channel আলাদা করা | data/code channel আলাদা করা | data/code channel আলাদা করা |
শেষ দুইটা row-ই লেসনের মূল কথা আরেকবার বলছে: তিনটা “fix”-এর নাম আলাদা, কিন্তু নীতিটা প্রতিটাতেই এক — এমন একটা API ব্যবহার করা যেখানে ডেটা আর syntax structurally আলাদা দুইটা প্যারামিটার/চ্যানেল, একটা string-এর দুই অংশ নয়।
ছবিতে
বিপজ্জনক (একটা চ্যানেল — ডেটা আর syntax একসাথে concatenate):
developer syntax ──┐
├──► একটাই string ──► interpreter (পার্থক্য করতে পারে না)
user-controlled data ┘
নিরাপদ (দুইটা চ্যানেল — ডেটা কখনো syntax-string-এ ঢোকে না):
developer syntax (placeholder-সহ) ──────────► interpreter
▲
user-controlled data ───────(আলাদা parameter)────┘
(interpreter নিজে জানে
এটা কখনো syntax না,
সবসময় literal value)naive (concatenation):
query = "SELECT ... WHERE username = '" + username + "'"
└── username-এর ভেতরে ' থাকলেই string-boundary ভেঙে নতুন syntax শুরু
parameterized (placeholder):
query = "SELECT ... WHERE username = ?"
params = (username,)
└── driver username-কে ALWAYS literal value হিসেবে পাঠায় ডাটাবেসে,
query-string-এর কম্পাইলেশনের *পরে* — তাই এতে কোনো ' থাকলেও
সেটা "একটা অক্ষর" হিসেবেই থেকে যায়, কখনো syntax নানিজে চালিয়ে দেখুন
Login bypass — naive query বনাম parameterized query, real sqlite3-এ
# sql_injection.py — নিরাপদ (in-memory) sqlite3 দিয়ে সত্যিকারের SQLi আক্রমণ ও প্রতিরোধ
import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (username TEXT, password TEXT, role TEXT)")
conn.executemany(
"INSERT INTO users VALUES (?, ?, ?)",
[
("alice", "alice-pw-2024", "user"),
("admin", "S3cr3t!Root99", "admin"),
],
)
conn.commit()
def naive_login(username, password):
# বিপজ্জনক: user input সরাসরি SQL string-এর ভেতরে বসানো হচ্ছে (concatenation)
query = f"SELECT username, role FROM users WHERE username = '{username}' AND password = '{password}'"
print(f" [naive] পাঠানো query : {query}")
return conn.execute(query).fetchall()
def safe_login(username, password):
# নিরাপদ: placeholder (?) — user input কখনো SQL syntax-এর অংশ হয় না
query = "SELECT username, role FROM users WHERE username = ? AND password = ?"
print(f" [safe] পাঠানো query : {query} params={(username, password)}")
return conn.execute(query, (username, password)).fetchall()
print("== ধাপ ১: naive query — বৈধ credential দিয়ে স্বাভাবিক login ==")
rows = naive_login("alice", "alice-pw-2024")
print(f" ফলাফল: {rows}\n")
print("== ধাপ ২: naive query — ভুল password দিয়ে login ব্যর্থ (স্বাভাবিক) ==")
rows = naive_login("alice", "wrong-password")
print(f" ফলাফল: {rows}\n")
print("== ধাপ ৩: naive query — SQL injection payload দিয়ে আক্রমণ ==")
payload_user = "' OR '1'='1' -- "
rows = naive_login(payload_user, "whatever-this-is-never-checked")
print(f" ফলাফল: {rows}")
print(f" authentication বাইপাস হলো: {len(rows) > 0}")
print(f" admin row leak হলো : {('admin', 'admin') in rows}\n")
print("== ধাপ ৪: ঠিক একই payload — কিন্তু এবার parameterized (safe) query-তে ==")
rows = safe_login(payload_user, "whatever-this-is-never-checked")
print(f" ফলাফল: {rows}")
print(f" authentication বাইপাস হলো: {len(rows) > 0}\n")
print("== ধাপ ৫: parameterized query-তে বৈধ credential আগের মতোই কাজ করে ==")
rows = safe_login("admin", "S3cr3t!Root99")
print(f" ফলাফল: {rows}")প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):
== ধাপ ১: naive query — বৈধ credential দিয়ে স্বাভাবিক login ==
[naive] পাঠানো query : SELECT username, role FROM users WHERE username = 'alice' AND password = 'alice-pw-2024'
ফলাফল: [('alice', 'user')]
== ধাপ ২: naive query — ভুল password দিয়ে login ব্যর্থ (স্বাভাবিক) ==
[naive] পাঠানো query : SELECT username, role FROM users WHERE username = 'alice' AND password = 'wrong-password'
ফলাফল: []
== ধাপ ৩: naive query — SQL injection payload দিয়ে আক্রমণ ==
[naive] পাঠানো query : SELECT username, role FROM users WHERE username = '' OR '1'='1' -- ' AND password = 'whatever-this-is-never-checked'
ফলাফল: [('alice', 'user'), ('admin', 'admin')]
authentication বাইপাস হলো: True
admin row leak হলো : True
== ধাপ ৪: ঠিক একই payload — কিন্তু এবার parameterized (safe) query-তে ==
[safe] পাঠানো query : SELECT username, role FROM users WHERE username = ? AND password = ? params=("' OR '1'='1' -- ", 'whatever-this-is-never-checked')
ফলাফল: []
authentication বাইপাস হলো: False
== ধাপ ৫: parameterized query-তে বৈধ credential আগের মতোই কাজ করে ==
[safe] পাঠানো query : SELECT username, role FROM users WHERE username = ? AND password = ? params=('admin', 'S3cr3t!Root99')
ফলাফল: [('admin', 'admin')]ধাপ ৩-এর ফলাফল দেখুন: naive query attacker-কে শুধু login-ই করিয়ে দেয়নি —
[('alice', 'user'), ('admin', 'admin')] — দুইটা row ফেরত দিয়েছে,
কারণ WHERE username = '' OR '1'='1'-এর পরে '1'='1' সবসময় true, তাই
পুরো table-ই match করেছে; role-column-সহ (admin) ডেটা leak হয়ে গেছে।
ধাপ ৪-এ হুবহু সেই একই payload string — কোনো special-case কোড লেখা
হয়নি, কোনো “যদি input-এ quote থাকে” চেক নেই — অথচ ফলাফল খালি []। কারণ
sqlite3-এর driver placeholder (?) দেখলে সেই জায়গায় বসা মানটাকে
কখনোই SQL parse করে না — সেটা query compile হওয়ার পরে, একটা আলাদা
wire-protocol message হিসেবে ডাটাবেসে যায়, বাইন্ড হয় একটা literal value
হিসেবে। Attacker-এর ' অক্ষরটা তখন নিছক একটা অক্ষর — string-boundary
ভাঙার কোনো ক্ষমতাই তার নেই, কারণ সেটা আর কখনো “string-এর ভেতরের অক্ষর”
হিসেবে পার্স-ই হচ্ছে না।
একই ' OR '1'='1' -- payload naive string-concatenation query-তে authentication সম্পূর্ণ বাইপাস করে এবং admin row leak করে দেয়, কিন্তু হুবহু একই payload parameterized (placeholder + params) query-র বিরুদ্ধে সম্পূর্ণ নিষ্ফল থাকে — কোনো special-case handling ছাড়াই।
নিজে বানান
SafeHostCheck — command injection থেকে argument-list API-তে migration
- naive_check() লিখুন — shell=True আর f-string concatenation দিয়ে একটা hostname-echo কমান্ড বানান
- স্বাভাবিক hostname দিয়ে চালিয়ে দেখুন এটা প্রত্যাশিতভাবে কাজ করে
- একটা shell-metacharacter-সহ payload hostname (";" ব্যবহার করে) দিয়ে চালান — injected কমান্ড execute হয় দেখুন
- safe_check() লিখুন — argv list আর shell=False (subprocess-এর ডিফল্ট) ব্যবহার করে
- ঠিক একই payload safe_check()-এ চালিয়ে দেখান এটা এখন নিছক একটা literal string-argument, আর কোনো injected কমান্ড execute হয় না
লক্ষ্য: shell=True + string-concatenation প্যাটার্ন কেন বিপজ্জনক, আর
argument-list (shell=False) API কীভাবে সেই একই আক্রমণকে কাঠামোগতভাবেই
অসম্ভব করে দেয় — attacker-এর ইনপুটকে “একটা মাত্র argument” বানিয়ে, কখনো
শেল-syntax পার্স হতে না দিয়ে।
# cmd_injection.py — subprocess দিয়ে command injection ও argument-list fix
import subprocess
def naive_check(hostname):
# বিপজ্জনক: shell=True + string concatenation — shell metacharacter (; && | ` $())
# সবকিছুকেই "নতুন কমান্ড" হিসেবে পড়ে
cmd = f"echo Checking host: {hostname}"
print(f" [naive] চালানো shell command : {cmd}")
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return result.stdout.strip()
def safe_check(hostname):
# নিরাপদ: argument-list API, shell=False (default) — hostname string
# হুবহু একটা argument হিসেবে যায়, shell কখনো parse করে না
argv = ["echo", "Checking host:", hostname]
print(f" [safe] চালানো argv : {argv}")
result = subprocess.run(argv, shell=False, capture_output=True, text=True)
return result.stdout.strip()
print("== ধাপ ১: naive — স্বাভাবিক hostname ==")
print(f" output: {naive_check('example.com')!r}\n")
print("== ধাপ ২: naive — payload hostname-এ শেল metacharacter (';') ==")
payload = "example.com; echo INJECTED-COMMAND-RAN"
print(f" output: {naive_check(payload)!r}\n")
print("== ধাপ ৩: ঠিক একই payload — কিন্তু এবার argument-list (safe) API-তে ==")
print(f" output: {safe_check(payload)!r}\n")প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):
== ধাপ ১: naive — স্বাভাবিক hostname ==
[naive] চালানো shell command : echo Checking host: example.com
output: 'Checking host: example.com'
== ধাপ ২: naive — payload hostname-এ শেল metacharacter (';') ==
[naive] চালানো shell command : echo Checking host: example.com; echo INJECTED-COMMAND-RAN
output: 'Checking host: example.com\nINJECTED-COMMAND-RAN'
== ধাপ ৩: ঠিক একই payload — কিন্তু এবার argument-list (safe) API-তে ==
[safe] চালানো argv : ['echo', 'Checking host:', 'example.com; echo INJECTED-COMMAND-RAN']
output: 'Checking host: example.com; echo INJECTED-COMMAND-RAN'মূল মুহূর্ত ধাপ ২-এ — output-এ একটা নতুন লাইন INJECTED-COMMAND-RAN
এসে গেছে, যেটা attacker-এর hostname-এর ভেতরে থাকা ;-এর পরের অংশ, একটা
সম্পূর্ণ আলাদা কমান্ড হিসেবে চলেছে। ধাপ ৩-এ output-এ লক্ষ্য করুন —
পুরো payload string-টাই (সেমিকোলনসহ) একটা মাত্র echo-argument হিসেবে
প্রিন্ট হয়েছে, INJECTED-COMMAND-RAN আলাদা লাইনে আসেনি। কারণ argv-list
API-তে hostname-কে shell কখনো পার্স-ই করে না — সেটা সরাসরি process-এর
argv[2]-তে বসে, ; অক্ষরটা সেখানে নিছক একটা character, কোনো
statement-separator না।
নিজে বাড়ান:
naive_check-এsubprocess.runবাদ দিয়েos.system(cmd)ব্যবহার করলে কী বদলায়? (ইঙ্গিত: কিছুই না — একই ঝুঁকি, ভিন্ন API)safe_check-এ যদি কেউ ভুল করেshell=Trueযোগ করে দেয় (argv list রেখেই), কী ঘটবে? চালিয়ে দেখান — এটাই একটা বাস্তব production bug-এর ধরনshlex.quote()দিয়ে naive পদ্ধতিতেই “escape” করার চেষ্টা করুন — এটা কাজ করে, কিন্তু কেন argument-list পদ্ধতির চেয়ে ভঙ্গুর (একটা ভুলে গিয়ে quote করা বাদ পড়লেই আবার bug) তা যুক্তি দিন
বাস্তব সিস্টেমে
বাস্তব জগতে injection
TalkTalk (২০১৫)। UK-এর telecom TalkTalk-এর ওয়েবসাইট একটা SQL injection আক্রমণে প্রায় ১,৫৭,০০০ গ্রাহকের ব্যক্তিগত তথ্য (নাম, ঠিকানা, ব্যাংক ডিটেইলের আংশিক তথ্যসহ) হারায় — আক্রমণকারী একটা কিশোর বয়সী hacker- গ্রুপের সদস্য ছিল, কোনো advanced technique লাগেনি, decade-পুরনো একটা known-vulnerable ওয়েব পেজেই আক্রমণ চলেছিল। UK-এর ICO (Information Commissioner’s Office) TalkTalk-কে তখনকার রেকর্ড £৪০০,০০০ জরিমানা করে, আর রিপোর্টে স্পষ্ট লেখে — এই আক্রমণ ঠেকানো “সহজ ও সস্তা” ছিল যদি company মৌলিক SQL-injection-defense (parameterized query) মেনে চলত।
OWASP Top 10 — বছরের পর বছর শীর্ষে। ২০০৩ সাল থেকে প্রতিটা OWASP Top 10 সংস্করণেই injection প্রথম পাঁচের মধ্যে থেকেছে, ২০২১ সংস্করণেও A03:2021 হিসেবে আছে। এতদিনের সচেতনতা, এতগুলো framework built-in parameterized-query support দেওয়ার পরেও injection টিকে থাকার কারণ: সমস্যাটা framework-এর অভাব না — legacy কোড, hand-written raw query, আর “শুধু এই একটা জায়গায় দ্রুত string বসিয়ে দিই” জাতীয় shortcut।
SSTI বাস্তব bug bounty রিপোর্টে। 2০১৫ সালে James Kettle-এর গবেষণার পর থেকে SSTI একটা নিজস্ব category হিসেবে স্বীকৃত পেয়েছে — বহু bug-bounty প্রোগ্রামে (Uber, Shopify সহ বহু কোম্পানি) রিপোর্ট হওয়া উচ্চ-severity বাগ template-injection-থেকে remote-code-execution পর্যন্ত পৌঁছেছে, বিশেষত যেখানে ডেভেলপাররা user-controlled string (যেমন profile bio, email subject) সরাসরি একটা “email preview” বা “report generator” টেমপ্লেটের মধ্যে বসিয়েছেন।
যে ভুলগুলো সবাই করে
“একটা single quote (') এস্কেপ করে দিলেই (যেমন addslashes()) SQL injection ঠেকানো যায়।”
Character-escaping একটা patch, structural fix না — আর অনেক জায়গায়
এটা ভাঙে। ভিন্ন ডাটাবেস, ভিন্ন encoding (multi-byte character set-এ
escape-character নিজেই bypass হতে পারে), numeric context (যেখানে quote-ই
লাগে না, id = এর পরে সরাসরি সংখ্যা বসে), সবগুলোতেই escaping-নির্ভর
defense আলাদাভাবে ভাঙার ইতিহাস আছে। Parameterized query-তে এই সমস্যাটাই
নেই — কারণ escaping-এর প্রশ্নই ওঠে না, ডেটা কখনো সেই string-এ ঢোকেই না
যেটা parse হয়।
“Parameterized query মানে driver ভেতরে ভেতরে input-টা 'escape' করে দেয়, তারপর সেটা string-এ জুড়ে দেয়।”
এটা injection-defense-এর সবচেয়ে বেশি ভুল-বোঝা যান্ত্রিকতা। Parameterized query-তে escaping হয়ই না — query-টা প্রথমে placeholder-সহ compile হয় (তখনও প্যারামিটারের মান জানাই নেই), তারপর মানগুলো একটা সম্পূর্ণ আলাদা wire-protocol message হিসেবে driver থেকে ডাটাবেসে পাঠানো হয়, বাইন্ড হয় literal value হিসেবে। Experiment অংশের ধাপ ৪-এ এই কারণেই কোনো special “quote-handling” কোড ছাড়াই payload নিষ্ফল হয়ে গেছে — string-boundary ভাঙার প্রশ্নটাই কখনো ওঠেনি, কারণ ডেটা string-এ ঢোকেইনি।
“Command injection ঠেকাতে input থেকে বিপজ্জনক অক্ষর (;, |, &) স্ট্রিপ করে দিলেই যথেষ্ট।”
Blocklist-ভিত্তিক character-stripping একটা অসম্পূর্ণ তালিকার ওপর নির্ভর
করে — শেলের metacharacter তালিকা দীর্ঘ ($(), backtick, newline,
$IFS জাতীয় whitespace-বাইপাস কৌশল, আরও অনেক), আর নতুন bypass কৌশল
নিয়মিত আবিষ্কৃত হয়। Build অংশে দেখানো argument-list API structural fix —
কোনো blocklist লাগে না, কারণ shell-parsing ধাপটাই বাদ পড়ে যায়।
“Client-side input validation (JavaScript regex, HTML pattern attribute) SQL/command injection থেকে রক্ষা করে।”
Client-side validation একটা UX-সুবিধা, নিরাপত্তা-boundary না — attacker ব্রাউজার সম্পূর্ণ বাদ দিয়ে সরাসরি HTTP request পাঠাতে পারে (curl, Burp Suite, নিজের script), তখন কোনো JS regex-ই চলে না। Trust boundary সবসময় server-side হতে হবে, ঠিক যেখানে query/command আসলে তৈরি হচ্ছে — threat-modeling লেসনের ভাষায়, client কখনো trusted zone-এর ভেতরে না।
বুঝেছেন কি না দেখুন
1SQL injection, command injection, আর SSTI — তিনটারই root cause এক
বাক্যে কী?
স্মরণ
Untrusted, user-controlled ডেটা সরাসরি সেই একই string-এর ভেতরে বসিয়ে দেওয়া হয় যেটা পরে একটা interpreter (ডাটাবেস, শেল, টেমপ্লেট ইঞ্জিন) দিয়ে CODE/SYNTAX হিসেবে পার্স হবে — ডেটা আর syntax কখনো আলাদা চ্যানেলে পাঠানো হয় না।
2Under-the-hood অংশে ' OR '1'='1' (কোনো comment ছাড়া) ব্যর্থ হয়েছিল,
কিন্তু ' OR '1'='1' -- সফল হয়েছিল। এই পার্থক্যের পেছনে ঠিক কোন দুইটা
আলাদা mechanism কাজ করেছে?
যুক্তি
' OR '1'='1' (কোনো comment ছাড়া) ব্যর্থ হয়েছিল,
কিন্তু ' OR '1'='1' -- সফল হয়েছিল। এই পার্থক্যের পেছনে ঠিক কোন দুইটা
আলাদা mechanism কাজ করেছে?প্রথমটায় SQL-এর operator precedence-এর কারণে (AND OR-এর চেয়ে বেশি
precedence পায়) পুরো condition আসলে username='' OR ('1'='1' AND password='...') হিসেবে পার্স হয়, আর password ভুল হওয়ায় পুরোটা false
থেকে যায়। দ্বিতীয়টায় -- কমেন্ট-মার্কার বাকি query-কে (password-check
সহ) সম্পূর্ণভাবে পার্স-না-হওয়া কমেন্টে পরিণত করে দেয় — password check-টাই
আর query-তে থাকে না।
3Experiment-এ safe_login("' OR '1'='1' -- ", "whatever...") কল করলে
ফলাফল খালি লিস্ট [] এসেছিল। Parameterized query আসলে ভেতরে ভেতরে কী
করে যার কারণে attacker-এর ' অক্ষরটা কোনো ক্ষতি করতে পারেনি?
প্রয়োগ
safe_login("' OR '1'='1' -- ", "whatever...") কল করলে
ফলাফল খালি লিস্ট [] এসেছিল। Parameterized query আসলে ভেতরে ভেতরে কী
করে যার কারণে attacker-এর ' অক্ষরটা কোনো ক্ষতি করতে পারেনি?Query string (placeholder-সহ) আগে থেকেই compile হয়ে যায়, তখনও
প্যারামিটারের মান driver জানে না। এরপর username-এর মান একটা আলাদা
wire-protocol message হিসেবে ডাটাবেসে পাঠানো হয়, বাইন্ড হয় literal value
হিসেবে — কোনো escaping বা string-জোড়া-লাগানো হয় না। Attacker-এর '
অক্ষরটা তাই কখনো “SQL syntax-এর অংশ” হিসেবে পড়াই হয়নি, শুধু একটা
সাধারণ character হিসেবে username-value-এর ভেতরে থেকে গেছে।
4Build অংশে shell=True + f-string বাদ দিয়ে argv-list + shell=False
ব্যবহার করলে একই payload ("example.com; echo INJECTED-COMMAND-RAN")
কেন আর injected কমান্ড execute করতে পারল না?
যুক্তি
shell=True + f-string বাদ দিয়ে argv-list + shell=False
ব্যবহার করলে একই payload ("example.com; echo INJECTED-COMMAND-RAN")
কেন আর injected কমান্ড execute করতে পারল না?Argv-list API-তে hostname স্ট্রিংটা কখনো শেল দিয়ে পার্স-ই হয় না —
সেটা সরাসরি child process-এর একটা argv-element হিসেবে বসে। শেল-parsing
ধাপ (যেখানে ; কে “statement separator” হিসেবে চেনা হতো) সম্পূর্ণ
বাদ পড়ে যায়, তাই ; অক্ষরটা নিছক একটা সাধারণ character থেকে যায়,
কোনো syntax-অর্থ পায় না।
5SSTI ডেমোতে attacker-এর পেলোড ছিল {{ 7*7 }}, আর output ছিল
49, World না। বাস্তব production system-এ এই একই bug থাকলে
attacker কী কী করতে পারত (শুধু arithmetic-এর বাইরে চিন্তা করুন)?
প্রয়োগ
{{ 7*7 }}, আর output ছিল
49, World না। বাস্তব production system-এ এই একই bug থাকলে
attacker কী কী করতে পারত (শুধু arithmetic-এর বাইরে চিন্তা করুন)?যদি template engine sandboxed না হয়, attacker যেকোনো Python
expression চালাতে পারে — ফাইল পড়া, environment variable dump করা,
এমনকি os/subprocess-জাতীয় module import করে সার্ভারে কমান্ড
execute করা পর্যন্ত (remote code execution)। ডেমোর সরল eval()
সেই একই ক্ষমতা রাখে — শুধু 7*7-এর জায়গায় আরও ক্ষতিকর একটা expression
বসালেই।
6একটা internal admin-tool ডিজাইন করছেন যেটা user-দেওয়া একটা “search
term” নিয়ে (ক) একটা SQL query চালায় রিপোর্ট বানাতে, আর (খ) সেই একই
term দিয়ে একটা log-analysis শেল-টুল (grep) চালায়। দুইটা জায়গাতেই
injection ঠেকানোর জন্য কী কী design decision নেবেন?
ডিজাইন
grep) চালায়। দুইটা জায়গাতেই
injection ঠেকানোর জন্য কী কী design decision নেবেন?SQL-অংশে: কখনো raw SQL string-concatenation না, সবসময় parameterized
query/prepared statement (বা ORM-এর built-in query-builder) — search
term সবসময় params-হিসেবে যাবে। Shell-অংশে: grep-কে shell=True +
concatenated string দিয়ে না ডেকে argument-list (subprocess.run(["grep", search_term, logfile], shell=False)) দিয়ে ডাকা — search term একটা
মাত্র argv-element হিসেবে যাবে, শেল কখনো সেটা পার্স করবে না। দুইটা
জায়গাতেই নীতি এক: user input যেন কখনো এমন কোনো string-এর অংশ না হয়
যেটা পরে syntax হিসেবে পার্স হবে — আলাদা placeholder/argv চ্যানেল
ব্যবহার করা, string বানিয়ে পাঠানো না। এর ওপর defense-in-depth হিসেবে
least-privilege DB user (শুধু SELECT-অনুমতি রিপোর্টিং টেবিলে) আর
timeout/resource-limit যোগ করা যায়, কিন্তু primary fix সবসময় structural
channel-separation-ই।
এরপর কী
এরপর কী
আজ injection-এর এক-লাইনের থিসিস দেখলাম — DATA যখন CODE/SYNTAX-এর জায়গায় ঢুকে পড়ে — আর তিনটা ভিন্ন interpreter-এ (ডাটাবেস, শেল, টেমপ্লেট ইঞ্জিন) সেই একই প্যাটার্ন real কোড চালিয়ে সত্যিই ভেঙে-গড়ে দেখলাম। SQL injection একটা DATABASE-কে বোকা বানিয়ে attacker-এর নিজের লজিক execute করায়।
পরের লেসনের তিনটা আক্রমণ — XSS, CSRF, আর SSRF — সম্পূর্ণ ভিন্ন টার্গেট বেছে নেয়: একটা ভিকটিমের নিজের ব্রাউজার, অথবা আপনার নিজের সার্ভার-কেই বোকা বানিয়ে attacker-এর হয়ে কাজ করানো। Trust-boundary-র ভাষা একই থাকবে, শুধু কোন পক্ষ “ভুল করে অন্যকে বিশ্বাস করছে” সেই সমীকরণটা বদলে যাবে — user-এর ব্রাউজার একটা malicious script-কে বিশ্বাস করবে (XSS), একটা ওয়েবসাইট ভিকটিমের ব্রাউজার থেকে আসা request-কে বিশ্বাস করবে (CSRF), অথবা আপনার নিজের সার্ভার attacker-এর দেওয়া একটা URL-কে বিশ্বাস করে সেখানে request পাঠিয়ে দেবে (SSRF)।
আরও পড়ুন
- OWASP Top 10:2021 — A03:2021 Injection — OWASP Foundation · Injection-কে আজও Top-10-এর একটা প্রধান category রাখার আনুষ্ঠানিক যুক্তি ও পরিসংখ্যান
- SQL Injection Prevention Cheat Sheet — OWASP Foundation · Parameterized query, stored procedure, আর allowlist input validation-এর আনুষ্ঠানিক defense-in-depth নির্দেশিকা — এই লেসনের Build ও Experiment অংশের ভিত্তি
- OS Command Injection Defense Cheat Sheet — OWASP Foundation · Argument-list API (shell-বিহীন exec) কেন প্রথম-পছন্দের প্রতিরোধ, সেই যুক্তির সরকারি উৎস
- Server-Side Template Injection: RCE For The Modern Web App — James Kettle — PortSwigger, Black Hat USA 2015 · SSTI-কে একটা নিজস্ব injection class হিসেবে প্রথম systematic-ভাবে চিহ্নিত করা গবেষণা