sql >> Database >  >> RDS >> Database

SQL-injectie-aanvallen voorkomen met Python

Om de paar jaar rangschikt het Open Web Application Security Project (OWASP) de meest kritieke beveiligingsrisico's voor webapplicaties. Sinds het eerste rapport hebben injectierisico's altijd voorop gestaan. Van alle injectietypes, SQL-injectie is een van de meest voorkomende aanvalsvectoren en misschien wel de gevaarlijkste. Omdat Python een van de meest populaire programmeertalen ter wereld is, is het van cruciaal belang om te weten hoe je je kunt beschermen tegen Python SQL-injectie.

In deze tutorial leer je:

  • Wat Python SQL-injectie is en hoe het te voorkomen
  • Hoe query's op te stellen met zowel letterlijke als identifiers als parameters
  • Hoe het veilig uitvoeren van zoekopdrachten in een database

Deze tutorial is geschikt voor gebruikers van alle database-engines . De voorbeelden hier gebruiken PostgreSQL, maar de resultaten kunnen worden gereproduceerd in andere databasebeheersystemen (zoals SQLite, MySQL, Microsoft SQL Server, Oracle, enzovoort).

Gratis bonus: 5 Thoughts On Python Mastery, een gratis cursus voor Python-ontwikkelaars die je de routekaart en de mindset laat zien die je nodig hebt om je Python-vaardigheden naar een hoger niveau te tillen.


Python SQL-injectie begrijpen

SQL Injection-aanvallen zijn zo'n veelvoorkomend beveiligingslek dat de legendarische xkcd webcomic wijdde er een strip aan:

Het genereren en uitvoeren van SQL-query's is een veelvoorkomende taak. Bedrijven over de hele wereld maken echter vaak vreselijke fouten als het gaat om het opstellen van SQL-instructies. Hoewel de ORM-laag meestal SQL-query's opstelt, moet u soms uw eigen query's schrijven.

Wanneer u Python gebruikt om deze query's rechtstreeks in een database uit te voeren, bestaat de kans dat u fouten maakt die uw systeem in gevaar kunnen brengen. In deze zelfstudie leert u hoe u met succes functies implementeert die dynamische SQL-query's samenstellen zonder uw systeem in gevaar brengen voor Python SQL-injectie.



Een database opzetten

Om te beginnen, ga je een nieuwe PostgreSQL-database opzetten en deze vullen met gegevens. Tijdens de hele tutorial zul je deze database gebruiken om uit de eerste hand te zien hoe Python SQL-injectie werkt.


Een database maken

Open eerst uw shell en maak een nieuwe PostgreSQL-database die eigendom is van de gebruiker postgres :

$ createdb -O postgres psycopgtest

Hier gebruikte je de opdrachtregeloptie -O om de eigenaar van de database in te stellen op de gebruiker postgres . U hebt ook de naam van de database opgegeven, namelijk psycopgtest .

Opmerking: postgres is een speciale gebruiker , die je normaal gesproken zou reserveren voor administratieve taken, maar voor deze tutorial is het prima om postgres te gebruiken . In een echt systeem moet u echter een aparte gebruiker aanmaken om de eigenaar van de database te worden.

Uw nieuwe database is klaar voor gebruik! Je kunt er verbinding mee maken met psql :

$ psql -U postgres -d psycopgtest
psql (11.2, server 10.5)
Type "help" for help.

Je bent nu verbonden met de database psycopgtest als de gebruiker postgres . Deze gebruiker is ook de eigenaar van de database, dus je hebt leesrechten voor elke tabel in de database.



Een tabel maken met gegevens

Vervolgens moet u een tabel maken met wat gebruikersinformatie en er gegevens aan toevoegen:

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)

De tabel heeft twee kolommen:username en admin . De admin kolom geeft aan of een gebruiker al dan niet beheerdersrechten heeft. Uw doel is om de admin . te targeten veld en probeer het te misbruiken.



Een virtuele Python-omgeving opzetten

Nu je een database hebt, is het tijd om je Python-omgeving in te richten. Voor stapsgewijze instructies om dit te doen, bekijk Python Virtual Environments:A Primer.

Creëer uw virtuele omgeving in een nieuwe directory:

(~/src) $ mkdir psycopgtest
(~/src) $ cd psycopgtest
(~/src/psycopgtest) $ python3 -m venv venv

Nadat u deze opdracht hebt uitgevoerd, wordt een nieuwe map met de naam venv zal gemaakt worden. In deze map worden alle pakketten opgeslagen die u in de virtuele omgeving installeert.



Verbinding maken met de database

Om verbinding te maken met een database in Python, hebt u een databaseadapter nodig . De meeste database-adapters volgen versie 2.0 van de Python Database API-specificatie PEP 249. Elke grote database-engine heeft een leidende adapter:

Database Adapter
PostgreSQL Psychopg
SQLite sqlite3
Oracle cx_oracle
MijnSql MySQLdb

Om verbinding te maken met een PostgreSQL-database, moet u Psycopg installeren, de populairste adapter voor PostgreSQL in Python. Django ORM gebruikt het standaard, en het wordt ook ondersteund door SQLAlchemy.

Activeer in uw terminal de virtuele omgeving en gebruik pip om psycopg te installeren :

(~/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

Nu bent u klaar om een ​​verbinding met uw database tot stand te brengen. Dit is het begin van je Python-script:

import psycopg2

connection = psycopg2.connect(
    host="localhost",
    database="psycopgtest",
    user="postgres",
    password=None,
)
connection.set_session(autocommit=True)

Je hebt psycopg2.connect() gebruikt om de verbinding te maken. Deze functie accepteert de volgende argumenten:

  • host is het IP-adres of de DNS van de server waar uw database zich bevindt. In dit geval is de host uw lokale computer, of localhost .

  • database is de naam van de database waarmee verbinding moet worden gemaakt. U wilt verbinding maken met de database die u eerder hebt gemaakt, psycopgtest .

  • user is een gebruiker met machtigingen voor de database. In dit geval wilt u als eigenaar verbinding maken met de database, dus u geeft de gebruiker postgres door .

  • password is het wachtwoord voor degene die je hebt opgegeven in user . In de meeste ontwikkelomgevingen kunnen gebruikers zonder wachtwoord verbinding maken met de lokale database.

Na het opzetten van de verbinding, heb je de sessie geconfigureerd met autocommit=True . Activeren van autocommit betekent dat u transacties niet handmatig hoeft te beheren door een commit of rollback . Dit is het standaardgedrag in de meeste ORM's. U gebruikt dit gedrag hier ook, zodat u zich kunt concentreren op het opstellen van SQL-query's in plaats van het beheren van transacties.

Opmerking: Django-gebruikers kunnen de instantie van de verbinding die door de ORM wordt gebruikt ophalen van django.db.connection :

from django.db import connection


Een zoekopdracht uitvoeren

Nu je verbinding hebt met de database, ben je klaar om een ​​query uit te voeren:

>>>
>>> with connection.cursor() as cursor:
...     cursor.execute('SELECT COUNT(*) FROM users')
...     result = cursor.fetchone()
... print(result)
(2,)

Je hebt de connection gebruikt object om een ​​cursor te maken . Net als een bestand in Python, cursor wordt geïmplementeerd als contextmanager. Wanneer u de context maakt, wordt een cursor wordt geopend zodat u opdrachten naar de database kunt verzenden. Wanneer de context wordt afgesloten, wordt de cursor sluit en u kunt het niet langer gebruiken.

Opmerking: Voor meer informatie over contextmanagers, bekijk Python-contextmanagers en de "met"-verklaring.

Terwijl je in de context was, gebruikte je cursor om een ​​query uit te voeren en de resultaten op te halen. In dit geval heeft u een zoekopdracht gegeven om de rijen in de users . te tellen tafel. Om het resultaat van de query op te halen, heb je cursor.fetchone() . uitgevoerd en kreeg een tupel. Omdat de zoekopdracht maar één resultaat kan opleveren, heb je fetchone() . gebruikt . Als de zoekopdracht meer dan één resultaat zou opleveren, moet u ofwel de cursor herhalen of gebruik een van de andere fetch* methoden.




Queryparameters gebruiken in SQL

In de vorige sectie hebt u een database gemaakt, er een verbinding mee tot stand gebracht en een query uitgevoerd. De zoekopdracht die u gebruikte was statisch . Met andere woorden, het had geen parameters . Nu ga je parameters gebruiken in je zoekopdrachten.

Eerst ga je een functie implementeren die controleert of een gebruiker een beheerder is of niet. is_admin() accepteert een gebruikersnaam en geeft de beheerdersstatus van die gebruiker terug:

# 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

Deze functie voert een query uit om de waarde van de admin . op te halen kolom voor een bepaalde gebruikersnaam. Je gebruikte fetchone() om een ​​tuple met een enkel resultaat te retourneren. Vervolgens heb je deze tuple uitgepakt in de variabele admin . Om uw functie te testen, controleert u enkele gebruikersnamen:

>>>
>>> is_admin('haki')
False
>>> is_admin('ran')
True

Tot zover goed. De functie heeft het verwachte resultaat voor beide gebruikers geretourneerd. Maar hoe zit het met niet-bestaande gebruikers? Bekijk deze Python-traceback eens:

>>>
>>> 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

Als de gebruiker niet bestaat, wordt een TypeError wordt verhoogd. Dit komt omdat .fetchone() retourneert None wanneer er geen resultaten worden gevonden, en uitpakken None roept een TypeError . op . De enige plaats waar u een tuple kunt uitpakken, is waar u admin invult van result .

Om niet-bestaande gebruikers te behandelen, maakt u een speciaal geval aan voor wanneer result is 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

Hier heb je een speciaal geval toegevoegd voor het afhandelen van None . Als username bestaat niet, dan zou de functie False moeten retourneren . Test de functie nogmaals bij enkele gebruikers:

>>>
>>> is_admin('haki')
False
>>> is_admin('ran')
True
>>> is_admin('foo')
False

Super goed! De functie kan nu ook niet-bestaande gebruikersnamen aan.



Gebruik van queryparameters met Python SQL-injectie

In het vorige voorbeeld hebt u tekenreeksinterpolatie gebruikt om een ​​query te genereren. Vervolgens hebt u de query uitgevoerd en de resulterende tekenreeks rechtstreeks naar de database verzonden. Er is echter iets dat u tijdens dit proces over het hoofd hebt gezien.

Denk terug aan de username argument dat je hebt doorgegeven aan is_admin() . Wat stelt deze variabele precies voor? Je zou kunnen aannemen dat username is slechts een tekenreeks die de naam van een echte gebruiker vertegenwoordigt. Zoals je zult zien, kan een indringer echter gemakkelijk misbruik maken van dit soort onoplettendheid en grote schade aanrichten door Python SQL-injectie uit te voeren.

Probeer te controleren of de volgende gebruiker een beheerder is of niet:

>>>
>>> is_admin("'; select true; --")
True

Wacht... Wat is er net gebeurd?

Laten we nog eens kijken naar de uitvoering. Print de eigenlijke query die wordt uitgevoerd in de database:

>>>
>>> print("select admin from users where username = '%s'" % "'; select true; --")
select admin from users where username = ''; select true; --'

De resulterende tekst bevat drie uitspraken. Om precies te begrijpen hoe Python SQL-injectie werkt, moet u elk onderdeel afzonderlijk inspecteren. De eerste verklaring is als volgt:

select admin from users where username = '';

Dit is uw beoogde vraag. De puntkomma (; ) beëindigt de query, dus het resultaat van deze query doet er niet toe. De volgende is de tweede verklaring:

select true;

Deze verklaring is opgesteld door de indringer. Het is ontworpen om altijd True te retourneren .

Ten slotte zie je dit korte stukje code:

--'

Dit fragment maakt alles wat erna komt onschadelijk. De indringer heeft het commentaarsymbool toegevoegd (-- ) om alles wat je na de laatste tijdelijke aanduiding hebt geplaatst om te zetten in een opmerking.

Wanneer u de functie met dit argument uitvoert, het zal altijd True retourneren . Als u deze functie bijvoorbeeld gebruikt op uw inlogpagina, kan een indringer inloggen met de gebruikersnaam '; select true; -- , en ze krijgen toegang.

Als je denkt dat dit erg is, kan het nog erger worden! Indringers met kennis van uw tabelstructuur kunnen Python SQL-injectie gebruiken om blijvende schade aan te richten. De indringer kan bijvoorbeeld een update-instructie invoegen om de informatie in de database te wijzigen:

>>>
>>> is_admin('haki')
False
>>> is_admin("'; update users set admin = 'true' where username = 'haki'; select true; --")
True
>>> is_admin('haki')
True

Laten we het nog eens op een rijtje zetten:

';

Dit fragment beëindigt de zoekopdracht, net als bij de vorige injectie. De volgende verklaring is als volgt:

update users set admin = 'true' where username = 'haki';

Deze sectie werkt admin bij naar true voor gebruiker haki .

Ten slotte is er dit codefragment:

select true; --

Net als in het vorige voorbeeld, retourneert dit stuk true en becommentarieert alles wat erop volgt.

Waarom is dit erger? Welnu, als de indringer erin slaagt de functie met deze invoer uit te voeren, dan zal gebruiker haki wordt een beheerder:

psycopgtest=# select * from users;
 username | admin
----------+-------
 ran      | t
 haki     | t
(2 rows)

De indringer hoeft de hack niet meer te gebruiken. Ze kunnen gewoon inloggen met de gebruikersnaam haki . (Als de indringer echt kwaad wilden doen, dan konden ze zelfs een DROP DATABASE issue afgeven commando.)

Herstel voordat je het vergeet haki terug naar de oorspronkelijke staat:

psycopgtest=# update users set admin = false where username = 'haki';
UPDATE 1

Dus, waarom gebeurt dit? Nou, wat weet je over de username argument? Je weet dat het een string zou moeten zijn die de gebruikersnaam vertegenwoordigt, maar je controleert of dwingt deze bewering niet af. Dit kan gevaarlijk zijn! Dit is precies waar aanvallers naar op zoek zijn wanneer ze uw systeem proberen te hacken.


Veilige queryparameters maken

In het vorige gedeelte heb je gezien hoe een indringer je systeem kan misbruiken en beheerdersrechten kan krijgen door een zorgvuldig ontworpen string te gebruiken. Het probleem was dat je toestond dat de waarde die door de client werd doorgegeven rechtstreeks naar de database werd uitgevoerd, zonder enige vorm van controle of validatie uit te voeren. SQL-injecties zijn afhankelijk van dit type kwetsbaarheid.

Elke keer dat gebruikersinvoer wordt gebruikt in een databasequery, is er een mogelijke kwetsbaarheid voor SQL-injectie. De sleutel tot het voorkomen van Python SQL-injectie is ervoor te zorgen dat de waarde wordt gebruikt zoals de ontwikkelaar het bedoeld heeft. In het vorige voorbeeld bedoelde je username als string te gebruiken. In werkelijkheid werd het gebruikt als een onbewerkte SQL-instructie.

Om ervoor te zorgen dat waarden worden gebruikt zoals ze bedoeld zijn, moet je escape de waarde. Om bijvoorbeeld te voorkomen dat indringers onbewerkte SQL injecteren in plaats van een stringargument, kunt u aanhalingstekens laten ontsnappen:

>>>
>>> # BAD EXAMPLE. DON'T DO THIS!
>>> username = username.replace("'", "''")

Dit is slechts één voorbeeld. Er zijn veel speciale tekens en scenario's om over na te denken wanneer u Python SQL-injectie probeert te voorkomen. Gelukkig voor jou, moderne database-adapters, worden geleverd met ingebouwde tools om Python SQL-injectie te voorkomen door gebruik te maken van queryparameters . Deze worden gebruikt in plaats van gewone string-interpolatie om een ​​query op te stellen met parameters.

Opmerking: Verschillende adapters, databases en programmeertalen verwijzen naar queryparameters met verschillende namen. Veelvoorkomende namen zijn onder meer bindvariabelen , vervangingsvariabelen , en substitutievariabelen .

Nu u de kwetsbaarheid beter begrijpt, bent u klaar om de functie te herschrijven met behulp van queryparameters in plaats van tekenreeksinterpolatie:

 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

Dit is wat er in dit voorbeeld anders is:

  • In lijn 9, je hebt een benoemde parameter username gebruikt om aan te geven waar de gebruikersnaam moet komen. Merk op hoe de parameter username staat niet langer tussen enkele aanhalingstekens.

  • In lijn 11, je hebt de waarde van username doorgegeven als het tweede argument voor cursor.execute() . De verbinding gebruikt het type en de waarde van username bij het uitvoeren van de query in de database.

Om deze functie te testen, probeert u een aantal geldige en ongeldige waarden, inclusief de gevaarlijke string van eerder:

>>>
>>> is_admin('haki')
False
>>> is_admin('ran')
True
>>> is_admin('foo')
False
>>> is_admin("'; select true; --")
False

Verbazingwekkend! De functie heeft het verwachte resultaat voor alle waarden geretourneerd. Bovendien werkt de gevaarlijke string niet meer. Om te begrijpen waarom, kunt u de query inspecteren die is gegenereerd door 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; --'

De verbinding behandelde de waarde van username als een tekenreeks en escapetekens voor alle tekens die de tekenreeks kunnen beëindigen en Python SQL-injectie introduceren.



Veilige queryparameters doorgeven

Databaseadapters bieden meestal verschillende manieren om queryparameters door te geven. Genoemde tijdelijke aanduidingen zijn meestal het beste voor de leesbaarheid, maar sommige implementaties kunnen baat hebben bij het gebruik van andere opties.

Laten we eens kijken naar enkele van de juiste en verkeerde manieren om queryparameters te gebruiken. Het volgende codeblok toont de soorten zoekopdrachten die u wilt vermijden:

# 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}'");

Elk van deze instructies geeft username . door rechtstreeks van de klant naar de database, zonder enige vorm van controle of validatie uit te voeren. Dit soort code is rijp voor het uitnodigen van Python SQL-injectie.

Daarentegen zouden dit soort zoekopdrachten veilig voor u moeten zijn om uit te voeren:

# 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});

In deze verklaringen, username wordt doorgegeven als een benoemde parameter. Nu gebruikt de database het gespecificeerde type en de waarde van username bij het uitvoeren van de query, bescherming bieden tegen Python SQL-injectie.




SQL-compositie gebruiken

Tot nu toe heb je parameters gebruikt voor letterlijke waarden. Letterlijk zijn waarden zoals getallen, tekenreeksen en datums. Maar wat als u een use-case heeft waarvoor u een andere query moet opstellen, een waarbij de parameter iets anders is, zoals een tabel- of kolomnaam?

Laten we, geïnspireerd door het vorige voorbeeld, een functie implementeren die de naam van een tabel accepteert en het aantal rijen in die tabel retourneert:

# 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

Probeer de functie op uw gebruikerstabel uit te voeren:

>>>
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'
                        ^

De opdracht kan de SQL niet genereren. Zoals je al hebt gezien, behandelt de database-adapter de variabele als een tekenreeks of een letterlijke. Een tabelnaam is echter geen gewone tekenreeks. Dit is waar SQL-compositie van pas komt.

U weet al dat het niet veilig is om tekenreeksinterpolatie te gebruiken om SQL samen te stellen. Gelukkig biedt Psycopg een module genaamd psycopg.sql om u te helpen veilig SQL-query's op te stellen. Laten we de functie herschrijven met 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

Er zijn twee verschillen in deze uitvoering. Eerst gebruikte je sql.SQL() om de vraag op te stellen. Vervolgens gebruikte je sql.Identifier() annoteren van de argumentwaarde table_name . (Een identificatie is een kolom- of tabelnaam.)

Opmerking: Gebruikers van het populaire pakket django-debug-toolbar kan een foutmelding krijgen in het SQL-paneel voor query's die zijn samengesteld met psycopg.sql.SQL() . Er wordt een oplossing verwacht voor release in versie 2.0.

Probeer nu de functie uit te voeren op de users tafel:

>>>
>>> count_rows('users')
2

Super goed! Laten we vervolgens eens kijken wat er gebeurt als de tabel niet bestaat:

>>>
>>> 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"
                        ^

De functie gooit de UndefinedTable uitzondering. In de volgende stappen gebruikt u deze uitzondering als indicatie dat uw functie veilig is voor een Python SQL-injectieaanval.

Opmerking: De uitzondering UndefinedTable is toegevoegd in psycopg2 versie 2.8. Als u met een eerdere versie van Psycopg werkt, krijgt u een andere uitzondering.

Om het allemaal samen te voegen, voegt u een optie toe om rijen in de tabel tot een bepaalde limiet te tellen. Deze functie kan handig zijn voor zeer grote tabellen. Voeg een LIMIT . toe om dit te implementeren clausule toe aan de query, samen met queryparameters voor de waarde van de limiet:

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

In dit codeblok heb je limit geannoteerd met behulp van sql.Literal() . Net als in het vorige voorbeeld, psycopg bindt alle queryparameters als letterlijke waarden bij gebruik van de eenvoudige benadering. Bij gebruik van sql.SQL() , moet u elke parameter expliciet annoteren met behulp van sql.Identifier() of sql.Literal() .

Opmerking: Helaas heeft de Python API-specificatie geen betrekking op de binding van identifiers, alleen letterlijke. Psycopg is de enige populaire adapter die de mogelijkheid heeft toegevoegd om veilig SQL samen te stellen met zowel letterlijke als identifiers. Dit feit maakt het nog belangrijker om goed op te letten bij het binden van identifiers.

Voer de functie uit om te controleren of deze werkt:

>>>
>>> count_rows('users', 1)
1
>>> count_rows('users', 10)
2

Nu u ziet dat de functie werkt, moet u ervoor zorgen dat deze ook veilig is:

>>>
>>> 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...
                            ^

Deze traceback laat zien dat psycopg ontsnapte aan de waarde en de database behandelde het als een tabelnaam. Aangezien een tabel met deze naam niet bestaat, wordt een UndefinedTable Er is een uitzondering gemaakt en je bent niet gehackt!



Conclusie

U heeft met succes een functie geïmplementeerd die dynamische SQL samenstelt zonder uw systeem in gevaar brengen voor Python SQL-injectie! Je hebt zowel letterlijke als identifiers gebruikt in je zoekopdracht zonder de veiligheid in gevaar te brengen.

Je hebt geleerd:

  • Wat Python SQL-injectie is en hoe het kan worden uitgebuit
  • Hoe Python SQL-injectie te voorkomen queryparameters gebruiken
  • Hoe veilig SQL-instructies op te stellen die letterlijke waarden en identifiers als parameters gebruiken

U kunt nu programma's maken die aanvallen van buitenaf kunnen weerstaan. Ga door en dwarsboom de hackers!



  1. Datum selecteren zonder tijd in SQL

  2. Hoe u SQLite-tools kunt downloaden en installeren

  3. Kan ik een binaire tekenreeks opslaan in de CLOB-kolom?

  4. Wat is er nieuw in MariaDB Server 10.5?