Injektion — grundorsak och parametriserade frågor
Observera
Genomför PortSwigger Web Security Academys modul om SQL injection, eller DVWA:s SQL Injection-övning på nivå "Low". Båda är guidade och har facit. Notera vad som händer med applikationens svar när indata inte behandlas som enbart data.
Höj sedan DVWA:s säkerhetsnivå till "High" och observera att samma tillvägagångssätt inte längre fungerar. Skillnaden mellan nivåerna är hela lärdomen.
Analysera grundorsaken
Använd DVWA:s inbyggda "View Source"-knapp och jämför koden mellan säkerhetsnivåerna. Detta är det mest lärorika momentet i hela laborationen.
Grundorsaken är alltid densamma: applikationen bygger ett kommando genom att klistra ihop kod och användardata till en enda sträng. Databasen får sedan en färdig sträng och har ingen möjlighet att veta vilken del som var avsedd som kommando och vilken som var användarens data.
// Grundorsaken: indata blir en del av kommandots struktur
$query = "SELECT * FROM users WHERE id = '" . $_GET['id'] . "'";
Vad koden gör — förklaring rad för rad
Den sårbara raden, i PHP. Den bygger en databasfråga genom att klistra ihop text. Det är precis det som orsakar injektionssårbarheter.
- $query
- I PHP börjar variabelnamn med dollartecken. En variabel är en namngiven låda som håller ett värde.
- "SELECT * FROM users WHERE id = '"
- En databasfråga skriven som text. SELECT * betyder "hämta alla kolumner", FROM users "ur tabellen users", WHERE id = "där id är lika med".
- .
- Punkten mellan textbitarna är PHP:s sätt att klistra ihop text. Här ligger hela problemet.
- $_GET['id']
- Värdet besökaren skickade med i webbadressen. Det klistras rakt in i frågan, och databasen kan sedan inte skilja frågans struktur från besökarens text.
Samma mönster återkommer i andra former — OS-kommandon som byggs ihop som strängar, LDAP-filter, filsökvägar. Lär dig känna igen strängkonkatenering till ett kommando som varningssignal, oavsett teknik.
Bygg försvaret
1. Parametriserade frågor (prepared statements). Här skickas kommandots struktur och datan separat. Databasen vet då alltid vad som är vad, och användardata kan per definition inte ändra frågans struktur.
PHP med PDO:
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
$user = $stmt->fetch();
Vad koden gör — förklaring rad för rad
Den säkra versionen i PHP. I stället för att klistra ihop skickas frågan och värdet var för sig, i två steg. Databasen vet då alltid vad som är kommando och vad som är data.
- $pdo->prepare(...)
- Steg ett: skicka frågans form till databasen, utan värdet. PDO är PHP:s standardsätt att prata med databaser.
- ?
- Frågetecknet är en platshållare — inte ett skrivfel. Det betyder "här kommer ett värde senare".
- $stmt->execute([$_GET['id']])
- Steg två: skicka värdet. Databasen vet redan hur frågan ser ut, så värdet kan aldrig ändra dess betydelse.
- $stmt->fetch()
- Hämtar ut resultatraden så att programmet kan använda den.
Python med psycopg:
cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
Vad koden gör — förklaring rad för rad
Samma skydd i Python. Formen skiljer sig från PHP men principen är identisk: frågan och värdet skickas som två separata saker.
- cur.execute(
- cur står för cursor, programmets handtag mot databasen. execute kör frågan.
- %s
- Pythons platshållare, motsvarigheten till PHP:s frågetecken. Den ska inte omges av citattecken.
- (user_id,)
- Värdena skickas som ett andra, separat argument. Kommat efter user_id är Pythons sätt att markera att detta är en lista med ett enda element.
Node.js med mysql2:
const [rows] = await conn.execute(
'SELECT * FROM users WHERE id = ?',
[userId]
);
Vad koden gör — förklaring rad för rad
Samma sak igen, nu i JavaScript på serversidan (Node.js). Tredje språket, samma princip — det är avsiktligt, så att du känner igen mönstret oavsett teknik.
- const
- Deklarerar ett värde som inte ska ändras efteråt.
- await
- Vänta tills databasen svarat innan programmet går vidare. Databasanrop tar tid, och utan await skulle programmet rusa förbi.
- ?
- Platshållaren, precis som i PHP.
- [userId]
- Värdena i en egen lista, separat från frågan.
- const [rows] =
- Plockar ut den första delen av databasens svar och kallar den rows.
execute("... WHERE id = " + user_id) är fortfarande sårbart även om du använder ett bibliotek som stöder parametrar. Det är parameteranvändningen som skyddar, inte biblioteket i sig.2. ORM med parametrisering som standard. Django ORM, SQLAlchemy, Eloquent och Prisma parametriserar automatiskt. Undantaget är råa frågor (raw(), DB::raw()), som måste hanteras manuellt.
3. Minsta möjliga behörighet i databasen. Applikationskontot ska inte vara root. Ett konto som bara har SELECT, INSERT och UPDATE på de tabeller det behöver begränsar skadan kraftigt om något ändå går fel:
CREATE USER 'webapp'@'localhost' IDENTIFIED BY '<starkt lösenord>';
GRANT SELECT, INSERT, UPDATE ON shop.orders TO 'webapp'@'localhost';
-- Medvetet INTE: DROP, ALTER, GRANT, eller åtkomst till andra databaser
Vad koden gör — förklaring rad för rad
SQL-kommandon som körs i databasen, inte i applikationen. De skapar ett konto åt webbapplikationen och ger det bara de rättigheter det faktiskt behöver.
- CREATE USER 'webapp'@'localhost'
- Skapar ett databaskonto som heter webapp och bara får anslutas från samma maskin.
- GRANT SELECT, INSERT, UPDATE
- Ger rätt att läsa, lägga till och ändra rader — men ingenting mer.
- ON shop.orders
- Rättigheterna gäller bara tabellen orders i databasen shop, inte något annat.
- -- Medvetet INTE: DROP, ALTER...
- Två bindestreck inleder en kommentar i SQL. Raden påminner om vad som avsiktligt utelämnats: rätten att radera tabeller eller dela ut rättigheter vidare.
4. Validering som komplement, inte som huvudförsvar. Validera att ett ID är ett heltal och att en e-postadress ser ut som en e-postadress. Men förlita dig aldrig på svartlistning av tecken som enda skydd — det går nästan alltid att kringgå. Parametrisering är huvudförsvaret, validering är ett extra lager.
Verifiera
Kör om samma övning från Del A mot din åtgärdade kod. Bekräfta dessutom att den normala funktionaliteten fortfarande fungerar — en vanlig miss är att en "fix" också sönder den legitima användningen. Testa gärna med indata som innehåller apostrof (t.ex. efternamnet O'Brien), som ska fungera felfritt i en korrekt parametriserad fråga.
Dokumentera
Skriv fyndet med allvarlighetsgrad enligt CVSS, kodreferens (fil och radnummer), och en konkret rekommendation med kodexempel. Lägg till en notering om huruvida sårbarheten var nåbar utan inloggning, vilket påverkar allvarlighetsgraden.
Reflektionsfrågor: Varför är parametrisering ett strukturellt försvar medan teckenfiltrering är ett ytligt? Vilka andra ställen i en applikation bygger kommandon av strängar?
Kunskapskontroll
Välj ett svar per fråga. Du får återkoppling direkt, och avsnittet markeras som klart när alla frågor är besvarade.
1Vad är grundorsaken till en injektionssårbarhet?
2Varför skyddar en parametriserad fråga?
3Du använder ett bibliotek som stöder parametrar, men bygger frågesträngen med konkatenering ändå. Vad gäller?
Injection — root cause and parameterized queries
Observe
Work through PortSwigger Web Security Academy's SQL injection module, or DVWA's SQL Injection exercise at the "Low" security level. Both are guided and include solutions. Note what happens to the application's response when input is not treated strictly as data.
Then raise DVWA's security level to "High" and observe that the same approach no longer works. The difference between those levels is the entire lesson.
Analyze the root cause
Use DVWA's built-in "View Source" button and compare the code across security levels. This is the single most instructive step in the lab.
The root cause is always the same: the application builds a command by gluing code and user data together into one string. The database then receives a finished string and has no way of knowing which part was intended as a command and which part was the user's data.
// The root cause: input becomes part of the command's structure
$query = "SELECT * FROM users WHERE id = '" . $_GET['id'] . "'";
What this code does — line by line
The vulnerable line, in PHP. It builds a database query by gluing text together. That is exactly what causes injection vulnerabilities.
- $query
- In PHP, variable names start with a dollar sign. A variable is a named box holding a value.
- "SELECT * FROM users WHERE id = '"
- A database query written as text. SELECT * means "fetch all columns", FROM users "from the users table", WHERE id = "where id equals".
- .
- The dot between pieces of text is PHP's way of gluing text together. The entire problem lives here.
- $_GET['id']
- The value the visitor supplied in the web address. It is pasted straight into the query, and the database can then no longer tell the query's structure from the visitor's text.
The same pattern recurs in other forms — OS commands assembled as strings, LDAP filters, file paths. Learn to recognize string concatenation into a command as a warning sign, regardless of technology.
Build the defense
1. Parameterized queries (prepared statements). Here the command's structure and the data are sent separately. The database always knows which is which, so user data cannot by definition alter the query's structure.
PHP with PDO:
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
$user = $stmt->fetch();
What this code does — line by line
The safe version in PHP. Instead of gluing, the query and the value are sent separately, in two steps. The database then always knows which is command and which is data.
- $pdo->prepare(...)
- Step one: send the query's shape to the database, without the value. PDO is PHP's standard way of talking to databases.
- ?
- The question mark is a placeholder — not a typo. It means "a value is coming here later".
- $stmt->execute([$_GET['id']])
- Step two: send the value. The database already knows the query's shape, so the value can never change its meaning.
- $stmt->fetch()
- Retrieves the result row so the program can use it.
Python with psycopg:
cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
What this code does — line by line
The same protection in Python. The form differs from PHP but the principle is identical: the query and the value are sent as two separate things.
- cur.execute(
- cur stands for cursor, the program's handle on the database. execute runs the query.
- %s
- Python's placeholder, the equivalent of PHP's question mark. It should not be wrapped in quotes.
- (user_id,)
- The values are passed as a second, separate argument. The comma after user_id is Python's way of marking this as a list with a single element.
Node.js with mysql2:
const [rows] = await conn.execute(
'SELECT * FROM users WHERE id = ?',
[userId]
);
What this code does — line by line
The same thing again, now in server-side JavaScript (Node.js). Third language, same principle — that is deliberate, so you recognize the pattern regardless of technology.
- const
- Declares a value that will not be changed afterwards.
- await
- Wait until the database has answered before the program continues. Database calls take time, and without await the program would race past.
- ?
- The placeholder, exactly as in PHP.
- [userId]
- The values in their own list, separate from the query.
- const [rows] =
- Picks out the first part of the database's answer and calls it rows.
execute("... WHERE id = " + user_id) is still vulnerable even if you are using a library that supports parameters. It is the use of parameters that protects you, not the library itself.2. An ORM that parameterizes by default. Django ORM, SQLAlchemy, Eloquent and Prisma parameterize automatically. The exception is raw queries (raw(), DB::raw()), which must be handled manually.
3. Least privilege in the database. The application account should not be root. An account holding only SELECT, INSERT and UPDATE on the tables it needs sharply limits the damage if something does go wrong:
CREATE USER 'webapp'@'localhost' IDENTIFIED BY '<strong password>';
GRANT SELECT, INSERT, UPDATE ON shop.orders TO 'webapp'@'localhost';
-- Deliberately NOT: DROP, ALTER, GRANT, or access to other databases
What this code does — line by line
SQL commands run in the database, not in the application. They create an account for the web application and give it only the rights it actually needs.
- CREATE USER 'webapp'@'localhost'
- Creates a database account named webapp that may only connect from the same machine.
- GRANT SELECT, INSERT, UPDATE
- Grants the right to read, add and change rows — and nothing more.
- ON shop.orders
- The rights apply only to the orders table in the shop database, nothing else.
- -- Deliberately NOT: DROP, ALTER...
- Two hyphens start a comment in SQL. The line records what was deliberately left out: the right to delete tables or hand out rights further.
4. Validation as a complement, not the main defense. Validate that an ID is an integer and that an email address looks like one. But never rely on blacklisting characters as your only protection — it can almost always be worked around. Parameterization is the primary defense; validation is an additional layer.
Verify
Repeat the Part A exercise against your remediated code. Also confirm normal functionality still works — a common mistake is a "fix" that breaks legitimate use. Test with input containing an apostrophe (the surname O'Brien, for instance), which should work flawlessly in a correctly parameterized query.
Document
Write the finding with a CVSS severity rating, a code reference (file and line number), and a concrete recommendation including a code example. Add a note on whether the vulnerability was reachable without authentication, which affects severity.
Reflection: Why is parameterization a structural defense while character filtering is a superficial one? Where else in an application are commands built out of strings?
Knowledge check
Pick one answer per question. You get feedback immediately, and the unit is marked complete once every question is answered.
1What is the root cause of an injection vulnerability?
2Why does a parameterized query protect you?
3You use a library that supports parameters, but build the query string by concatenation anyway. What applies?