Secure Development
Lifecycle
SQL Injection
derived from
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 1
Example for SQL-Injection Attacks
2009: „Hackers breached a database
at social networking application
maker RockYou Inc. and accessed
username and password information
on more than 30 million individuals
with accounts at the company.
The passwords and user names were
stored in clear text on the
compromised database and the user
names were by default the same as
the users Gmail, Yahoo, Hotmail or
other Web mail account.“
https://www.computerworld.com/article/2522045/rockyou-hack-exposes-
names--passwords-of-30m-accounts.html
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 2
Example for SQL-Injection Attacks
2021: „According to DDoSecrets'
Best, the hacker says that they
pulled out Gab's data via a SQL
https://www.wired.com/story/gab-hack-data-breach-ddosecrets/
injection vulnerability in the site—
a common web bug in which a
text field on a site doesn't
differentiate between a user's
input and commands in the site's
code, allowing a hacker to reach
in and meddle with its backend
SQL database. “
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 3
The Fundamental Cause
Mixing data and code together is
the cause of several types of
vulnerabilities and attacks!!!
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 4
Overview
• SQL Tutorial
• SQL Injection Basics
• Practice on SQL Injection
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 5
SQL Tutorial
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 6
SQL Tutorial
Start your own playground instance:
$ docker run --rm --name db -e • we can use a local Docker container
MYSQL_ROOT_PASSWORD=secret -d
mysql:latest as a SQL playground
• MySQL is a common database engine
$ docker exec -it db /bin/bash Enter container to use database:
• in order to work with database, we
need a client program
• we enter the database container to
use it
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 7
SQL Tutorial
Log in to MySQL:
# mysql -h localhost -u root -psecret • We can log in to our local database
Welcome …
. . . instance using the password setup
mysql> before
Create a Database:
• Inside MySQL, we can create multiple
mysql> SHOW DATABASES;
. . .
databases. “SHOW DATABSES”
mysql> CREATE DATABASE dbtest; command can be used to list existing
databases.
• We will create a new database called
dbtest
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 8
SQL Tutorial: Create a Table
mysql> USE dbtest;
mysql> CREATE TABLE employee ( • A relational database engine
ID
Name
INT (6) NOT NULL AUTO_INCREMENT,
VARCHAR (30) NOT NULL, organizes its data using databases
and tables.
EID VARCHAR (7) NOT NULL,
PASSWORD VARCHAR (60),
Salary INT (10),
• We create a table called employee
SSN VARCHAR (11),
PRIMARY KEY (ID)
);
mysql> DESCRIBE employee;
with seven attributes (i.e. columns)
+----------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra | for the database “dbtest”
+----------+-------------+------+-----+---------+----------------+
| ID
| Name
| int | NO
| varchar(30) | NO
| PRI | NULL
| | NULL
| auto_increment |
| | • We need to let the system know
| EID | varchar(7) | NO
| PASSWORD | varchar(60) | YES |
| | NULL
| NULL
|
|
|
| which database to use as there may
be multiple databases
| Salary | int | YES | | NULL | |
| SSN | varchar(11) | YES | | NULL | |
+----------+-------------+------+-----+---------+----------------+
• After a table is created, we can use
6 rows in set (0.00 sec)
describe to display the structure of
the table
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 9
SQL Tutorial: Insert a Row
• We can use the INSERT INTO statement to insert a new record into a
table :
mysql> INSERT INTO employee (Name, EID, Password, Salary, SSN)
VALUES ('Alice', 'EID5000', 'passwd123', 80000,
'555-55-555');
• Here, we insert a record into the “employee” table.
• We do not specify a value of the ID column, as it will be automatically
set by the database.
mysql> INSERT INTO employee (Name, EID, Password, Salary, SSN) VALUES
('Bob', 'EID5001', 'passwd123', 80000, '555-66-555'),
('Charlie', 'EID5002', 'passwd123', 80000, '555-77-555'),
('David', 'EID5003', 'passwd123', 80000, '555-88-555');
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 10
SQL Tutorial: SELECT Statement
mysql> SELECT * FROM employee;
+----+---------+---------+-----------+--------+------------+ • The SELECT statement is the
most common operation on
| ID | Name | EID | PASSWORD | Salary | SSN |
+----+---------+---------+-----------+--------+------------+
| 1 | Alice | EID5000 | passwd123 | 80000 | 555-55-555 |
| 2 | Bob | EID5001 | passwd123 | 80000 | 555-66-555 |
| 3 | Charlie | EID5002 | passwd123 | 80000 | 555-77-555 | databases
| 4 | David | EID5003 | passwd123 | 80000 | 555-88-555 |
+----+---------+---------+-----------+--------+------------+
4 rows in set (0.00 sec) • It retrieves information from a
mysql> SELECT Name, EID, Salary FROM employee;
+---------+---------+--------+
database
• asks the database for all its
| Name | EID | Salary |
+---------+---------+--------+
records, including all the columns
| Alice | EID5000 | 80000 |
| Bob | EID5001 | 80000 |
| Charlie | EID5002 | 80000 |
| David | EID5003 | 80000 |
+---------+---------+--------+
• asks the database only for Name,
4 rows in set (0.00 sec) EID and Salary columns
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 11
SQL Tutorial: WHERE Clause
• It is uncommon for a SQL query to retrieve all records in a database.
• WHERE clause is used to set conditions for several types of SQL
statements including SELECT, UPDATE, DELETE etc.
mysql> SQL Statement
WHERE predicate;
• The above SQL statement only reflects the rows for which the
predicate in the WHERE clause is TRUE.
• The predicate is a logical expression; multiple predicates can be
combined using keywords AND and OR.
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 12
SQL Tutorial: WHERE Clause
mysql> SELECT * FROM employee WHERE EID='EID5001';
+----+------+---------+-----------+--------+------------+
| ID | Name | EID | PASSWORD | Salary | SSN |
+----+------+---------+-----------+--------+------------+
| 2 | Bob | EID5001 | passwd123 | 80000 | 555-66-555 |
+----+------+---------+-----------+--------+------------+
1 row in set (0.00 sec)
mysql> SELECT * FROM employee WHERE EID='EID5001' OR Name='David';
+----+-------+---------+-----------+--------+------------+
| ID | Name | EID | PASSWORD | Salary | SSN |
+----+-------+---------+-----------+--------+------------+
| 2 | Bob | EID5001 | passwd123 | 80000 | 555-66-555 |
| 4 | David | EID5003 | passwd123 | 80000 | 555-88-555 |
+----+-------+---------+-----------+--------+------------+
2 rows in set (0.00 sec)
• The first query returns a record that has EID5001 in EID field
• The second query returns the records that satisfy either EID=‘EID5001’ or
Name=‘David’
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 13
SQL Tutorial: WHERE Clause
• If the condition is always True, then all the rows are affected by the SQL
statement
mysql> SELECT * FROM employee WHERE 1=1;
+----+---------+---------+-----------+--------+------------+
| ID | Name | EID | PASSWORD | Salary | SSN |
+----+---------+---------+-----------+--------+------------+
| 1 | Alice | EID5000 | passwd123 | 80000 | 555-55-555 |
| 2 | Bob | EID5001 | passwd123 | 80000 | 555-66-555 |
| 3 | Charlie | EID5002 | passwd123 | 80000 | 555-77-555 |
| 4 | David | EID5003 | passwd123 | 80000 | 555-88-555 |
+----+---------+---------+-----------+--------+------------+
4 rows in set (0.00 sec)
• This 1=1 predicate looks quite useless in real queries, but it will become
useful in SQL Injection attacks
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 14
SQL Tutorial: ORDER BY Clause
mysql> SELECT * FROM employee;
+----+---------+---------+-----------+--------+------------+ • using „order“ we can ask results
to be sorted by one or more
| ID | Name | EID | PASSWORD | Salary | SSN |
+----+---------+---------+-----------+--------+------------+
| 1 | Alice | EID5000 | passwd123 | 80000 | 555-55-555 |
| 2 | Bob | EID5001 | passwd123 | 80000 | 555-66-555 |
| 3 | Charlie | EID5002 | passwd123 | 80000 | 555-77-555 | culumns
| 4 | David | EID5003 | passwd123 | 80000 | 555-88-555 |
+----+---------+---------+-----------+--------+------------+
4 rows in set (0.00 sec) • to reference these columns, we
mysql> SELECT * FROM employee ORDER BY 2;
can provide their name – or the
+----+---------+---------+-----------+--------+------------+
| ID | Name | EID | PASSWORD | Salary | SSN | index in the resulting table
+----+---------+---------+-----------+--------+------------+
| 1 | Alice | EID5000 | passwd123 | 80000 | 555-55-555 |
| 2 | Bob | EID5001 | passwd123 | 80000 | 555-66-555 |
| 3 | Charlie | EID5002 | passwd123 | 80000 | 555-77-555 |
| 4 | David | EID5003 | passwd123 | 80000 | 555-88-555 |
+----+---------+---------+-----------+--------+------------+
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 15
SQL Tutorial: UPDATE Statement
• We can use the UPDATE Statement to modify an existing record
mysql> UPDATE employee SET Salary=82000 WHERE Name='Bob';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT * from employee WHERE Name='Bob';
+----+------+---------+-----------+--------+------------+
| ID | Name | EID | PASSWORD | Salary | SSN |
+----+------+---------+-----------+--------+------------+
| 2 | Bob | EID5001 | passwd123 | 82000 | 555-66-555 |
+----+------+---------+-----------+--------+------------+
1 row in set (0.00 sec)
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 16
SQL Tutorial: Metadata
• A relational database system has separate databases for organization
mysql> DESCRIBE information_schema.columns;
+--------------------------+----------------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------------------+----------------------------+------+-----+---------+-------+
| TABLE_CATALOG | varchar(64) | NO | | NULL | |
| TABLE_SCHEMA | varchar(64) | NO | | NULL | |
| TABLE_NAME | varchar(64) | NO | | NULL | |
| COLUMN_NAME | varchar(64) | YES | | NULL | |
| ORDINAL_POSITION | int unsigned | NO | | NULL | |
| COLUMN_DEFAULT | text | YES | | NULL | |
| IS_NULLABLE | varchar(3) | NO | | | |
| DATA_TYPE | longtext | YES | | NULL | |
| CHARACTER_MAXIMUM_LENGTH | bigint | YES | | NULL | |
| CHARACTER_OCTET_LENGTH | bigint | YES | | NULL | |
| NUMERIC_PRECISION | bigint unsigned | YES | | NULL | |
| NUMERIC_SCALE | bigint unsigned | YES | | NULL | |
| DATETIME_PRECISION | int unsigned | YES | | NULL | |
| CHARACTER_SET_NAME | varchar(64) | YES | | NULL | |
| COLLATION_NAME | varchar(64) | YES | | NULL | |
| COLUMN_TYPE | mediumtext | NO | | NULL | |
| COLUMN_KEY | enum('','PRI','UNI','MUL') | NO | | NULL | |
| EXTRA | varchar(256) | YES | | NULL | |
| PRIVILEGES | varchar(154) | YES | | NULL | |
| COLUMN_COMMENT | text | NO | | NULL | |
| GENERATION_EXPRESSION | longtext | NO | | NULL | |
| SRS_ID | int unsigned | YES | | NULL | |
+--------------------------+----------------------------+------+-----+---------+-------+
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 17
SQL Tutorial: Metadata
mysql> SELECT DISTINCT table_schema
FROM information_schema.columns; • “table_schema” is another word
for “database”
+--------------------+
| TABLE_SCHEMA |
+--------------------+
| mysql |
| information_schema |
| performance_schema |
• we can easily find out which
| sys
| dbtest
|
| databases are there on the
system
+--------------------+
5 rows in set (0.00 sec)
mysql> SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema='dbtest';
• also, we can find out e.g. all the
+------------+-------------+-----------+
| TABLE_NAME | COLUMN_NAME | DATA_TYPE | names of columns in tables
+------------+-------------+-----------+
| employee | ID | int |
| employee | Name | varchar |
| employee | EID | varchar |
| employee | PASSWORD | varchar |
| employee | Salary | int |
| employee | SSN | varchar |
+------------+-------------+-----------+
6 rows in set (0.00 sec)
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 18
SQL Tutorial: Functions and Variables
mysql> SELECT null, @@version, database(), user();
+------+-----------+------------+----------------+ • queries do not need to work on
| NULL | @@version | database() | user() |
+------+-----------+------------+----------------+
tables
• “null” represents a special value
| NULL | 8.0.33 | dbtest | root@localhost |
+------+-----------+------------+----------------+
1 row in set (0.00 sec)
that may become useful …..
• system variables represent
configuration settings and start
with “@@”
• various functions can be used,
among them information
functions
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 19
SQL Tutorial: Functions and Variables
mysql> SELECT SSN, substring(SSN,5,2)
FROM employee; • various functions in SQL allow
+------------+--------------------+
| SSN | substring(SSN,5,2) | for transformation of data
+------------+--------------------+
| 555-55-555 | 55
| 555-66-555 | 66
|
| • „substring()“ allows to extract
| 555-77-555 | 77
| 555-88-555 | 88
|
| parts of strings
+------------+--------------------+
4 rows in set (0.00 sec)
mysql> SELECT
• „concat()“ allows to concatenate
concat(substring(SSN,5,2),'xxx') 'Sub-SSN'
FROM employee;
+---------+
| Sub-SSN |
+---------+
strings
| 55xxx
| 66xxx
|
|
• note the new name for the
| 77xxx
| 88xxx
|
|
resulting computed attribute
+---------+
4 rows in set (0.00 sec)
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 20
SQL Tutorial: Functions and Variables
mysql> SELECT column_name
FROM information_schema.columns • „group_concat()“ combines all
values in a column into one
WHERE table_schema='dbtest' and table_name='employee';
+-------------+
| COLUMN_NAME |
+-------------+
| ID | value
| Name |
| EID
| PASSWORD
|
|
• note that we could also provide
| Salary
| SSN
|
|
an extra argument to indicate a
+-------------+
6 rows in set (0.00 sec)
custom separator
mysql> SELECT group_concat(column_name)
FROM information_schema.columns
WHERE table_schema='dbtest' and table_name='employee';
+---------------------------------+
| group_concat(column_name) |
+---------------------------------+
| ID,Name,EID,PASSWORD,Salary,SSN |
+---------------------------------+
1 row in set (0.00 sec)
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 21
SQL Tutorial: Type Conversion
mysql> SELECT ID, concat(cast(ID as char),'st')
FROM employee; • „cast()“ allows for various type
conversions
+----+-------------------------------+
| ID | concat(cast(ID as char),'st') |
+----+-------------------------------+
• into strings: „char“
| 1 | 1st |
| 2 | 2st |
| 3 | 3st |
| 4 | 4st |
+----+-------------------------------+
• into numbers: „decimal“
4 rows in set (0.00 sec)
mysql> SELECT
substring(SSN,5,2) SubSSN,
cast(substring(SSN,5,2) as decimal)+1 Next
FROM employee;
+--------+------+
| SubSSN | Next |
+--------+------+
| 55 | 56 |
| 66 | 67 |
| 77 | 78 |
| 88 | 89 |
+--------+------+
4 rows in set (0.00 sec)
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 22
SQL Tutorial: UNION Clause
mysql> SELECT 1, 2, 3, 4, 5
UNION • „union“ allows to combine the
results of two queries
SELECT 6, 7, 8, 9, 10;
+---+---+---+---+----+
| 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+----+
| 1 | 2 | 3 | 4 | 5 |
| 6 | 7 | 8 | 9 | 10 |
• note that the tables combined
+---+---+---+---+----+
2 rows in set (0.00 sec) using „union“ need to be
mysql> SELECT *
FROM employee
„compatible“
• same number of columns
UNION
SELECT 1, cast(2 as char), cast(3 as char),
NULL, 5, NULL;
+----+---------+---------+-----------+--------+------------+
| ID | Name | EID | PASSWORD | Salary | SSN |
• same types in colums
+----+---------+---------+-----------+--------+------------+
| 1 | Alice | EID5000 | passwd123 | 80000 | 555-55-555 | • „NULL“ is compatible with all
| 2 | Bob | EID5001 | passwd123 | 80000 | 555-66-555 |
| 3 | Charlie | EID5002 | passwd123 | 80000 | 555-77-555 | types
| 4 | David | EID5003 | passwd123 | 80000 | 555-88-555 |
| 1 | 2 | 3 | NULL | 5 | NULL |
+----+---------+---------+-----------+--------+------------+
5 rows in set (0.00 sec)
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 23
SQL Tutorial: Extras
mysql> SELECT sleep(5);
+----------+ • an apparently useless function is
| sleep(5) |
+----------+ the “sleep()” function
| 0 |
+----------+
1 row in set (5.00 sec) • ….. but it may well become
useful for us ….. J
mysql> SELECT 0 FROM (SELECT sleep(5));
ERROR 1248 (42000): Every derived table must have its
own alias • in some cases we have to
mysql> SELECT 0 FROM (SELECT sleep(5)) x; provide explicit names (alias) for
+---+
| 0 | resulting tables
+---+
| 0 |
+---+
1 row in set (5.00 sec)
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 24
SQL Tutorial: Comments
• MySQL supports three comment styles
mysql> SELECT * FROM employee; # Comment to the end of line
mysql> SELECT * FROM employee; -- Comment to the end of line
mysql> SELECT * FROM /* In-line comment */ employee;
• Text from the # character to the end of line is treated as a comment
• Text from the “-- ” to the end of line is treated as a comment.
• Note the space character after “--”
• Similar to C language, text between /* and */ is treated as a
comment
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 25
SQL Tutorial: Errors and Warnings
mysql> select "hello" where 1 in (select 1);
+-------+ • applications typically do not
show the queries executed or
| hello |
+-------+
| hello |
+-------+
1 row in set (0.00 sec) their direct results
mysql> select "hello" where 1 in (select "bye"); • but some applications try to
provide the user with error
Empty set, 1 warning (0.00 sec)
mysql> show warnings;
+---------+------+------------------------------------------+
| Level | Code | Message | messages or warnings
+---------+------+------------------------------------------+
| Warning | 1292 | Truncated incorrect DOUBLE value: 'bye' |
+---------+------+------------------------------------------+ • also note that for some
statements, it can just be
1 row in set (0.00 sec)
determined at runtime if there
are problems
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 26
SQL Injection Basics
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 27
Interaction with Database in Web Applications
• A typical web application
consists of three major
components.
"Dieses Foto" von Unbekannter Autor ist lizenziert gemäß CC BY-SA
• As we notice in the figure, the
users do not directly interact
with the database but through a
web server.
• If this channel is not
implemented properly,
malicious users can attack the
database.
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 28
Naïve Implementation of DB Queries
$id = $_COOKIE["mid"];
mysql_query("SELECT MessageID, Subject FROM • DB queries to be executed are
messages WHERE MessageID = '$id'");
often SQL statements to which
data provided by users is added
+
• naïve approach: string
•
1432
concatenation or interpolation
è
• problem: unsanitized input may
change semantics of query
SELECT MessageID, Subject FROM messages WHERE
MessageID = '1432'
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 29
Example Attacks on DB Queries
$id = $_COOKIE["mid"]; 1432' or '1'='1
mysql_query("SELECT MessageID, Subject FROM
messages WHERE MessageID = '$id'");
SELECT MessageID, Subject FROM messages WHERE
MessageID = '1432' or '1'='1'
$id = $_COOKIE["mid"]; 1432' #
$author = $_COOKIE["author"];
mysql_query("SELECT MessageID, Subject FROM
SELECT MessageID, Subject FROM messages WHERE
messages WHERE MessageID = '$id'
MessageID = '1432' # AND Author='Tom'
AND Author='$author'");
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 30
Example Attacks on DB Queries
$eid = $_POST['EID']; "EID": "35"
$oldpwd = $_POST['OldPassword']; "OldPassword": "something"
$newpwd = $_POST['NewPassword']; "NewPassword": "notsecure' #"
$sql = "UPDATE employee UPDATE employee
SET password='$newpwd' SET password='notsecure' #
WHERE eid='$eid' and WHERE eid='35' and
password='$oldpwd'"; password='something'
mysql_query($sql);
"EID": "35' #"
"OldPassword": "secret"
"NewPassword": "secret', salary=10000000"
UPDATE employee
SET password='secret', salary=10000000
WHERE eid='35' #' and
password='$oldpwd'
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 31
Example Attacks on DB Queries
$id = $_COOKIE["mid"];
mysql_query("SELECT MessageID, Subject FROM • note that some attempted
messages WHERE MessageID = '$id'");
injections might fail due to
implementation restrictions
1432'; DROP TABLE messages #
• here: „mysql_query“ does (did)
not support multiple queries
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 32
Other Variants of SQL Injection
union-based SQL injection blind SQL injection (boolean-based)
• use „UNION“ to combine results of • if no output is shown, try to inject
queries conditions to find out information
• look e.g. in „information_schema“ about database
for other tables, … blind SQL injection (time-based)
error-based SQL injection • ….. inject sleep commands and
• if no output of forms is shown, try observe timing behaviour
to provoke errors
• errors may provide information No-SQL injection ….
about the database
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 33
Practice on SQL Injection
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 34
Practice
• appsecco/sqlinjection-training-
app provides a nice extended
training environment with more
sophisticated challenges
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 35
Cheat Sheet(s) on SQL Injection
• proper „cheating“ is acceptable
• e.g. via pentestmonkey.net
• also note that approaches may
be different for different DBMS
used
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 36
Extra
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 37
Automation
• note that in practice, hackers/
attackers only rarely do SQL
injection manually
• instead, various specialized
tools are used that have
implemented detection and
exploitation mechanisms for
various database engines
• example: sqlmap
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 38
Further Training
• various further resources exist
• e.g. PortSwigger Academy on
SQL injection
• structured learning with labs
• e.g. audi-1/sqli-labs
• older, no Docker setup
• e.g. anukall/SQLI-Labs
• no Docker setup
SDLC / D. Moser, M. Moser / SoSe 2023 / Slide 39