পাঠ ২০ · ৬০-এর মধ্যে · মডিউল ৫
Home / Courses / Cybersecurity & Ethical Hacking / SQL ইনজেকশন

A03: SQL ইনজেকশন

A03: SQL injection
৯ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • SQL ইনজেকশন কীভাবে ঘটে — স্ট্রিং কনক্যাটেনেশন দিয়ে কোয়েরি তৈরির বিপদ
  • ক্লাসিক ' OR '1'='1 অথেন্টিকেশন-বাইপাস পেলোড ঠিক কীভাবে কাজ করে, ধাপে-ধাপে
  • SQL কমেন্ট (--) কীভাবে বাকি কোয়েরিকে অকার্যকর করে দেয়
  • প্যারামিটারাইজড কোয়েরি/প্রিপেয়ার্ড স্টেটমেন্ট কেন একমাত্র নির্ভরযোগ্য প্রতিরক্ষা

১ · SQL ইনজেকশন আসলে কী

SQL ইনজেকশনSQL Injectionঅবিশ্বস্ত ইউজার ইনপুট সরাসরি একটি SQL কোয়েরি স্ট্রিং-এ কনক্যাটেনেট করার ফলে, attacker সেই ইনপুটের মাধ্যমে নিজের SQL সিনট্যাক্স/লজিক ইনজেক্ট করতে পারে। হয় যখন একটি অ্যাপ্লিকেশন ইউজারের ইনপুট নিয়ে সরাসরি একটি SQL কোয়েরি স্ট্রিং হিসেবে জোড়া লাগায় — যেমন একটি ক্লাসিক লগইন কোয়েরি এভাবে তৈরি হতে পারে:

"SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'"

এখানে সমস্যা হলো — অ্যাপ্লিকেশন ইউজারের দেওয়া username/password-কে সবসময় "শুধু ডেটা" হিসেবে ধরে নিচ্ছে। কিন্তু যদি ইউজার একটি সিঙ্গেল কোট (') দিয়ে দেয়, সেই কোটটি স্ট্রিং লিটারেল আগেই বন্ধ করে দেয় — এর পরের অংশ আর "ডেটা" থাকে না, সেটা SQL সিনট্যাক্স হিসেবে ব্যাখ্যা হতে শুরু করে।

২ · ক্লাসিক বাইপাস — ' OR '1'='1

সবচেয়ে বিখ্যাত উদাহরণ: username ফিল্ডে লেখা হয় ' OR '1'='1' -- । এখানে তিনটি জিনিস ঘটছে —

  1. প্রথম ' username-এর জন্য উদ্দিষ্ট স্ট্রিং লিটারেল আগেই বন্ধ করে দেয়।
  2. OR '1'='1' একটি সবসময়-সত্য (tautology) শর্ত যোগ করে — '1'='1' কখনো false হয় না।
  3. -- হলো SQL-এর লাইন-কমেন্ট মার্কার — এর পরের পুরো অংশ (আসল password চেকসহ) সম্পূর্ণ বাতিল হয়ে যায়।

ফলাফল: পুরো WHERE ক্লজ প্রতিটি সারির জন্য true হয়ে যায় — অ্যাপ্লিকেশন ভাবে ইউজার সফলভাবে লগইন করেছে, যদিও প্রকৃত কোনো পাসওয়ার্ড যাচাই হয়ইনি।

নিচের কোড সেলটি এই বাইপাস কীভাবে কাজ করে তা বোঝার জন্য একটি ছোট্ট, ভুয়া "WHERE-ক্লজ ইভালুয়েটর" ব্যবহার করে — সম্পূর্ণ ইন-মেমরি Python ডেটার উপর। এটি কোনো বাস্তব SQL ইঞ্জিন নয়, কোনো বাস্তব ডেটাবেসের সাথে সংযুক্ত নয়, এবং কখনো কোনো বাস্তব সিস্টেমে প্রয়োগযোগ্য নয় — শুধুমাত্র ক্লাসিক বাইপাসের যুক্তি শেখানোর জন্য।
Python
# ভুয়া, ইন-মেমরি "ইউজার ডেটাবেস"
users = [
    {"username": "admin", "password": "S3cretPass!42"},
    {"username": "hasan", "password": "hasan_pw_99"},
]

def build_where_clause(username, password):
    # ভালনারেবল অ্যাপ ঠিক এভাবেই ইউজার ইনপুট কোয়েরিতে জোড়া লাগায়
    return f"username='{username}' AND password='{password}'"

def strip_sql_comment(expr):
    # বাস্তব SQL ইঞ্জিনে "--" মানে লাইনের বাকি অংশ কমেন্ট -- attacker এটি দিয়ে
    # কোয়েরির বাকি অংশ (যেমন password চেক) সম্পূর্ণ বাতিল করে দিতে পারে
    idx = expr.find("--")
    return expr[:idx] if idx != -1 else expr

def _split_outside_quotes(expr, token):
    # কোটেড স্ট্রিং-এর ভেতরের অংশ উপেক্ষা করে, শুধু বাইরের token (" OR "/" AND ") ধরে স্প্লিট করে
    parts, current, in_quote, i = [], "", False, 0
    while i < len(expr):
        ch = expr[i]
        if ch == "'":
            in_quote = not in_quote
            current += ch
            i += 1
            continue
        if not in_quote and expr[i:i + len(token)] == token:
            parts.append(current)
            current = ""
            i += len(token)
            continue
        current += ch
        i += 1
    parts.append(current)
    return parts

def _resolve(token, row):
    token = token.strip()
    if token.startswith("'") and token.endswith("'"):
        return token[1:-1]        # কোটেড লিটারেল ভ্যালু, যেমন '1'
    return row.get(token)         # কলাম রেফারেন্স -> আসল রো ভ্যালু

def _eval_comparison(cmp_str, row):
    left, right = cmp_str.split("=", 1)
    return _resolve(left, row) == _resolve(right, row)

def eval_where(expr, row):
    # SQL-এর OR/AND প্রায়োরিটি সিমুলেট করে: প্রতিটি OR-অংশ নিজের ভেতরে AND দিয়ে যাচাই হয়
    for or_part in _split_outside_quotes(expr, " OR "):
        comparisons = _split_outside_quotes(or_part, " AND ")
        if all(_eval_comparison(c, row) for c in comparisons):
            return True
    return False

def login_insecure(username, password, users):
    raw_query = strip_sql_comment(build_where_clause(username, password))
    for row in users:
        if eval_where(raw_query, row):
            return row
    return None

def login_safe(username, password, users):
    # প্যারামিটারাইজড কোয়েরির সিমুলেশন -- ইনপুট কখনো কোয়েরি-স্ট্রাকচারে জোড়া লাগে না,
    # শুধু plain ডেটা হিসেবে তুলনা করা হয় -- তাই কোনো SQL সিনট্যাক্স হিসেবে ব্যাখ্যা হয় না
    for row in users:
        if row["username"] == username and row["password"] == password:
            return row
    return None

malicious_username = "' OR '1'='1' -- "
malicious_password = "যেকোনো-কিছু (আক্রমণকারী পাসওয়ার্ড জানে না)"

print("=== ইনসিকিউর লগইন (স্ট্রিং কনক্যাটেনেশন) ===")
print("ইনপুট username:", repr(malicious_username))
result_insecure = login_insecure(malicious_username, malicious_password, users)
print("বাইপাস সফল হলো:", result_insecure is not None)
print("লগইন হয়ে গেল এই অ্যাকাউন্ট হিসেবে:", result_insecure)

print()
print("=== সিকিউর লগইন (প্যারামিটারাইজড-স্টাইল তুলনা) ===")
result_safe = login_safe(malicious_username, malicious_password, users)
print("বাইপাস সফল হলো:", result_safe is not None)
print("ফলাফল:", result_safe, " <- সঠিকভাবে প্রত্যাখ্যাত, কোনো অ্যাকাউন্টেই লগইন হয়নি")

    

৩ · প্রকৃত সমাধান — প্যারামিটারাইজড কোয়েরি

উপরের login_safe ফাংশনটি একটি গুরুত্বপূর্ণ নীতি দেখায় — প্যারামিটারাইজড কোয়েরিParameterized Queryকোয়েরির স্ট্রাকচার (SQL সিনট্যাক্স) ও ইউজারের ডেটা আলাদা চ্যানেলে ডেটাবেস ইঞ্জিনে পাঠানো হয়, ফলে ডেটার ভেতরে থাকা কোনো ক্যারেক্টার কখনো SQL সিনট্যাক্স হিসেবে ব্যাখ্যা হয় না।-এ কোয়েরির স্ট্রাকচার ও ইউজারের ডেটা সবসময় আলাদা থাকে — কখনো একসাথে একটি স্ট্রিং হিসেবে জোড়া লাগানো হয় না। বাস্তব কোডে এটি দেখতে এমন হয় (উদাহরণ, real SQL চালানো হচ্ছে না) — cursor.execute("SELECT * FROM users WHERE username=? AND password=?", (username, password)) — এখানে ? প্লেসহোল্ডার এবং প্রকৃত মান আলাদাভাবে পাঠানো হয়, তাই ডেটাবেস ইঞ্জিন কখনোই ইউজারের ইনপুটকে কোয়েরি-সিনট্যাক্সের অংশ হিসেবে বিবেচনা করে না — ', --, বা OR যাই লেখা হোক না কেন, তা শুধু ডেটা থেকে যায়।

মূল কথা · Key takeaway

login_insecure ও login_safe ঠিক একই ম্যালিশিয়াস ইনপুট পেয়েছে — কিন্তু ফলাফল সম্পূর্ণ ভিন্ন, কারণ একটিতে ইনপুট কোয়েরি-স্ট্রাকচারের অংশ হয়ে গেছে, অন্যটিতে ইনপুট শুধুই ডেটা থেকে গেছে। এটাই SQL ইনজেকশনের সম্পূর্ণ প্রতিরক্ষা কৌশলের সারমর্ম — কখনো স্ট্রিং কনক্যাটেনেশন দিয়ে কোয়েরি তৈরি করবেন না, সবসময় প্যারামিটারাইজড API ব্যবহার করুন।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ ' OR '1'='1' -- পেলোডের তিনটি অংশের প্রতিটি ঠিক কী কাজ করে, আলাদাভাবে ব্যাখ্যা করুন।

প্রথম ' ইনটেন্ডেড username স্ট্রিংটি আগেই বন্ধ করে দেয়, যাতে বাকি ইনপুট ডেটা না হয়ে SQL সিনট্যাক্স হিসেবে ব্যাখ্যা হয়। OR '1'='1' একটি সবসময়-সত্য শর্ত যোগ করে, যা পুরো WHERE ক্লজকে true বানিয়ে দেয়। -- SQL-এর লাইন-কমেন্ট মার্কার — এটি কোয়েরির বাকি অংশ (আসল password যাচাই) সম্পূর্ণ বাতিল করে দেয়, যাতে সেটাও পাস করার প্রয়োজন না পড়ে।

প্র ০২ প্যারামিটারাইজড কোয়েরি কেন "ইনপুট থেকে বিপজ্জনক ক্যারেক্টার সরিয়ে ফেলা" (sanitization) থেকে বেশি নির্ভরযোগ্য প্রতিরক্ষা?

Sanitization-ভিত্তিক প্রতিরক্ষা নির্ভর করে ডেভেলপার সব সম্ভাব্য বিপজ্জনক প্যাটার্ন (quotes, comments, keyword, encoding variants) আগে থেকে চিন্তা করে ব্লক করবে — একটি patternও মিস হলে বাইপাস সম্ভব। প্যারামিটারাইজড কোয়েরিতে সমস্যাটাই মূলে দূর হয়ে যায়: ডেটা কখনো কোয়েরি-সিনট্যাক্সের চ্যানেলে ঢোকেই না, তাই কোনো নির্দিষ্ট ক্যারেক্টার ব্লক করার দরকারই পড়ে না।

প্র ০৩ উপরের কোড সেলে যদি -- কমেন্ট মার্কারটি বাদ দিয়ে শুধু ' OR '1'='1 পাঠানো হতো, ফলাফল একই রকম হতো কি? কেন/কেন নয়?

একই হতো না বাস্তবসম্মতভাবে — কমেন্ট ছাড়া বাকি কোয়েরি অংশ (AND password='...') এখনো সক্রিয় থাকত। SQL precedence অনুযায়ী AND, OR-এর চেয়ে বেশি প্রাধান্য পায়, তাই পুরো ক্লজ হতো username='' OR ('1'='1' AND password='...') — অর্থাৎ password সঠিক না হলে ডান পাশ false-ই থেকে যেত। comment marker (-- ) ছাড়া classic বাইপাসটি নির্ভরযোগ্যভাবে কাজ করে না, এটাই কারণ বাস্তব SQLi পেলোডে প্রায় সবসময় কমেন্ট মার্কার দেখা যায়।

অনুশীলন

  1. চিন্তা করুন: কেন একটি "ব্লকলিস্ট" পদ্ধতি (যেমন শুধু ' OR '1'='1 স্ট্রিংটি খুঁজে ব্লক করা) SQL ইনজেকশনের বিরুদ্ধে যথেষ্ট প্রতিরক্ষা নয়?

    কারণ একই তাৎপর্যের অসংখ্য ভিন্ন পেলোড লেখা যায় — যেমন ' OR 'a'='a, ' OR 1=1 -- , বা এনকোডেড ভ্যারিয়েন্ট। একটি নির্দিষ্ট স্ট্রিং ব্লক করলে আক্রমণকারী সহজেই সমতুল্য আরেকটি ফর্ম ব্যবহার করে বাইপাস করতে পারে। প্যারামিটারাইজড কোয়েরি এই পুরো সমস্যার শ্রেণিকেই দূর করে দেয়, কোনো নির্দিষ্ট প্যাটার্নের উপর নির্ভর না করেই।

  2. পরীক্ষা করুন: উপরের কোড সেলে malicious_username-কে "hasan' OR '1'='1' -- " করে দেখুন — login_insecure এবার কোন অ্যাকাউন্ট ফেরত দেয়?

    login_insecure ইউজার লিস্টে প্রথম যে রো-তে eval_where true হবে সেটাই ফেরত দেবে — যেহেতু '1'='1' অংশটি এখনো row-নিরপেক্ষভাবে সবসময় true, ফলাফল একই থাকে: তালিকার প্রথম ইউজার (এই ক্ষেত্রে "admin")-এর অ্যাকাউন্টে বাইপাস হয়ে যাবে, "hasan" নামটি থাকা সত্ত্বেও। এটি দেখায় যে tautology-ভিত্তিক বাইপাস username-এর সঠিক মান নয়, বরং WHERE ক্লজের যৌক্তিক গঠনের উপর নির্ভর করে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
L19 · A02: ক্রিপ্টোগ্রাফিক ফেইলিওর