Gogs: Kueri database tidak aman

Dibuat pada 27 Mar 2016  ·  39Komentar  ·  Sumber: gogs/gogs

Basis kode Gogs tampaknya sangat tidak konsisten dalam cara menghasilkan kueri. Ada seluruh potongan kode di mana kueri berparameter digunakan bersama kueri string-concatenated, dan ini telah menyebabkan kerentanan injeksi SQL di masa lalu .

Apakah ada alasan khusus mengapa Gogs tidak menggunakan kueri berparameter _everywhere_? Mengingat bahwa semua database utama sekarang mendukung mereka, agak mengejutkan bagi saya bahwa ini belum terjadi, dan membuat saya agak khawatir tentang penerapan Gogs di sistem saya, dari segi keamanan.

🙇‍♂️ help wanted 🚨 security

Komentar yang paling membantu

@tboerger Fakta bahwa ada string-concatenation _at all_ adalah masalahnya di sini. Kueri yang diparameterisasi ada untuk alasan yang sangat bagus - kueri tersebut menghilangkan kebutuhan untuk memikirkan seluruh jalur kode yang dilalui nilai untuk menilai apakah "dapat dimasukkan", dengan memperlakukan nilai sebagai terpisah dari kueri sepenuhnya. Ini sangat penting karena dua alasan:

  1. Manusia membuat kesalahan. Jika Anda membiarkan pengembang melakukan hal yang sama berulang kali, apa pun kompetensinya, mereka _akan_ akan mengacaukannya di beberapa titik. Contoh kasus: berbagai CVE SQLi untuk Gogs di masa lalu.
  2. Asumsi tentang apa isi suatu variabel atau dari mana asalnya, tidak selalu berlaku seiring waktu. Persyaratan berubah, kode berubah, dan di beberapa titik, di suatu tempat, beberapa asumsi ini akan menjadi tidak valid. Dan begitulah masalah keamanan diperkenalkan dalam kode yang sebelumnya aman.

Ide keseluruhan dari query database string-concatenating adalah artefak sejarah; hasil dari fakta bahwa aplikasi yang dihadapi publik tumbuh lebih cepat daripada database, dan aplikasi harus _somehow_ menggunakan masukan pengguna dalam kueri. Ini telah diperbaiki dengan diperkenalkannya kueri berparameter, dan hari ini, sama sekali tidak ada alasan untuk tetap menjadi kueri penggabungan string, dengan semua risiko keamanan yang terkait.

Semua 39 komentar

Kamu benar.

Saya akan mengatakan: PR dipersilakan jika Anda bersedia membantu.

Sayangnya, saya tidak bisa bahasa Go, kalau tidak saya pasti akan mengirimkan PR.

Ini memang tampak seperti sesuatu yang benar-benar perlu diprioritaskan - relatif sedikit usaha (untuk pengembang Go yang berpengalaman) untuk mengesampingkan permukaan serangan yang sangat besar, karena sebagian besar hanya akan mengubah sintaks.

Apakah Anda memiliki contoh yang tidak aman?

Tautan di OP ke issue.go#L805 memberikan contoh yang baik:

    queryStr := "SELECT COUNT(*) FROM `issue` "
    if opts.LabelID > 0 {
        queryStr += "INNER JOIN `issue_label` ON `issue`.id=`issue_label`.issue_id AND `issue_label`.label_id=" + com.ToStr(opts.LabelID)
    }

    baseCond := " WHERE issue.repo_id=" + com.ToStr(opts.RepoID) + " AND issue.is_closed=?"
    if opts.MilestoneID > 0 {
        baseCond += " AND issue.milestone_id=" + com.ToStr(opts.MilestoneID)
    }
    if opts.AssigneeID > 0 {
        baseCond += " AND assignee_id=" + com.ToStr(opts.AssigneeID)
    }
    baseCond += " AND issue.is_pull=?"

com.ToStr naif mengubah semua tipe menjadi string, dan kode di sini hanya menggabungkan string ke dalam kueri SQL - mis.

baseCond := " WHERE issue.repo_id=" + com.ToStr(opts.RepoID) + " AND issue.is_closed=?"

Ini (dan kueri apa pun yang serupa) perlu dijadikan parameter.

Kita tidak perlu membahas apakah itu kode yang baik dari sudut pandang kebersihan atau gaya. Tapi dari segi keamanan, ini adalah contoh yang sangat buruk / naif karena dipaksa menjadi nilai integer, dimasukkan ke dalam string (diperlukan untuk string concat), jadi tidak bisa dilakukan untuk menyuntikkan apa pun di sini.

@tboerger Fakta bahwa ada string-concatenation _at all_ adalah masalahnya di sini. Kueri yang diparameterisasi ada untuk alasan yang sangat bagus - kueri tersebut menghilangkan kebutuhan untuk memikirkan seluruh jalur kode yang dilalui nilai untuk menilai apakah "dapat dimasukkan", dengan memperlakukan nilai sebagai terpisah dari kueri sepenuhnya. Ini sangat penting karena dua alasan:

  1. Manusia membuat kesalahan. Jika Anda membiarkan pengembang melakukan hal yang sama berulang kali, apa pun kompetensinya, mereka _akan_ akan mengacaukannya di beberapa titik. Contoh kasus: berbagai CVE SQLi untuk Gogs di masa lalu.
  2. Asumsi tentang apa isi suatu variabel atau dari mana asalnya, tidak selalu berlaku seiring waktu. Persyaratan berubah, kode berubah, dan di beberapa titik, di suatu tempat, beberapa asumsi ini akan menjadi tidak valid. Dan begitulah masalah keamanan diperkenalkan dalam kode yang sebelumnya aman.

Ide keseluruhan dari query database string-concatenating adalah artefak sejarah; hasil dari fakta bahwa aplikasi yang dihadapi publik tumbuh lebih cepat daripada database, dan aplikasi harus _somehow_ menggunakan masukan pengguna dalam kueri. Ini telah diperbaiki dengan diperkenalkannya kueri berparameter, dan hari ini, sama sekali tidak ada alasan untuk tetap menjadi kueri penggabungan string, dengan semua risiko keamanan yang terkait.

@tboerger Dalam contoh ini, itu benar, tetapi Anda sepenuhnya mengandalkan objek yang Anda berikan ke com.ToStr untuk menjadi tipe yang tidak dapat dibuat menjadi vektor SQLi. Itulah masalahnya: kode di sini rapuh. Jika ini telah diparameterisasi dengan benar, Anda bisa melempar semua tipe melalui com.ToStr dan OK.

Edit: @ joepie91 merangkum ini lebih baik dari yang saya miliki.

@tboerger Dalam contoh ini, itu benar, tetapi Anda sepenuhnya mengandalkan objek yang Anda teruskan ke com.ToStr menjadi tipe yang tidak dapat dibuat menjadi vektor SQLi. Itulah masalahnya: kode di sini rapuh. Jika ini telah diparameterisasi dengan benar, Anda bisa melempar semua jenis melalui com.ToStr dan OK.

Seperti yang saya katakan, gaya atau kode saat ini adalah segalanya selain indah, tetapi kasus khusus ini belum menjadi masalah keamanan.

Ini adalah masalah keamanan, bukan masalah gaya. Seperti yang sudah saya jelaskan di komentar saya sebelumnya.

@ joepie91 poc apapun mungkin untuk menggambarkan pemikiran Anda? Saya akan sangat tertarik untuk proposal pendidikan saja, karena saya sedang mengevaluasi para gogs sekarang

Ini adalah masalah keamanan, bukan masalah gaya. Seperti yang sudah saya jelaskan di komentar saya sebelumnya.

Contoh khusus ini bukanlah masalah keamanan karena Anda tidak dapat memasukkan apa pun. Ini hanya praktik yang sangat buruk dan bukan kode yang bersih.

Terima kasih atas komentarnya!

Jika ada yang bisa menjelaskan bagaimana membuat nilai int dapat diinjeksi, saya akan sangat senang menandai ini sebagai masalah keamanan.

Pembaruan: Saya mencoba menggunakan kueri berparameter sebanyak mungkin, tetapi gagal dalam kasus ini. Jika seseorang dapat melakukan PR yang lebih baik diimplementasikan dengan kueri berparameter, itu adalah hal terbaik yang dapat saya pikirkan. Alih-alih berbicara filosofi. : sweat_smile:

Intinya bukanlah bahwa nilai int itu sendiri dapat diinjeksi, tetapi saat apa pun di hulu kode database ini berubah, Anda berisiko terkena SQLi.

misalnya

- queryStr += "INNER JOIN `issue_label` ON `issue`.id=`issue_label`.issue_id AND `issue_label`.label_id=" + com.ToStr(opts.LabelID)
+ queryStr += "INNER JOIN `issue_label` ON `issue`.id=`issue_label`.issue_id AND `issue_label`.label_id=" + com.ToStr(opts.LabelName) // An injection issue

atau

type IssueStatsOptions struct {
    RepoID      int64
    UserID      int64
-   LabelID     int64
+   LabelID     []byte // Now a UUID type; you could pass a string ID as a query param and it could potentially traverse its way into your query
    MilestoneID int64

Seperti yang ditunjukkan @ joepie91 :

Asumsi tentang apa isi suatu variabel atau dari mana asalnya, tidak selalu berlaku seiring waktu.

Kode di sini (dan di tempat lain, di mana asumsi yang sama diterapkan) secara efektif aman secara tidak sengaja. Bahwa itu membutuhkan waktu yang lama untuk menjelaskan / membahas hal ini sendiri.

Saya rasa saya mungkin perlu memberikan sedikit latar belakang desain database Gogs.

Semuanya diakhiri dengan ID benar-benar int64 di Go, dan bidang utama id dalam tabel database.

UUID disebut UUID , itulah konvensi.

@Unknwon saya mengerti. Masalahnya adalah Anda mengandalkan _never_ memberikan apa pun kecuali int64 sini. Jika kode ini berubah, ada risiko SQLi. Anda berharap bahwa siapa pun yang melakukan (mengirimkan | meninjau) kode terhadap repo ini mengetahui hal itu.

@Unknwon saya mengerti. Masalahnya adalah Anda mengandalkan tidak pernah melewatkan apa pun kecuali int64 di sini. Jika kode ini berubah, ada risiko SQLi. Anda berharap bahwa siapa pun yang melakukan (mengirimkan | meninjau) kode terhadap repo ini mengetahui hal itu.

Ini mengarah ke topik yang sama sekali berbeda dari utas ini, Anda sedang membicarakan peninjauan kode sekarang.

Saya ingin menghapus pernyataan saya lagi:

Jika seseorang dapat melakukan PR yang lebih baik diimplementasikan dengan kueri berparameter, itu adalah hal terbaik yang dapat saya pikirkan. Alih-alih berbicara filosofi.

Bagi mereka yang ingin memperbaiki masalah ini dan mengirimkan PR, saya _think_ perintah berikut akan mengungkap semua contoh penggabungan string dalam kueri:

grep -rE "\+.*ToStr\(" .

(... serta beberapa positif palsu.)

EDIT: Ditambah dua kasus lainnya, tetapi ini mungkin disengaja / tidak dapat dihindari (meskipun masih harus ditangani dengan hati-hati dan curiga):

./models/migrations/migrations.go:          baseSQL := "UPDATE `" + table.name + "` SET "
./models/migrations/migrations.go:                  fieldSQL += col + "_unix = "

@ joepie91 meminta saya untuk melihat sekilas kodenya.

API database Go memiliki cara yang sangat berguna untuk menggunakan pernyataan dan placeholder yang disiapkan. Tapi ini sepertinya dibuang keluar jendela.

Mengapa? Placeholder kueri Go bersifat posisional.

Cara saya melihatnya, penggabungan kueri tampaknya menjadi solusi untuk membangun kueri berparameter yang kompleks. Sesuatu yang jauh lebih sulit dilakukan dengan menggunakan placeholder posisi. Merangkai kueri akan memindahkan tempat penampung tersebut sehingga sulit untuk melacak urutan parameter, sehingga data ditambahkan dalam penggabungan sebagai gantinya.

Untuk mengatasi masalah penggabungan, saya mengusulkan salah satu solusi ini:

1
Tempelkan ketentuan dalam kueri tetapi buat agar dapat diubah oleh parameter.
Sebagai contoh:

if opts.AssigneeID > 0 {
    baseCond += " AND assignee_id=" + com.ToStr(opts.AssigneeID)
}

Bisa ditulis ulang menjadi:

condAssigneeId := opts.AssigneeID > 0

db.Query(`
    SELECT * FROM stuff
    WHERE
    ...
    AND (? OR assignee_id=?)
    ...
`, ..., !condAssigneeId, opts.AssigneeID)

Dengan cara ini, kondisinya diabaikan jika condAssigneeId disetel ke false. Tentu saja, kueri mungkin sedikit lebih lambat karena harus memeriksa lebih banyak kondisi, tetapi database yang cerdas mungkin akan mengoptimalkannya.

2
Gunakan pembuat kueri. Dengan sesuatu seperti GORM itu
mungkin memiliki kueri kompleks yang dibangun menggunakan serangkaian kondisi
dan memiliki manfaat pengganti.

@PolyFloyd tidak dapat ditulis seperti itu di dalam Gogs. Jangan berpikir seperti database / sql, Gogs menggunakan XORM.

Di GORM, kami akan menulis sesuatu seperti ini:

query := dm.Where("foo =  ?", 1)
if cond == 1 {
  query = query.Where("bar = ?", 2)
}
if cond == 2 {
  query = query.Where("baz = ?", 3)
}
// ...

Gogs menggunakan Xorm, bukan Gorm. Saya pikir itu akan mungkin, masalahnya mungkin GABUNG. Saya tidak tahu seberapa baik Gorm dan Xorm menangani JOINS.

Saya pikir XORM membutuhkan penanganan yang tepat untuk kondisi Join. Saat ini ada kode seperti ini:

labelIDs := base.StringsToInt64s(strings.Split(opts.Labels, ","))
if len(labelIDs) > 0 {
  validJoin := false
  queryStr := "issue.id=issue_label.issue_id"
  for _, id := range labelIDs {
    if id == 0 {
      continue
    }
    validJoin = true
    queryStr += " AND issue_label.label_id=" + com.ToStr(id)
  }
  if validJoin {
    sess.Join("INNER", "issue_label", queryStr)
  }
}

Karena Join hanya menerima string sebagai kondisi gabungan, tidak mungkin untuk mengubahnya. Alih-alih mengandalkan string mereka harus menerima kondisi, daripada Anda harus menulis sesuatu seperti ini:

labelIDs := base.StringsToInt64s(strings.Split(opts.Labels, ","))

if len(labelIDs) > 0 {
  validJoin := false
  cond := x.Where("issue.id=issue_label.issue_id")

  for _, id := range labelIDs {
    if id == 0 {
      continue
    }

    validJoin = true
    cond.Where("issue_label.label_id = ?", id)
  }

  if validJoin {
    sess.Join("INNER", "issue_label", cond)
  }
}

Gorm akan menerima sesuatu seperti ini:

query := db.Where("something_id IN (?)", []int{1, 2, 3, 4, 5})

Gorm akan menerima sesuatu seperti ini:

kueri: = db.Where ("sesuatu_id IN (?)", [] int {1, 2, 3, 4, 5})

Dan XORM mendapat In , tetapi tidak untuk kondisi bergabung.

Saya telah membuat https://github.com/gogits/gogs/pull/2893 sebagai awal untuk meningkatkan penanganan parameter. Silakan lihat. Jika Anda ingin melihat lebih banyak di dalamnya, silakan berkontribusi.

Masalah dengan menggunakan pernyataan yang disiapkan dalam program ini adalah kita tidak lagi database agnostik. Misalnya, di MySQL ini adalah pernyataan yang disiapkan:

INSERT INTO this(param) VALUES(?)

Sementara di PgSQL ini adalah pernyataan yang disiapkan:

INSERT INTO this(param) VALUES($1)

Tidak ada cara mudah untuk mengakomodasi kedua database ini tanpa mempertahankan dua versi basis kode. Saya pikir pendekatan saat ini benar-benar dapat diterima ( SAMPAI XORM menaikkan JOIN permainan) karena injeksi SQL tidak dapat dilakukan dalam basis kode saat ini, dan kami menargetkan lebih dari sekedar MySQL.

Pernyataan yang disiapkan dengan gaya MySQL sudah digunakan dalam kode sebelum masalah ini diangkat (misalnya https://github.com/gogits/gogs/blame/master/models/issue.go#L817 ditambahkan kembali pada November tahun lalu). Kapal itu telah berlayar, maaf.

(Saya juga diarahkan ke utas ini oleh @ joepie91 .)

(sunting: Seperti yang @tboerger tunjukkan dengan ramah, saya "salah" tentang ini. Meskipun gaya pernyataan yang disiapkan yang digunakan oleh XORM mirip dengan yang digunakan oleh MySQL, itu hanya kemiripan, dan contoh di atas diterjemahkan oleh XORM ke sintaks sebenarnya apa pun yang digunakan oleh database yang mendasarinya (lihat postgres_dialect.go dan filter.go untuk detail tentang bagaimana ini dilakukan untuk Postgresql.))

Demi kejelasan; karena permintaan PR dan saya tidak berbicara tentang Go sendiri, saya telah bertanya-tanya sedikit untuk mencoba dan menemukan orang yang dapat membantu mengimplementasikan perbaikan untuk ini. Oleh karena itu disebutkan di atas :)

@ barracks510 Saya membayangkan seharusnya relatif mudah untuk mengabstraksikan perbedaan ini. Saya memahami bahwa XORM mungkin bukan fitur lengkap, tetapi bagaimanapun juga, itu _never_ dapat diterima untuk kueri gabungan string, apa pun skenarionya. Ini hanyalah API yang salah untuk digunakan.

Jika perbedaannya seperti yang Anda jelaskan, maka tentunya itu hanya masalah mengganti setiap ? dengan $n secara bertahap tepat sebelum query dijalankan?

@JesseWeinstein Anda salah. Kami tidak menggunakan pernyataan yang disiapkan MySQL, kami hanya menggunakan XORM yang menjadikannya database agnostik.

Sejauh yang saya bisa lihat dengan permintaan tarik saya hanya ada GetRepoIssueStats dan GetUserIssueStats tersisa untuk pemfaktoran ulang untuk menyingkirkan masalah string concat. Perbaikan PR pertama sudah digabungkan, yang berikutnya jika terbuka jadi hanya satu atau dua yang perlu diikuti.

Hai, saya telah menambahkan parameter kondisi bergabung yang didukung di go-xorm / xorm @ 7d2967c78698d206727517c4c3ea318c1564f2c7

@lunny keren banget

Jadi https://github.com/gogits/gogs/pull/2895 harus menyelesaikan semua masalah yang tersisa, harap tinjau itu.

Saya pikir gogs sepenuhnya menggunakan ORM. Tapi di sini menunjukkan TIDAK. Ada pernyataan SQL sebaris.

@linquize dengan permintaan tarik saya murni atau lagi

Semua kueri diparameterisasi sekarang, jadi kita bisa menutup masalah ini sekarang

Terlihat bagus - terima kasih untuk semua pekerjaan dari semua orang, bahkan dari hulu :)

Apa statusnya ini? Saya bisa bercabang dan mengerjakan ini jika belum selesai. Sepertinya itu tidak ditutup?

Apa statusnya ini? Saya bisa bercabang dan mengerjakan ini jika belum selesai. Sepertinya itu tidak ditutup?

https://github.com/gogits/gogs/issues/2892#issuecomment-214821639
Saya sudah memperbaiki masalahnya. Saya tidak bisa menutup masalah ini;)

Menutup sebagai sudah diperbaiki :)

Apakah halaman ini membantu?
0 / 5 - 0 peringkat