Lär dig SQL

Back to All Courses

Lesson 12

Injections och parameteriserade queries

by Ted Klein Bergman

Injections

En ny användare vill vara med i vårt nätverk. Denna användare vill ha användarnamnet supertroll','tricked@gmail.com'); DROP TABLE users; --. Vi ska inte döma folks val av namn, så vi kör in den nya användaren i vår databas med email = troll1337@fake.com och age = NULL.

Så vi skriver Pythonkod som konstruerar vår query. Vi använder oss av format string, som skapar en ny sträng genom att stoppa in vardera argument mellan måsvingarna {} i mallen. Till sist kör vi vår query.

username = "supertroll', 'tricked@gmail.com'); DROP TABLE users; --"
email = "troll1337@fake.com"
age = "NULL"

query = f"INSERT INTO users (username, email, age) VALUES ({username}, {email}, {age});"

cursor.execute(query)

Vad händer när vi kör programmet? Jo, alla våra användare raderas. Ifall någon vet strukturen på vårat program så kan de utnyttja det för att interagera med vår databas hur de vill. Hur då? Vi börjar med att titta på vad vår query faktiskt resulterade i.

INSERT INTO Users (username, email, age)
VALUES ('supertroll', 'tricked@gmail.com'); DROP TABLE users; -- , 'troll1337@fake.com', NULL);

Det som hände var att de lyckades injicera deras egna SQL kod mellan våran kod. Detta är varför det kallas injections. För att skydda sig emot detta använder vi oss av parametriserade queries.

Varning: skapa aldrig queries genom att manipulera strängar! Använd alltid parametriserade queries!

Parametriserade queries

Med hjälp av vår cursorinstans kan vi enkelt använda oss av parametriserade queries. Vad det betyder är att vi, istället för att konstruera queryn genom manipulera strängar, skickar vår query och argumenten till cursorn och låter den ta hand om att sätta in parameterna på rätt sätt.

I psycopg2 har vi två sätt att nämna var parametrarna ska in i vår query. Vi kan ersätta våra parametrar med %s, ge vår cursor en lista med våra variabler och så kommer de sättas in i ordning.

username = "supertroll', 'tricked@gmail.com'); DROP TABLE users; -- "
email = "troll1337@fake.com"
age = "NULL"

query_template = "INSERT INTO users (username, email, age) VALUES (%s, %s, %s);"

cursor.execute(query, [username, email, age])

Eller så kan vi använda oss av namn. Vi ersätter våra parametrar med %(name)s och sedan ger vår cursor en dictionary. Då spelar inte positionen någon roll.

username = "supertroll', 'tricked@gmail.com'); DROP TABLE users; -- "
email = "troll1337@fake.com"
age = "NULL"

query_template = "INSERT INTO users (username, email, age) VALUES (%(username)s, %(email)s, %(age)s);"

cursor.execute(query, {'email': email, 'age': age, 'username': username})

Fixa vår funktion

I förra kapitlet struntade vi att använda oss av parametern number i funktionen get_newest_tweets(number) då vi inte visste om parametriserade queries. Nu kan vi fixa den.

def get_newest_tweets(number):
    """
    Fetch x amount of tweets ordered by time_posted (descending order).

    The values in each tuple should be:
        tweet_id, poster_id, username, content, time_posted

    Return:
         all tuples in a list.

    Example:

        all_tuples = []
        for tuple in get_tuples_from_query():
            all_tuples.append(tuple)

        return all_tuples

    Hardness:
        2
    """
    query = """
        SELECT tweet_id, poster_id, username, content, time_posted
        FROM   users JOIN tweets ON user_id = poster_id
        ORDER BY time_posted DESC
        LIMIT %(number)s;
        """
    cursor.execute(query, {'number': number})
    table  = cursor.fetchall()
    result = []
    for tweet in table:
        result.append(tweet)
    return result

På startsidan kommer nu färre tweets att visas.

Hashade och saltade lösenord

Eftersom vi redan är på ämnet säkerhet kan det vara ett bra tillfälle att lägga till en tabell med lösenord för alla användare. En tabell med lösenord kommer då att innehålla tuples med en användare från tabellen users och ett lösenord den användaren har (som inte får vara NULL).

CREATE TABLE passwords (
    user_id  INTEGER REFERENCES users(user_id) ON DELETE CASCADE PRIMARY KEY,
    password TEXT NOT NULL
);

Om ni har kört Tweeter/setup.py är denna tabell redan skapad!

Ett problem att spara lösenord är att alla som har tillgång till databasen lätt kan se vilket lösenord varje användare har. Detta är ett enormt säkerhetsproblem! Därför sparar lyckligtvis inte företag lösenord rätt av, utan de hashar och saltar dem.

Att hasha och salta är ett sätt att konvertera ett lösenord genom en systematisk process så att det går att återställa lösenordet (genom den omvända processen) men inte att gissa sig till eller på något sätt skriva ett program som kan lista ut lösenordet (inom rimlig tidsram). För oss är denna process enkel tack vare biblioteket passlib (som installeras om ni kör Tweeter/setup.py), som gör detta åt oss.

Följande är ett exempel på hur vi saltar och hashar ett lösenord:

from passlib.hash import sha256_crypt
password = input("Password: ")
salted_and_hashed_password = sha256_crypt.encrypt(password)
print(salted_and_hashed_password)

Koden kommer skriva ut något som är rena rappakaljan! Dock är det bara rappakalja för den som inte vet processen bakom saltningen och hashningen (vilket vi inte vet, vilket är bra för då kan vi inte lista ut lösenorden från rå data heller). Nedan visas ett enkelt program för att skapa ett lösenord och sedan "logga in".

from passlib.hash import sha256_crypt
correct_password = input("Password: ")
salted_and_hashed_password = sha256_crypt.encrypt(correct_password)
print('Salted and hashed password is:', salted_and_hashed_password)

guessed_password = input("Password: ")
password_is_correct = sha256_crypt.verify(guessed_password, salted_and_hashed_password)
while not password_is_correct:
    print('Wrong!')
    guessed_password = input("Password: ")
    password_is_correct = sha256_crypt.verify(guessed_password, salted_and_hashed_password)
print('Logged in!')

Varning: spara aldrig råa lösenord! Salta och hasha dem innan de sparas någonstans!

Exercise

Det är två saker man alltid måste tänka på när man arbetar med databaser i applikationer för att:

  1. Undvika SQL-injections.

  2. Hålla användares lösenord hemliga.

Vilka är dem?

Solution

  1. Använda parametriserade queries.

  2. Lösenorden ska alltid vara saltade och hashade.

Exercise

Vad kommer hända i följande exempel om användaren 87 har angett följande sträng som sitt lösenord: '0000'; -- '

def change_password(user_id, new_password)
    query = f"""
        UPDATE TABLE passwords
        SET password = {password} WHERE user_id = {user_id};
    """
    cursor.execute(query)


current_user_id = get_current_user_id()
new_password    = get_text_from_user()
change_password(current_user_id, new_password)

Solution

UPDATE TABLE passwords
SET password = '0000'; -- ' WHERE user_id = 87;

Alla användares lösenord kommer att sättas till 0000.

Exercise

Hur bör koden på föregående fråga vara skriven?

Solution

def change_password(user_id, new_password)
    query_template = """
        UPDATE TABLE passwords
        SET password = %(password) WHERE user_id = %(user_id);
    """
    cursor.execute(query, {'password': new_password, 'user_id': user_id})


current_user_id = get_current_user_id()
new_password    = get_text_from_user()
change_password(current_user_id, new_password)