Tarkibga o'tish

SQL Injection oldini olish

SQL injection — foydalanuvchi kiritgan matn SQL buyrug‘ining bir qismiga aylanib, uning ma’nosini o‘zgartirib yuboradigan hujum. Natijada hujumchi parolsiz tizimga kiradi, boshqalarning ma’lumotini o‘qiydi yoki butun jadvalni o‘chirib yuboradi.

Bu zaiflik 25 yildan ortiq vaqtdan beri ma’lum. Shunga qaramay, u hanuzgacha OWASP Top 10 ro‘yxatidagi eng xavfli zaifliklar qatorida turibdi. Sababi — uni yo‘l qo‘yish juda oson: bitta fmt.Sprintf yetarli.

Hujum qanday ishlaydi?

Login uchun quyidagi kod yozildi deylik:

query := fmt.Sprintf(
    "SELECT id FROM users WHERE email = '%s' AND password = '%s'",
    email, password,
)

Oddiy foydalanuvchi [email protected] va maxfiy123 kiritadi. Natijada SQL shunday bo‘ladi:

SELECT id FROM users WHERE email = '[email protected]' AND password = 'maxfiy123'

Hammasi to‘g‘ri ishlayapti. Endi hujumchi email maydoniga quyidagi matnni kiritadi:

Satrlar ulanganda:

SELECT id FROM users WHERE email = '[email protected]' --' AND password = '...'

' belgisi satrni yopdi. -- esa SQL’da izoh boshlanishini bildiradi. Parol tekshiruvi izohga aylanib qoldi. Hujumchi Alining akkauntiga parolsiz kirdi.

Yana bir variant. Parol maydoniga ' OR '1'='1 kiritiladi:

SELECT id FROM users WHERE email = 'x' AND password = '' OR '1'='1'

'1'='1' doim rost. Shu sababli so‘rov jadvaldagi barcha foydalanuvchilarni qaytaradi va kod birinchisini, odatda administratorni oladi.

Muammoning ildizi: ma’lumot va buyruq bitta satrga aralashib ketdi. Baza qaysi qism dasturchi yozgan buyruq, qaysi qism foydalanuvchi kiritgan ma’lumot ekanini ajrata olmaydi.

Amalda ko‘ramiz

Quyidagi dastur zaif va xavfsiz funksiyani yonma-yon ko‘rsatadi. U oldingi darslarda ishga tushirilgan PostgreSQL’da ishlaydi.

main.go
package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "os"

    "github.com/jackc/pgx/v5"
    "github.com/jackc/pgx/v5/pgxpool"
)

// ❌ ZAIF: foydalanuvchi matni SQL satriga qo‘shilmoqda
func findUserUnsafe(ctx context.Context, db *pgxpool.Pool, email string) (string, error) {
    query := fmt.Sprintf("SELECT name FROM demo_users WHERE email = '%s'", email)
    var name string
    err := db.QueryRow(ctx, query).Scan(&name)
    return name, err
}

// ✅ XAVFSIZ: qiymat parametr sifatida alohida yuboriladi
func findUserSafe(ctx context.Context, db *pgxpool.Pool, email string) (string, error) {
    var name string
    err := db.QueryRow(ctx, "SELECT name FROM demo_users WHERE email = $1", email).Scan(&name)
    return name, err
}

func main() {
    ctx := context.Background()
    db, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    _, err = db.Exec(ctx, `
        DROP TABLE IF EXISTS demo_users;
        CREATE TABLE demo_users (email TEXT PRIMARY KEY, name TEXT NOT NULL);
        INSERT INTO demo_users VALUES ('[email protected]', 'Administrator'),
                                      ('[email protected]', 'Ali');`)
    if err != nil {
        log.Fatal(err)
    }

    attack := "[email protected]' OR '1'='1"

    name, err := findUserUnsafe(ctx, db, attack)
    fmt.Printf("Zaif funksiya:     name=%q err=%v\n", name, err)

    name, err = findUserSafe(ctx, db, attack)
    if errors.Is(err, pgx.ErrNoRows) {
        err = errors.New("foydalanuvchi topilmadi")
    }
    fmt.Printf("Xavfsiz funksiya:  name=%q err=%v\n", name, err)
}
Zaif funksiya:     name="Administrator" err=<nil>
Xavfsiz funksiya:  name="" err=foydalanuvchi topilmadi

Zaif funksiya hujumchiga administratorni qaytardi. Xavfsiz funksiya esa [email protected]' OR '1'='1 matnini oddiy satr sifatida qabul qildi va shunday emailli foydalanuvchini qidirdi. Tabiiyki, bunday foydalanuvchi topilmadi.

Asosiy himoya: parametrli so‘rovlar

Parametrli so‘rovda SQL matni va qiymatlar bazaga alohida yuboriladi:

db.QueryRow(ctx, "SELECT name FROM users WHERE email = $1", email)
Bazaga yuboriladi:
  1. SQL:       SELECT name FROM users WHERE email = $1
  2. Parametr:  [email protected]' OR '1'='1

Baza avval SQL’ni tahlil qiladi va uning tuzilishini aniqlaydi. Parametr keyin, tayyor tuzilmaga qiymat sifatida joylanadi. Qiymat ichida ', -- yoki OR bo‘lsa ham, ular SQL buyrug‘iga aylanmaydi.

PostgreSQL’da parametrlar $1, $2, $3 ko‘rinishida yoziladi. MySQL va SQLite’da esa ? ishlatiladi.

// PostgreSQL
db.Exec(ctx, "UPDATE books SET title = $1, year = $2 WHERE id = $3", title, year, id)

Danger

Qiymatlarni qo‘lda "tozalash" — ' belgisini '' ga almashtirish yoki strings.ReplaceAll bilan xavfli belgilarni olib tashlash — himoya emas. Kodlashlar, maxsus belgilar va baza versiyalari o‘rtasidagi farqlar tufayli bunday tozalashni aylanib o‘tish yo‘llari doim topiladi. Yagona ishonchli usul — parametrlar.

Parametr bilan bo‘lmaydigan joylar

Parametrlar faqat qiymatlar o‘rnida ishlaydi. Jadval nomi, ustun nomi, ASC/DESC yo‘nalishi kabi SQL identifikatorlari va kalit so‘zlarini parametr qilib bo‘lmaydi:

// ❌ Ishlamaydi: $1 ustun nomi emas, satr qiymati sifatida qabul qilinadi
db.Query(ctx, "SELECT * FROM books ORDER BY $1", sortColumn)

Aynan shu joylarda dasturchilar ko‘pincha fmt.Sprintfga qaytadi va zaiflik paydo bo‘ladi.

Yechim: ruxsat ro‘yxati (allowlist)

Klient ustun nomini emas, kalitni yuboradi. Server esa kalitni oldindan tayyorlab qo‘yilgan SQL bo‘lagiga moslaydi:

var sortColumns = map[string]string{
    "title":  "title",
    "year":   "year",
    "newest": "created_at",
}

func orderBy(sort, dir string) (string, error) {
    col, ok := sortColumns[sort]
    if !ok {
        return "", fmt.Errorf("saralash maydoni noto‘g‘ri: %q", sort)
    }

    direction := "ASC"
    if dir == "desc" {
        direction = "DESC"
    }

    return " ORDER BY " + col + " " + direction, nil
}

SQL’ga qo‘shilayotgan har bir so‘z dasturchi yozgan konstantalardan olinadi. Klient kiritgan matn so‘rovga hech qachon to‘g‘ridan-to‘g‘ri tushmaydi. Ro‘yxatda bo‘lmagan qiymat esa xato bilan rad etiladi.

LIKE bilan qidirish

Qidiruv so‘rovlarida % shabloni qiymatga qo‘shiladi, SQL matniga emas:

// ❌ Zaif
query := "SELECT title FROM books WHERE title ILIKE '%" + q + "%'"

// ✅ Xavfsiz
rows, err := db.Query(ctx, "SELECT title FROM books WHERE title ILIKE $1", "%"+q+"%")

Bu SQL injection’dan himoya qiladi. Lekin yana bir nozik jihat bor: % va _ belgilari LIKE ichida maxsus ma’noga ega. Foydalanuvchi % qidirsa, u barcha qatorlarga mos keladi. Bu xavfsizlik teshigi emas, lekin kutilmagan natija beradi va og‘ir so‘rovga olib kelishi mumkin. Ularni ekranlab qo‘yish yaxshi:

func escapeLike(s string) string {
    r := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`)
    return r.Replace(s)
}

db.Query(ctx, `SELECT title FROM books WHERE title ILIKE $1`, "%"+escapeLike(q)+"%")

PostgreSQL’da \ — LIKE uchun standart ekranlash belgisi.

IN ro‘yxati

Bir nechta ID bo‘yicha qidirishda ko‘pincha satrlarni vergul bilan ulash vasvasasi paydo bo‘ladi:

// ❌ Zaif
query := "SELECT * FROM books WHERE id IN (" + strings.Join(ids, ",") + ")"

PostgreSQL’da massivni bitta parametr sifatida yuborish mumkin. pgx Go slice’ini PostgreSQL massiviga o‘zi aylantiradi:

// ✅ Xavfsiz
ids := []int64{1, 3, 7}
rows, err := db.Query(ctx, "SELECT id, title FROM books WHERE id = ANY($1)", ids)

Dinamik filtrlar

Qidiruv formasida ixtiyoriy filtrlar ko‘p bo‘ladi: muallif, yil oralig‘i, janr. Shartlarni dinamik qo‘shish kerak, lekin qiymatlar baribir parametr bo‘lib qolishi lozim:

main.go
package main

import (
    "fmt"
    "strings"
)

type BookFilter struct {
    Author   string
    YearFrom int
    YearTo   int
    Search   string
}

func escapeLike(s string) string {
    return strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(s)
}

func buildQuery(f BookFilter) (string, []any) {
    var (
        conds []string
        args  []any
    )

    add := func(cond string, value any) {
        args = append(args, value)
        conds = append(conds, fmt.Sprintf(cond, len(args)))
    }

    if f.Author != "" {
        add("author = $%d", f.Author)
    }
    if f.YearFrom > 0 {
        add("year >= $%d", f.YearFrom)
    }
    if f.YearTo > 0 {
        add("year <= $%d", f.YearTo)
    }
    if f.Search != "" {
        add("title ILIKE $%d", "%"+escapeLike(f.Search)+"%")
    }

    query := "SELECT id, title, author, year FROM books"
    if len(conds) > 0 {
        query += " WHERE " + strings.Join(conds, " AND ")
    }
    return query + " ORDER BY id", args
}

func main() {
    q, args := buildQuery(BookFilter{
        Author:   "Abdulla Qodiriy",
        YearFrom: 1920,
        Search:   "kun'; DROP TABLE books; --",
    })
    fmt.Println(q)
    fmt.Printf("%#v\n", args)
}
SELECT id, title, author, year FROM books WHERE author = $1 AND year >= $2 AND title ILIKE $3 ORDER BY id
[]interface {}{"Abdulla Qodiriy", 1920, "%kun'; DROP TABLE books; --%"}

fmt.Sprintf bu yerda ham ishlatildi, lekin faqat parametr raqamini ($1, $2) qo‘yish uchun. Bu raqamni kod o‘zi hisoblaydi. Foydalanuvchi kiritgan matnlar esa faqat args ro‘yxatiga tushadi. Hujum qilishga urinilgan DROP TABLE qatori oddiy qidiruv matni bo‘lib qoldi.

'ހ' — bu 1920 soni. %q formati intni belgi sifatida ko‘rsatadi. Bu yerda muhimi shundaki, qiymat SQL matnida emas, parametrlar ro‘yxatida turibdi.

Qo‘shimcha himoya qatlamlari

Parametrli so‘rovlar asosiy himoya. Lekin xavfsizlik bir necha qatlamdan iborat bo‘lishi kerak:

  • Eng kam huquq tamoyili. Ilova bazaga superuser sifatida ulanmasin. Unga faqat kerakli jadvallarda SELECT, INSERT, UPDATE, DELETE huquqlarini bering. DROP TABLE yoki boshqa bazalarga kirish huquqi bo‘lmasin. Shunda zaiflik topilgan taqdirda ham zarar cheklangan bo‘ladi.
  • Xato xabarlarini yashirish. Baza xatosining matnini klientga yubormang. Unda jadval va ustun nomlari bo‘ladi. Xatoliklarni standart formatda qaytarish darsidagi yondashuv shu muammoni hal qiladi.
  • Kirish validatsiyasi. Email email formatida, ID esa son bo‘lishi kerak. Validatsiya injection’ni to‘liq to‘xtatmaydi, lekin hujum imkoniyatlarini kamaytiradi.
  • Kod tekshiruvi. gosec kabi statik tahlil vositalari SQL satrini birlashtirish holatlarini topib beradi:
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...

Xulosa

  • SQL injection ma’lumot va buyruq bitta satrga aralashganda yuz beradi.
  • Qiymatlarni doim parametr ($1, $2) orqali uzating. SQL matnini fmt.Sprintf yoki + bilan foydalanuvchi ma’lumotidan yig‘mang.
  • Qo‘lda tozalash himoya emas.
  • Ustun nomlari va saralash yo‘nalishi parametr bo‘la olmaydi. Ular faqat ruxsat ro‘yxati orqali tanlanadi.
  • LIKE uchun % qiymatga qo‘shiladi, IN o‘rniga = ANY($1) ishlatiladi.
  • Bazaga ilova uchun kerakli minimal huquqlar bilan ulaning.

Keyingi darsda yana bir muhim mavzu — parollarni bazada xavfsiz saqlashni ko‘ramiz.