Setiap beberapa tahun, Open Web Application Security Project (OWASP) memberi peringkat pada risiko keamanan aplikasi web yang paling kritis. Sejak laporan pertama, risiko injeksi selalu di atas. Di antara semua jenis injeksi, injeksi SQL adalah salah satu vektor serangan yang paling umum, dan bisa dibilang yang paling berbahaya. Karena Python adalah salah satu bahasa pemrograman paling populer di dunia, mengetahui cara melindungi dari injeksi SQL Python sangat penting.
Dalam tutorial ini, Anda akan mempelajari:
- Apa Injeksi Python SQL itu dan cara pencegahannya
- Cara menulis kueri dengan literal dan pengidentifikasi sebagai parameter
- Cara menjalankan kueri dengan aman dalam basis data
Tutorial ini cocok untuk pengguna semua mesin database . Contoh di sini menggunakan PostgreSQL, tetapi hasilnya dapat direproduksi dalam sistem manajemen basis data lain (seperti SQLite, MySQL, Microsoft SQL Server, Oracle, dan sebagainya).
Bonus Gratis: 5 Thoughts On Python Mastery, kursus gratis untuk developer Python yang menunjukkan peta jalan dan pola pikir yang Anda perlukan untuk meningkatkan keterampilan Python Anda.
Memahami Injeksi SQL Python
Serangan SQL Injection adalah kerentanan keamanan yang umum sehingga xkcd legendary yang legendaris webcomic mendedikasikan komik untuk itu:
Membuat dan mengeksekusi kueri SQL adalah tugas umum. Namun, perusahaan di seluruh dunia sering membuat kesalahan fatal dalam menyusun pernyataan SQL. Sementara lapisan ORM biasanya menyusun kueri SQL, terkadang Anda harus menulisnya sendiri.
Saat Anda menggunakan Python untuk mengeksekusi kueri ini langsung ke database, ada kemungkinan Anda bisa membuat kesalahan yang dapat membahayakan sistem Anda. Dalam tutorial ini, Anda akan mempelajari cara berhasil mengimplementasikan fungsi yang menyusun kueri SQL dinamis tanpa menempatkan sistem Anda pada risiko injeksi Python SQL.
Menyiapkan Basis Data
Untuk memulai, Anda akan menyiapkan database PostgreSQL baru dan mengisinya dengan data. Sepanjang tutorial, Anda akan menggunakan database ini untuk menyaksikan secara langsung cara kerja injeksi Python SQL.
Membuat Basis Data
Pertama, buka shell Anda dan buat database PostgreSQL baru milik pengguna postgres
:
$ createdb -O postgres psycopgtest
Di sini Anda menggunakan opsi baris perintah -O
untuk mengatur pemilik database ke pengguna postgres
. Anda juga menentukan nama database, yaitu psycopgtest
.
Catatan: postgres
adalah pengguna khusus , yang biasanya Anda pesan untuk tugas administratif, tetapi untuk tutorial ini, boleh saja menggunakan postgres
. Namun, dalam sistem nyata, Anda harus membuat pengguna terpisah untuk menjadi pemilik database.
Basis data baru Anda siap digunakan! Anda dapat menghubungkannya menggunakan psql
:
$ psql -U postgres -d psycopgtest
psql (11.2, server 10.5)
Type "help" for help.
Anda sekarang terhubung ke database psycopgtest
sebagai pengguna postgres
. Pengguna ini juga pemilik database, jadi Anda akan memiliki izin membaca di setiap tabel dalam database.
Membuat Tabel Dengan Data
Selanjutnya, Anda perlu membuat tabel dengan beberapa informasi pengguna dan menambahkan data ke dalamnya:
psycopgtest=# CREATE TABLE users (
username varchar(30),
admin boolean
);
CREATE TABLE
psycopgtest=# INSERT INTO users
(username, admin)
VALUES
('ran', true),
('haki', false);
INSERT 0 2
psycopgtest=# SELECT * FROM users;
username | admin
----------+-------
ran | t
haki | f
(2 rows)
Tabel memiliki dua kolom:username
dan admin
. admin
kolom menunjukkan apakah pengguna memiliki hak administratif atau tidak. Sasaran Anda adalah menargetkan admin
dan coba menyalahgunakannya.
Menyiapkan Lingkungan Virtual Python
Sekarang setelah Anda memiliki database, saatnya untuk mengatur lingkungan Python Anda. Untuk petunjuk langkah demi langkah tentang cara melakukannya, lihat Python Virtual Environments:A Primer.
Buat lingkungan virtual Anda di direktori baru:
(~/src) $ mkdir psycopgtest
(~/src) $ cd psycopgtest
(~/src/psycopgtest) $ python3 -m venv venv
Setelah Anda menjalankan perintah ini, direktori baru bernama venv
akan dibuat. Direktori ini akan menyimpan semua paket yang Anda instal di dalam lingkungan virtual.
Menghubungkan ke Basis Data
Untuk terhubung ke database dengan Python, Anda memerlukan adaptor database . Sebagian besar adaptor database mengikuti versi 2.0 dari Spesifikasi API Database Python PEP 249. Setiap mesin database utama memiliki adaptor terkemuka:
Database | Adaptor |
---|---|
PostgreSQL | Psikopg |
SQLite | sqlite3 |
Oracle | cx_Oracle |
MySql | MySQLdb |
Untuk terhubung ke database PostgreSQL, Anda harus menginstal Psycopg, yang merupakan adaptor paling populer untuk PostgreSQL dengan Python. Django ORM menggunakannya secara default, dan juga didukung oleh SQLAlchemy.
Di terminal Anda, aktifkan lingkungan virtual dan gunakan pip
untuk menginstal psycopg
:
(~/src/psycopgtest) $ source venv/bin/activate
(~/src/psycopgtest) $ python -m pip install psycopg2>=2.8.0
Collecting psycopg2
Using cached https://....
psycopg2-2.8.2.tar.gz
Installing collected packages: psycopg2
Running setup.py install for psycopg2 ... done
Successfully installed psycopg2-2.8.2
Sekarang Anda siap untuk membuat koneksi ke database Anda. Inilah awal skrip Python Anda:
import psycopg2
connection = psycopg2.connect(
host="localhost",
database="psycopgtest",
user="postgres",
password=None,
)
connection.set_session(autocommit=True)
Anda menggunakan psycopg2.connect()
untuk membuat koneksi. Fungsi ini menerima argumen berikut:
-
host
adalah alamat IP atau DNS server tempat database Anda berada. Dalam hal ini, host adalah mesin lokal Anda, ataulocalhost
. -
database
adalah nama database untuk terhubung. Anda ingin terhubung ke database yang Anda buat sebelumnya,psycopgtest
. -
user
adalah pengguna dengan izin untuk database. Dalam hal ini, Anda ingin terhubung ke database sebagai pemilik, jadi Anda memberikanpostgres
pengguna . -
password
adalah kata sandi untuk siapa pun yang Anda tentukan diuser
. Di sebagian besar lingkungan pengembangan, pengguna dapat terhubung ke database lokal tanpa kata sandi.
Setelah menyiapkan koneksi, Anda mengonfigurasi sesi dengan autocommit=True
. Mengaktifkan autocommit
berarti Anda tidak perlu mengelola transaksi secara manual dengan mengeluarkan commit
atau rollback
. Ini adalah perilaku default kebanyakan ORM. Anda juga menggunakan perilaku ini di sini sehingga Anda dapat fokus menyusun kueri SQL daripada mengelola transaksi.
Catatan: Pengguna Django bisa mendapatkan contoh koneksi yang digunakan oleh ORM dari django.db.connection
:
from django.db import connection
Menjalankan Kueri
Sekarang setelah Anda memiliki koneksi ke database, Anda siap untuk menjalankan kueri:
>>>>>> with connection.cursor() as cursor:
... cursor.execute('SELECT COUNT(*) FROM users')
... result = cursor.fetchone()
... print(result)
(2,)
Anda menggunakan connection
objek untuk membuat cursor
. Sama seperti file dalam Python, cursor
diimplementasikan sebagai manajer konteks. Saat Anda membuat konteks, cursor
dibuka untuk Anda gunakan untuk mengirim perintah ke database. Saat konteks keluar, cursor
ditutup dan Anda tidak dapat menggunakannya lagi.
Catatan: Untuk mempelajari lebih lanjut tentang pengelola konteks, lihat Pengelola Konteks Python dan Pernyataan “dengan”.
Saat berada di dalam konteks, Anda menggunakan cursor
untuk mengeksekusi kueri dan mengambil hasilnya. Dalam hal ini, Anda mengeluarkan kueri untuk menghitung baris di user
meja. Untuk mengambil hasil dari kueri, Anda mengeksekusi cursor.fetchone()
dan menerima tupel. Karena kueri hanya dapat mengembalikan satu hasil, Anda menggunakan fetchone()
. Jika kueri mengembalikan lebih dari satu hasil, maka Anda harus mengulangi cursor
atau gunakan salah satu fetch*
. lainnya metode.
Menggunakan Parameter Kueri dalam SQL
Di bagian sebelumnya, Anda membuat database, membuat koneksi ke database, dan menjalankan kueri. Kueri yang Anda gunakan adalah statis . Dengan kata lain, ia tidak memiliki parameter . Sekarang Anda akan mulai menggunakan parameter dalam kueri Anda.
Pertama, Anda akan menerapkan fungsi yang memeriksa apakah pengguna adalah admin atau bukan. is_admin()
menerima nama pengguna dan mengembalikan status admin pengguna tersebut:
# BAD EXAMPLE. DON'T DO THIS!
def is_admin(username: str) -> bool:
with connection.cursor() as cursor:
cursor.execute("""
SELECT
admin
FROM
users
WHERE
username = '%s'
""" % username)
result = cursor.fetchone()
admin, = result
return admin
Fungsi ini mengeksekusi kueri untuk mengambil nilai admin
kolom untuk nama pengguna yang diberikan. Anda menggunakan fetchone()
untuk mengembalikan Tuple dengan satu hasil. Kemudian, Anda membongkar tuple ini ke dalam variabel admin
. Untuk menguji fungsi Anda, periksa beberapa nama pengguna:
>>> is_admin('haki')
False
>>> is_admin('ran')
True
Sejauh ini baik. Fungsi mengembalikan hasil yang diharapkan untuk kedua pengguna. Tapi bagaimana dengan pengguna yang tidak ada? Lihatlah traceback Python ini:
>>>>>> is_admin('foo')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 12, in is_admin
TypeError: cannot unpack non-iterable NoneType object
Saat pengguna tidak ada, TypeError
dibangkitkan. Ini karena .fetchone()
mengembalikan None
ketika tidak ada hasil yang ditemukan, dan membongkar None
memunculkan TypeError
. Satu-satunya tempat Anda dapat membongkar tuple adalah tempat Anda mengisi admin
dari result
.
Untuk menangani pengguna yang tidak ada, buat kasus khusus ketika result
adalah None
:
# BAD EXAMPLE. DON'T DO THIS!
def is_admin(username: str) -> bool:
with connection.cursor() as cursor:
cursor.execute("""
SELECT
admin
FROM
users
WHERE
username = '%s'
""" % username)
result = cursor.fetchone()
if result is None:
# User does not exist
return False
admin, = result
return admin
Di sini, Anda telah menambahkan kasus khusus untuk menangani None
. Jika username
tidak ada, maka fungsi tersebut harus mengembalikan False
. Sekali lagi, uji fungsi pada beberapa pengguna:
>>> is_admin('haki')
False
>>> is_admin('ran')
True
>>> is_admin('foo')
False
Besar! Fungsi ini sekarang dapat menangani nama pengguna yang tidak ada juga.
Mengeksploitasi Parameter Kueri Dengan Python SQL Injection
Pada contoh sebelumnya, Anda menggunakan interpolasi string untuk menghasilkan kueri. Kemudian, Anda mengeksekusi kueri dan mengirim string yang dihasilkan langsung ke database. Namun, ada sesuatu yang mungkin Anda abaikan selama proses ini.
Pikirkan kembali username
argumen yang Anda berikan ke is_admin()
. Apa sebenarnya yang diwakili oleh variabel ini? Anda mungkin berasumsi bahwa username
hanyalah string yang mewakili nama pengguna yang sebenarnya. Namun, seperti yang akan Anda lihat, penyusup dapat dengan mudah mengeksploitasi pengawasan semacam ini dan menyebabkan kerugian besar dengan melakukan injeksi Python SQL.
Coba periksa apakah pengguna berikut adalah admin atau bukan:
>>>>>> is_admin("'; select true; --")
True
Tunggu… Apa yang baru saja terjadi?
Mari kita lihat lagi implementasinya. Cetak kueri aktual yang sedang dieksekusi di database:
>>>>>> print("select admin from users where username = '%s'" % "'; select true; --")
select admin from users where username = ''; select true; --'
Teks yang dihasilkan berisi tiga pernyataan. Untuk memahami dengan tepat cara kerja injeksi Python SQL, Anda perlu memeriksa setiap bagian satu per satu. Pernyataan pertama adalah sebagai berikut:
select admin from users where username = '';
Ini adalah kueri yang Anda maksudkan. Titik koma (;
) mengakhiri kueri, jadi hasil kueri ini tidak masalah. Selanjutnya adalah pernyataan kedua:
select true;
Pernyataan ini dibuat oleh penyusup. Ini dirancang untuk selalu mengembalikan True
.
Terakhir, Anda melihat sedikit kode ini:
--'
Cuplikan ini meredakan apa pun yang muncul setelahnya. Penyusup menambahkan simbol komentar (--
) untuk mengubah semua yang mungkin Anda masukkan setelah placeholder terakhir menjadi komentar.
Saat Anda menjalankan fungsi dengan argumen ini, itu akan selalu mengembalikan True
. Jika, misalnya, Anda menggunakan fungsi ini di halaman login Anda, penyusup bisa login dengan nama pengguna '; select true; --
, dan mereka akan diberikan akses.
Jika Anda pikir ini buruk, itu bisa menjadi lebih buruk! Penyusup yang mengetahui struktur tabel Anda dapat menggunakan injeksi Python SQL untuk menyebabkan kerusakan permanen. Misalnya, penyusup dapat menyuntikkan pernyataan pembaruan untuk mengubah informasi dalam database:
>>>>>> is_admin('haki')
False
>>> is_admin("'; update users set admin = 'true' where username = 'haki'; select true; --")
True
>>> is_admin('haki')
True
Mari kita urai lagi:
';
Cuplikan ini mengakhiri kueri, seperti pada injeksi sebelumnya. Pernyataan selanjutnya adalah sebagai berikut:
update users set admin = 'true' where username = 'haki';
Bagian ini memperbarui admin
untuk true
untuk pengguna haki
.
Terakhir, ada cuplikan kode ini:
select true; --
Seperti pada contoh sebelumnya, bagian ini mengembalikan true
dan mengomentari semua yang mengikutinya.
Mengapa ini lebih buruk? Nah, jika penyusup berhasil menjalankan fungsi dengan input ini, maka pengguna haki
akan menjadi admin:
psycopgtest=# select * from users;
username | admin
----------+-------
ran | t
haki | t
(2 rows)
Penyusup tidak lagi harus menggunakan peretasan. Mereka cukup masuk dengan nama pengguna haki
. (Jika penyusup benar-benar ingin membahayakan, maka mereka bahkan dapat mengeluarkan DROP DATABASE
perintah.)
Sebelum Anda lupa, pulihkan haki
kembali ke keadaan semula:
psycopgtest=# update users set admin = false where username = 'haki';
UPDATE 1
Jadi mengapa hal ini terjadi? Nah, apa yang kamu ketahui tentang username
argumen? Anda tahu itu harus berupa string yang mewakili nama pengguna, tetapi Anda tidak benar-benar memeriksa atau menerapkan pernyataan ini. Ini bisa berbahaya! Ini persis seperti yang dicari penyerang saat mereka mencoba meretas sistem Anda.
Membuat Parameter Kueri Aman
Di bagian sebelumnya, Anda melihat bagaimana penyusup dapat mengeksploitasi sistem Anda dan mendapatkan izin admin dengan menggunakan string yang dibuat dengan cermat. Masalahnya adalah Anda mengizinkan nilai yang diteruskan dari klien untuk dieksekusi langsung ke database, tanpa melakukan pemeriksaan atau validasi apa pun. Injeksi SQL bergantung pada jenis kerentanan ini.
Setiap kali input pengguna digunakan dalam kueri database, ada kemungkinan kerentanan untuk injeksi SQL. Kunci untuk mencegah injeksi Python SQL adalah memastikan nilainya digunakan seperti yang diinginkan pengembang. Pada contoh sebelumnya, Anda bermaksud untuk username
untuk digunakan sebagai tali. Pada kenyataannya, itu digunakan sebagai pernyataan SQL mentah.
Untuk memastikan nilai digunakan sebagaimana mestinya, Anda harus melarikan diri nilai. Misalnya, untuk mencegah penyusup menyuntikkan SQL mentah sebagai pengganti argumen string, Anda dapat menghindari tanda kutip:
>>>>>> # BAD EXAMPLE. DON'T DO THIS!
>>> username = username.replace("'", "''")
Ini hanya salah satu contoh. Ada banyak karakter dan skenario khusus yang harus dipikirkan ketika mencoba mencegah injeksi Python SQL. Beruntung bagi Anda, adaptor database modern, hadir dengan alat bawaan untuk mencegah injeksi Python SQL dengan menggunakan parameter kueri . Ini digunakan sebagai pengganti interpolasi string biasa untuk membuat kueri dengan parameter.
Catatan: Adaptor, database, dan bahasa pemrograman yang berbeda merujuk ke parameter kueri dengan nama yang berbeda. Nama umum termasuk variabel pengikat , variabel pengganti , dan variabel substitusi .
Sekarang setelah Anda memiliki pemahaman yang lebih baik tentang kerentanan, Anda siap untuk menulis ulang fungsi menggunakan parameter kueri alih-alih interpolasi string:
1def is_admin(username: str) -> bool:
2 with connection.cursor() as cursor:
3 cursor.execute("""
4 SELECT
5 admin
6 FROM
7 users
8 WHERE
9 username = %(username)s
10 """, {
11 'username': username
12 })
13 result = cursor.fetchone()
14
15 if result is None:
16 # User does not exist
17 return False
18
19 admin, = result
20 return admin
Inilah yang berbeda dalam contoh ini:
-
Pada baris 9, Anda menggunakan parameter bernama
username
untuk menunjukkan di mana nama pengguna harus pergi. Perhatikan bagaimana parameterusername
tidak lagi dikelilingi oleh tanda kutip tunggal. -
Pada baris 11, Anda melewati nilai
username
sebagai argumen kedua untukcursor.execute()
. Koneksi akan menggunakan jenis dan nilaiusername
saat menjalankan kueri dalam database.
Untuk menguji fungsi ini, coba beberapa nilai valid dan tidak valid, termasuk string berbahaya dari sebelumnya:
>>>>>> is_admin('haki')
False
>>> is_admin('ran')
True
>>> is_admin('foo')
False
>>> is_admin("'; select true; --")
False
Luar biasa! Fungsi mengembalikan hasil yang diharapkan untuk semua nilai. Terlebih lagi, string berbahaya tidak lagi berfungsi. Untuk memahami alasannya, Anda dapat memeriksa kueri yang dihasilkan oleh execute()
:
>>> with connection.cursor() as cursor:
... cursor.execute("""
... SELECT
... admin
... FROM
... users
... WHERE
... username = %(username)s
... """, {
... 'username': "'; select true; --"
... })
... print(cursor.query.decode('utf-8'))
SELECT
admin
FROM
users
WHERE
username = '''; select true; --'
Koneksi memperlakukan nilai username
sebagai string dan lolos dari karakter apa pun yang mungkin menghentikan string dan memperkenalkan injeksi Python SQL.
Meneruskan Parameter Kueri Aman
Adaptor database biasanya menawarkan beberapa cara untuk melewatkan parameter kueri. Tempat penampung yang disebutkan biasanya yang terbaik untuk keterbacaan, tetapi beberapa implementasi mungkin mendapat manfaat dari penggunaan opsi lain.
Mari kita lihat sekilas beberapa cara yang benar dan salah dalam menggunakan parameter kueri. Blok kode berikut menunjukkan jenis kueri yang ingin Anda hindari:
# BAD EXAMPLES. DON'T DO THIS!
cursor.execute("SELECT admin FROM users WHERE username = '" + username + '");
cursor.execute("SELECT admin FROM users WHERE username = '%s' % username);
cursor.execute("SELECT admin FROM users WHERE username = '{}'".format(username));
cursor.execute(f"SELECT admin FROM users WHERE username = '{username}'");
Setiap pernyataan ini melewati username
dari klien langsung ke database, tanpa melakukan pemeriksaan atau validasi apa pun. Kode semacam ini siap untuk mengundang injeksi Python SQL.
Sebaliknya, jenis kueri ini seharusnya aman untuk Anda jalankan:
# SAFE EXAMPLES. DO THIS!
cursor.execute("SELECT admin FROM users WHERE username = %s'", (username, ));
cursor.execute("SELECT admin FROM users WHERE username = %(username)s", {'username': username});
Dalam pernyataan ini, username
dilewatkan sebagai parameter bernama. Sekarang, database akan menggunakan jenis dan nilai username
yang ditentukan saat menjalankan kueri, menawarkan perlindungan dari injeksi Python SQL.
Menggunakan Komposisi SQL
Sejauh ini Anda telah menggunakan parameter untuk literal. Literal adalah nilai-nilai seperti angka, string, dan tanggal. Tetapi bagaimana jika Anda memiliki kasus penggunaan yang mengharuskan pembuatan kueri yang berbeda—yang parameternya adalah sesuatu yang lain, seperti nama tabel atau kolom?
Terinspirasi oleh contoh sebelumnya, mari kita implementasikan fungsi yang menerima nama tabel dan mengembalikan jumlah baris dalam tabel itu:
# BAD EXAMPLE. DON'T DO THIS!
def count_rows(table_name: str) -> int:
with connection.cursor() as cursor:
cursor.execute("""
SELECT
count(*)
FROM
%(table_name)s
""", {
'table_name': table_name,
})
result = cursor.fetchone()
rowcount, = result
return rowcount
Coba jalankan fungsi di tabel pengguna Anda:
>>>Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 9, in count_rows
psycopg2.errors.SyntaxError: syntax error at or near "'users'"
LINE 5: 'users'
^
Perintah gagal menghasilkan SQL. Seperti yang sudah Anda lihat, adaptor database memperlakukan variabel sebagai string atau literal. Namun, nama tabel bukanlah string biasa. Di sinilah komposisi SQL masuk.
Anda sudah tahu bahwa tidak aman menggunakan interpolasi string untuk membuat SQL. Untungnya, Psycopg menyediakan modul bernama psycopg.sql
untuk membantu Anda menyusun kueri SQL dengan aman. Mari kita tulis ulang fungsinya menggunakan psycopg.sql.SQL()
:
from psycopg2 import sql
def count_rows(table_name: str) -> int:
with connection.cursor() as cursor:
stmt = sql.SQL("""
SELECT
count(*)
FROM
{table_name}
""").format(
table_name = sql.Identifier(table_name),
)
cursor.execute(stmt)
result = cursor.fetchone()
rowcount, = result
return rowcount
Ada dua perbedaan dalam implementasi ini. Pertama, Anda menggunakan sql.SQL()
untuk menyusun kueri. Kemudian, Anda menggunakan sql.Identifier()
untuk membubuhi keterangan nilai argumen table_name
. (Sebuah pengidentifikasi adalah nama kolom atau tabel.)
Catatan: Pengguna paket populer django-debug-toolbar
mungkin mendapatkan kesalahan di panel SQL untuk kueri yang disusun dengan psycopg.sql.SQL()
. Perbaikan diharapkan untuk rilis di versi 2.0.
Sekarang, coba jalankan fungsi pada users
tabel:
>>> count_rows('users')
2
Besar! Selanjutnya, mari kita lihat apa yang terjadi ketika tabel tidak ada:
>>>>>> count_rows('foo')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 11, in count_rows
psycopg2.errors.UndefinedTable: relation "foo" does not exist
LINE 5: "foo"
^
Fungsi melempar UndefinedTable
pengecualian. Pada langkah-langkah berikut, Anda akan menggunakan pengecualian ini sebagai indikasi bahwa fungsi Anda aman dari serangan injeksi Python SQL.
Catatan: Pengecualian UndefinedTable
telah ditambahkan di psycopg2 versi 2.8. Jika Anda menggunakan Psycopg versi sebelumnya, Anda akan mendapatkan pengecualian yang berbeda.
Untuk menggabungkan semuanya, tambahkan opsi untuk menghitung baris dalam tabel hingga batas tertentu. Fitur ini mungkin berguna untuk tabel yang sangat besar. Untuk menerapkan ini, tambahkan LIMIT
klausa ke kueri, bersama dengan parameter kueri untuk nilai batas:
from psycopg2 import sql
def count_rows(table_name: str, limit: int) -> int:
with connection.cursor() as cursor:
stmt = sql.SQL("""
SELECT
COUNT(*)
FROM (
SELECT
1
FROM
{table_name}
LIMIT
{limit}
) AS limit_query
""").format(
table_name = sql.Identifier(table_name),
limit = sql.Literal(limit),
)
cursor.execute(stmt)
result = cursor.fetchone()
rowcount, = result
return rowcount
Di blok kode ini, Anda memberi anotasi limit
menggunakan sql.Literal()
. Seperti pada contoh sebelumnya, psycopg
akan mengikat semua parameter kueri sebagai literal saat menggunakan pendekatan sederhana. Namun, saat menggunakan sql.SQL()
, Anda perlu membubuhi keterangan secara eksplisit setiap parameter menggunakan sql.Identifier()
atau sql.Literal()
.
Catatan: Sayangnya, spesifikasi Python API tidak membahas pengikatan pengidentifikasi, hanya literal. Psycopg adalah satu-satunya adaptor populer yang menambahkan kemampuan untuk menyusun SQL dengan aman dengan literal dan pengidentifikasi. Fakta ini membuatnya semakin penting untuk memperhatikan saat mengikat pengidentifikasi.
Jalankan fungsi untuk memastikan berfungsi:
>>>>>> count_rows('users', 1)
1
>>> count_rows('users', 10)
2
Sekarang setelah Anda melihat fungsinya berfungsi, pastikan itu juga aman:
>>>>>> count_rows("(select 1) as foo; update users set admin = true where name = 'haki'; --", 1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 18, in count_rows
psycopg2.errors.UndefinedTable: relation "(select 1) as foo; update users set admin = true where name = '" does not exist
LINE 8: "(select 1) as foo; update users set adm...
^
Traceback ini menunjukkan bahwa psycopg
lolos dari nilai, dan database memperlakukannya sebagai nama tabel. Karena tabel dengan nama ini tidak ada, UndefinedTable
pengecualian telah diajukan dan Anda tidak diretas!
Kesimpulan
Anda telah berhasil menerapkan fungsi yang menyusun SQL dinamis tanpa menempatkan sistem Anda pada risiko injeksi Python SQL! Anda telah menggunakan literal dan pengenal dalam kueri Anda tanpa mengorbankan keamanan.
Anda telah mempelajari:
- Apa Injeksi Python SQL adalah dan bagaimana hal itu dapat dieksploitasi
- Cara mencegah injeksi Python SQL menggunakan parameter kueri
- Cara membuat pernyataan SQL dengan aman yang menggunakan literal dan pengidentifikasi sebagai parameter
Anda sekarang dapat membuat program yang dapat menahan serangan dari luar. Maju dan kalahkan para peretas!