Tarkibga o'tish

Input sanitizatsiyasi

API’ga keladigan har qanday ma’lumot — JSON tana, URL, sarlavha, cookie yoki yuklangan fayl — ishonchsiz hisoblanadi. U mobil ilovangizdan kelgan bo‘lsa ham, chunki so‘rovni curl bilan ham yasash mumkin. Bu qoidaning qisqacha ifodasi: "Foydalanuvchi kiritgan ma’lumotga hech qachon ishonmang".

Sanitizatsiya (sanitization) — kiruvchi ma’lumotni tozalash va xavfsiz ko‘rinishga keltirish. Lekin bu so‘z ko‘pincha noto‘g‘ri tushuniladi: "xavfli belgilarni olib tashlasak, hammasi joyida bo‘ladi". Amalda esa himoya uch xil vositadan iborat va har biri o‘z joyida ishlatiladi.

Uch xil vosita

Vosita Nima qiladi Qachon
Validatsiya Qoidaga mos kelmagan ma’lumotni rad etadi Kirishda, har doim
Normallashtirish Ma’lumotni yagona ko‘rinishga keltiradi Kirishda, saqlashdan oldin
Ekranlash (encoding) Ma’lumotni chiqish joyiga mos kodlaydi Chiqishda: SQL, HTML, shell, log

Muhim tamoyil: kirishda tekshiring, chiqishda ekranlang.

Nima uchun "xavfli belgilarni olib tashlash" yetarli emas? Qaysi belgi xavfli ekani ma’lumot qayerga borishiga bog‘liq:

  • SQL uchun ' xavfli;
  • HTML uchun < va > xavfli;
  • shell uchun ;, | va $ xavfli;
  • fayl yo‘li uchun .. va / xavfli.

Kirish paytida ma’lumot keyin qayerda ishlatilishini bilmaysiz. O'Brien familiyasidagi ' belgisini olib tashlasangiz, ism buziladi. Uni saqlab, SQL’da parametr sifatida, HTML’da esa html/template orqali ishlatsangiz, ism ham to‘g‘ri qoladi, xavf ham bo‘lmaydi.

1. Validatsiya: ruxsat ro‘yxati

Validatsiyaning ikki yondashuvi bor:

  • denylist (taqiqlar ro‘yxati) — "<script> bo‘lmasin". Hujumchi ro‘yxatda yo‘q variantni topadi: <ScRiPt>, <img onerror=...> va hokazo;
  • allowlist (ruxsat ro‘yxati) — "faqat harf, raqam va _". Ro‘yxatda yo‘q hamma narsa rad etiladi.

Iloji bo‘lgan joyda doim allowlist tanlang:

var usernameRe = regexp.MustCompile(`^[a-z0-9_]{3,20}$`)

if !usernameRe.MatchString(username) {
    return errors.New("login 3–20 ta kichik lotin harfi, raqam yoki _ belgisidan iborat bo‘lishi kerak")
}

Validatsiya qoidalarini go-playground/validator bilan yozish qulay: oneof, alphanum, email, uuid4, min, max.

2. Normallashtirish

Bir xil ma’noli ma’lumot turli ko‘rinishda kelishi mumkin. Normallashtirish uni bitta ko‘rinishga keltiradi.

func cleanText(s string, maxRunes int) (string, error) {
    if !utf8.ValidString(s) {
        return "", errors.New("matn UTF-8 formatida emas")
    }

    s = strings.Map(func(r rune) rune {
        switch {
        case r == '\n' || r == '\t':
            return r // ko‘p qatorli matn uchun ruxsat
        case unicode.IsControl(r):
            return -1 // boshqaruv belgilari olib tashlanadi
        case unicode.Is(unicode.Cf, r):
            return -1 // ko‘rinmas formatlash belgilari: ​, ‮ ...
        }
        return r
    }, s)

    s = strings.TrimSpace(s)

    if utf8.RuneCountInString(s) > maxRunes {
        return "", fmt.Errorf("matn %d belgidan oshmasligi kerak", maxRunes)
    }
    return s, nil
}

Nimalar tozalanadi va nima uchun:

  • Noto‘g‘ri UTF-8. Buzilgan baytlar bazaga yozishda xato beradi yoki boshqa tizimlarda kutilmagan natija chiqaradi.
  • Boshqaruv belgilari (\x00, \x1b va boshqalar). \x00 ba’zi tizimlarda satrni kesib qo‘yadi. \x1b terminal buyruqlarini boshlaydi, shuning uchun loglarni terminalda ko‘rganda kutilmagan natija berishi mumkin.
  • Ko‘rinmas belgilar. ​ (nol kenglikdagi bo‘shliq) admin va adm​in satrlarini ko‘zga bir xil ko‘rsatadi. ‮ esa matn yo‘nalishini teskari qiladi: rekcah.exe fayl nomi exe.hacker bo‘lib ko‘rinishi mumkin.
  • Uzunlik belgilarda o‘lchanadi. len() baytlarni sanaydi. "O‘zbekiston" satri 11 belgidan iborat, lekin 13 bayt.

Email va login uchun qo‘shimcha ravishda harflar kichik qilinadi: strings.ToLower. Bu Login / Signup darsida ko‘rilgan.

Info

Unicode’da bitta harf turli usullarda yozilishi mumkin: é bitta belgi sifatida ham, e va urg‘u belgisi jufti sifatida ham. Ular ko‘zga bir xil ko‘rinadi, lekin baytlari farq qiladi. Bunday matnlarni solishtirish yoki noyoblikni tekshirish muhim bo‘lsa, golang.org/x/text/unicode/norm paketi bilan NFC shakliga keltiring: norm.NFC.String(s).

3. Fayl yo‘llari: path traversal

Foydalanuvchi bergan nom bilan fayl ochish — eng xavfli joylardan biri:

// ❌ XAVFLI
path := filepath.Join("uploads", r.PathValue("name"))
http.ServeFile(w, r, path)

name qiymati ../../etc/passwd bo‘lsa, filepath.Join natijasi ../etc/passwd bo‘ladi. Server uploads katalogidan chiqib, tizim fayliga yetib boradi. Bu hujum path traversal deb ataladi.

Go 1.24 versiyasida bu muammo uchun maxsus vosita qo‘shildi: os.Root. U fayl tizimiga murojaatni bitta katalog bilan cheklaydi. Na .., na absolyut yo‘l, na katalogdan tashqariga ishora qiluvchi symlink undan chiqib keta oladi:

root, err := os.OpenRoot("uploads")
if err != nil {
    log.Fatal(err)
}
defer root.Close()

f, err := root.Open(name) // ../../etc/passwd → xato

os.Rootdan tashqari yana ikki qoida bor:

  • fayllarni foydalanuvchi bergan nom bilan saqlamang. Diskda tasodifiy ID bilan saqlang, asl nomni esa bazada alohida yozib qo‘ying;
  • statik fayllarni tarqatish uchun http.FileServer yoki http.FileServerFS ishlating. Ular .. segmentlarini o‘zi tozalaydi.

4. Tashqi buyruqlar

Ba’zan server tashqi dasturni ishga tushiradi: rasmni kichraytirish, PDF yaratish va hokazo.

// ❌ XAVFLI: satr shell orqali bajariladi
exec.Command("sh", "-c", "convert "+filename+" -resize 200x200 out.png")

// ✅ XAVFSIZ: argumentlar alohida uzatiladi, shell yo‘q
exec.Command("convert", "--", filename, "-resize", "200x200", "out.png")

Birinchi variantda filename qiymati x.png; rm -rf / bo‘lsa, shell ikkita buyruqni bajaradi. Ikkinchisida esa exec.Command dasturni to‘g‘ridan-to‘g‘ri ishga tushiradi va har bir argumentni o‘zgartirmasdan uzatadi. ; belgisi oddiy belgi bo‘lib qoladi.

-- belgisi "bundan keyin faqat fayl nomlari keladi" degan ma’noni bildiradi. U -rf kabi - bilan boshlanadigan nom dasturning parametri deb tushunilishining oldini oladi.

5. Log injection

Foydalanuvchi ma’lumoti logga yozilganda ham ehtiyot bo‘lish kerak:

log.Printf("login urinishi: %s", username)

username qiymati ali\n2026/09/30 10:00:00 login muvaffaqiyatli: admin bo‘lsa, logda soxta qator paydo bo‘ladi. Loglarni tahlil qiladigan odam yoki dastur chalg‘iydi.

Yechim — tuzilgan loglar. slog.JSONHandler qiymatlarni JSON satri sifatida yozadi va \n belgisini ekranlaydi:

slog.Info("login urinishi", "username", username)
// {"msg":"login urinishi","username":"ali\n2026/09/30 ... admin"}

Soxta qator paydo bo‘lmadi, u bitta maydon ichida qoldi.

6. Fayl yuklash

Fayl yuklash endpointi uchun qoidalar ro‘yxati:

func upload(w http.ResponseWriter, r *http.Request) {
    r.Body = http.MaxBytesReader(w, r.Body, 5<<20) // 5 MB

    file, _, err := r.FormFile("avatar")
    if err != nil {
        http.Error(w, "fayl topilmadi yoki juda katta", http.StatusBadRequest)
        return
    }
    defer file.Close()

    head := make([]byte, 512)
    n, _ := io.ReadFull(file, head)
    contentType := http.DetectContentType(head[:n])

    allowed := map[string]string{"image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp"}
    ext, ok := allowed[contentType]
    if !ok {
        http.Error(w, "faqat PNG, JPEG yoki WebP", http.StatusUnsupportedMediaType)
        return
    }

    name := randomName() + ext // crypto/rand asosidagi nom; foydalanuvchi bergan nom ishlatilmaydi
    // ... faylni root.Create(name) bilan saqlash
}
  • Hajm cheklanadi, ham butun so‘rov, ham fayl uchun.
  • Turi tarkibiga qarab aniqlanadi. Fayl kengaytmasiga ham, klient yuborgan Content-Type sarlavhasiga ham ishonib bo‘lmaydi: virus.exe faylini cat.png deb nomlash oson. http.DetectContentType faylning birinchi 512 baytiga qaraydi.
  • Nom server tomonidan yaratiladi.
  • Fayllar veb-server kodi bilan bir katalogda emas, alohida joyda saqlanadi. Ular tarqatilganda Content-Disposition: attachment va X-Content-Type-Options: nosniff sarlavhalari qo‘yiladi.

7. Boy matn (HTML) qabul qilish

Ba’zan foydalanuvchiga formatlangan matn kiritishga ruxsat berish kerak bo‘ladi: blog maqolasi yoki izohdagi qalin matn va havolalar. Bu holatda HTML’ni ekranlab bo‘lmaydi, aks holda formatlash yo‘qoladi. HTML’ni qo‘lda regex bilan tozalash esa deyarli har doim chetlab o‘tiladi.

Buning uchun maxsus kutubxonalar ishlatiladi. Go’da keng tarqalgani — github.com/microcosm-cc/bluemonday. U ruxsat berilgan teglar va atributlardan boshqa hammasini olib tashlaydi:

p := bluemonday.UGCPolicy() // foydalanuvchi kontenti uchun tayyor siyosat
safe := p.Sanitize(`<b>Salom</b><script>alert(1)</script><a href="javascript:x">link</a>`)
// <b>Salom</b>link

Undan ham soddaroq yo‘l — HTML o‘rniga Markdown qabul qilish va uni serverda xavfsiz HTML’ga aylantirish.

To‘liq misol

main.go
package main

import (
    "errors"
    "fmt"
    "log"
    "os"
    "path/filepath"
    "strings"
    "unicode"
    "unicode/utf8"
)

func cleanText(s string, maxRunes int) (string, error) {
    if !utf8.ValidString(s) {
        return "", errors.New("matn UTF-8 formatida emas")
    }
    s = strings.Map(func(r rune) rune {
        switch {
        case r == '\n' || r == '\t':
            return r
        case unicode.IsControl(r), unicode.Is(unicode.Cf, r):
            return -1
        }
        return r
    }, s)
    s = strings.TrimSpace(s)
    if utf8.RuneCountInString(s) > maxRunes {
        return "", fmt.Errorf("matn %d belgidan oshmasligi kerak", maxRunes)
    }
    return s, nil
}

func main() {
    // ---------- Matnni tozalash ----------
    inputs := []string{
        "  Salom, dunyo!  ",
        "adm​in",
        "fayl‮gpj.exe",
        "satr\x00kesildi\x1b[31m",
        "O‘zbekiston",
        "\xff\xfe",
    }
    for _, in := range inputs {
        out, err := cleanText(in, 20)
        fmt.Printf("%-28q → %q err=%v\n", in, out, err)
    }

    // ---------- Path traversal ----------
    dir, _ := os.MkdirTemp("", "uploads")
    defer os.RemoveAll(dir)
    os.WriteFile(filepath.Join(dir, "avatar.png"), []byte("rasm"), 0o600)

    root, err := os.OpenRoot(dir)
    if err != nil {
        log.Fatal(err)
    }
    defer root.Close()

    fmt.Println()
    for _, name := range []string{"avatar.png", "../../etc/passwd", "/etc/passwd"} {
        f, err := root.Open(name)
        if err != nil {
            fmt.Printf("%-20q → rad etildi: %v\n", name, errors.Unwrap(err))
            continue
        }
        f.Close()
        fmt.Printf("%-20q → ochildi\n", name)
    }
}
"  Salom, dunyo!  "          → "Salom, dunyo!" err=<nil>
"adm​in"                → "admin" err=<nil>
"fayl‮gpj.exe"          → "faylgpj.exe" err=<nil>
"satr\x00kesildi\x1b[31m"    → "satrkesildi[31m" err=<nil>
"O‘zbekiston"                → "O‘zbekiston" err=<nil>
"\xff\xfe"                   → "" err=matn UTF-8 formatida emas

"avatar.png"         → ochildi
"../../etc/passwd"   → rad etildi: path escapes from parent
"/etc/passwd"        → rad etildi: path escapes from parent
  • Ko‘rinmas belgilar va boshqaruv belgilari olib tashlandi. \x1b belgisi olib tashlangach, qolgan [31m endi oddiy matn va terminalga ta’sir qilmaydi.
  • Buzilgan UTF-8 rad etildi.
  • os.Root katalogdan tashqariga chiqishga urinishlarni to‘xtatdi.

Xulosa

  • Kiruvchi ma’lumotning barchasi ishonchsiz. Bunga sarlavhalar, cookie’lar va fayllar ham kiradi.
  • Kirishda validatsiya (allowlist) va normallashtirish, chiqishda esa joyga mos ekranlash qilinadi.
  • "Xavfli belgilarni olib tashlash" universal himoya emas. Xavf ma’lumot qayerga borishiga bog‘liq.
  • Boshqaruv va ko‘rinmas belgilarni tozalang, uzunlikni belgilarda o‘lchang.
  • Fayl yo‘llari uchun os.Root, buyruqlar uchun shell’siz exec.Command, loglar uchun slog ishlating.
  • Yuklangan fayl turini tarkibiga qarab aniqlang va nomini server tomonida yarating.
  • Boy matn uchun bluemonday kabi tayyor sanitizer ishlating.

Keyingi darsda parolni tanlab topishga qaratilgan brute-force hujumlaridan himoyalanishni ko‘ramiz.