Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Mencegah Serangan Injeksi SQL Dengan Python

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, atau localhost .

  • 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 memberikan postgres pengguna .

  • password adalah kata sandi untuk siapa pun yang Anda tentukan di user . 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 parameter username tidak lagi dikelilingi oleh tanda kutip tunggal.

  • Pada baris 11, Anda melewati nilai username sebagai argumen kedua untuk cursor.execute() . Koneksi akan menggunakan jenis dan nilai username 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!



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Apakah Anda selalu membutuhkan database untuk aplikasi Anda?

  2. 9 sistem manajemen database teratas untuk template Joomla

  3. Cara Menggabungkan String dalam SQL

  4. Ambang Pengoptimalan – Pengelompokan dan Penggabungan Data, Bagian 5

  5. 10 sumber daya yang berguna bagi mereka yang ingin tahu lebih banyak tentang SQL