Cybersecurity
A03 Injection
SQL injection

A03:2021 - Injection

ℹ️

For the information about Injection, visit page (opens in a new tab)

SQL Injection

πŸ”

CWE-89: SQL Injection

Attack

Code preparation

To prepare for this attack, we need to slightly modify the implementation of handling the login request, because we have already implemented a secure way in section ❕JWT authentication.
Replace the handling of login request in routes/jwtAuth.ts with the following code:

jwtAuth.ts
// LOGIN
router.post("/login", bruteforce.prevent, async (req, res) => {
    const {email, password} = req.body;
    let user = null;
 
    try {
        user = await pool.query(
            "SELECT email, password from useraccount WHERE email = '" + email + "'"
        );
 
        if (user?.rows?.length === 0 || user?.length === 0) {
            return res.status(401).json({message: "Invalid email.", user: user});
        }
 
        const validPassword = password === user.rows[0].password;
 
        if (!validPassword) {
            return res.status(401).json({message: "Invalid password.", user: user});
        }
 
        const token = generateJWT(user.rows[0].password);
        res.json({
            success: true,
            message: "Authentication successful!",
            token: token,
            user: user
        });
    } catch (err) {
        console.error(err.message);
        res.status(500).send({message:"Server error", user: user});
    }
});
πŸ’‘

Compared to the previous implementation, we get rid of the inputValidation middleware, we handle passwords as plain text and we adjusted the query to be vulnerable to SQL injection.

Gather internal information

Firstly, we are going to gather some internal information about the database and tables, which will then help us to perform some more serious attacks.

Database type

  • Enter the following inside the email field on the HackHealth login screen.
' UNION SELECT usename, NULL FROM pg_user--
  • Leave the password field empty and click on the login button.
  • When you open Developer Tools, in the Console, when digging deeper into user.rows in JSON response from the server, you will see the type of database which is used.
SQL injection console log

Database name

  • Enter the following inside the email field on the HackHealth login screen, keep the password field empty, and hit Login.
' UNION SELECT current_database(), null--
  • After digging deeper into the servers response, in user.rows, you will see the name of the database: hackhealthdb

Names of tables

  • In the same way as in the previous step, we can get the names of the tables by inserting the following code into the email field.
'; SELECT table_name FROM information_schema.tables WHERE table_schema='public'; --
  • Again, after digging deeper into user[1].rows, you will see the names of the tables.
    • useraccount
    • appointment
    • medicalreport
πŸ’‘

To sum up, we have gathered the following information:

  • Database type: PostgreSQL
  • Database name: hackhealthdb
  • Names of tables: useraccount, appointment, medicalreport

We are going to use this information in the next steps.

Grant unauthorized access

In this attack, we aim to get login credentials for all the users in the table useraccount.
Into an email field, we are going to insert the following code:

' OR 1=1; --
ℹ️

This input would cause the SQL query to be constructed as follows:

SELECT email, password form useraccount WHERE email = '' OR 1=1; --

The semicolon (;) terminates the current statement and the two dashes (--) indicate a comment, effectively ignoring the remaining part of the original query. The injected SQL code OR 1=1 will always evaluate to true, effectively bypassing the authentication check and returning all records from the "useraccount" table.

  • In the response from the server (user.rows), we are now able to see all the users with their login credentials.

Corrupt the tables and database

SQL injection attack is not only about getting the data but also about corrupting the data.

Drop the table

  • Enter the following inside the email field on the HackHealth login screen.
'; DROP TABLE useraccount CASCADE; --
  • Congratulations, you have just dropped the table useraccount!
  • Now, when you try to log in with the credentials you have obtained in the previous step, you will not be able to log in -> the table of users just does not exist anymore.
ℹ️

This was made possible only because when creating the tables, foreign keys were created with CASCADE option. It means that when the table is dropped, all the tables that depend on it are also dropped.

Drop the database

Dropping the database is quite more challenging than dropping individual tables. When you will try to execute the command:

'; DROP DATABASE hackhealthdb; --

you will get an error in the server log: DROP DATABASE cannot run inside a transaction block. This indicates that PostgreSQL does not allow a database to be dropped while there are active connections to it. This is to prevent accidental data loss due to unintended drops. A solution would be to terminate all running connections to the database, but this could not be done through SQL injection.
Although the attempt about dropping the whole database was not successful, we have already caused considerable damage.

️🚫

Do not forget to put the database to its initial state after the attack.
To do so, stop the server and run the following commands in the terminal:

npm run db-drop
npm run db-create
npm run db-seed

After running the following commands, db should be in the initial state.

Prevention

To prevent SQL injection attacks, it's important to use parameterized queries instead of concatenating user input directly into SQL queries. Parameterized queries use placeholders for user input and sanitize the input to prevent malicious SQL code from being executed.
In the source code, we can replace the current implementation of handling login request with the following code:

jwtAuth.ts
// LOGIN
router.post("/login", bruteforce.prevent, inputValidation, async (req, res) => {
    const {email, password} = req.body;
 
    try {
        const user = await pool.query(
            "SELECT * FROM useraccount WHERE email = $1",
            [email]
        );
 
        if (user.rows.length === 0) {
            return res.status(401).json("Invalid email.");
        }
 
        // VUL A02
        // const validPassword = await bcrypt.compare(password, user.rows[0].password);
 
        const validPassword = password === user.rows[0].password;
 
        if (!validPassword) {
            return res.status(401).json("Invalid password.");
        }
 
        const token = generateJWT(user.rows[0].password);
        res.json({
            success: true,
            message: "Authentication successful!",
            token: token,
        });
    } catch (err) {
        console.error(err.message);
        res.status(500).send("Server error");
    }
});

As you can see in the code above, we are using placeholders for user input and sanitizing the input (inputValidation middleware) to prevent malicious SQL code from being executed.


Also, in the file Login.js (easily find by pressing Shift+Shift and typing Login.js), we need to delete the line:

console.log(parseJSON)

By deleting the line, we will no longer be able to see the response from the server in the Console in Developer Tools, which is a better practice.

Verification

To verify that the vulnerability has been fixed, you can try running some commands from the Attack section again. The result should be, that injecting malicious SQL code into the email field will not trigger any unintended behavior.

Sources