Course topics

By WebNest Studio Engineering Team

Database and SQL Tutorial

Database Security Fundamentals

Database security covers the practices that protect stored data from unauthorized access, tampering, and leakage. Because databases typically hold an organization's most sensitive information — customer records, payment details, credentials — security failures here tend to be far more damaging than in almost any other part of an application.

Three practical areas cover most of what a working developer needs to understand: preventing SQL injection, applying the principle of least privilege to database accounts, and encrypting sensitive data at the column level.

Why it matters

The database holds what attackers want most: customer records, passwords, payment details. SQL injection has caused some of the largest data breaches on record, and it is entirely preventable. An application account that can do only what the application needs limits the damage when something else goes wrong.

SQL Injection

SQL injection happens when untrusted input (typically from a user) is concatenated directly into a SQL query string, allowing an attacker to change the meaning of the query by injecting their own SQL. The fix is to always use parameterized queries (also called prepared statements), where the query structure and the data values are sent to the database separately — the database never interprets a parameter's contents as SQL syntax, no matter what it contains.

A Vulnerable Query vs. a Parameterized Query

The difference is not stylistic — it is the entire security boundary between "user input" and "executable SQL."

Least-Privilege Database Users

The principle of least privilege means every database account should have only the permissions it actually needs to do its job, and nothing more. An application's everyday database user should typically be able to SELECT, INSERT, UPDATE, and DELETE only in the specific tables it uses — it should not have permission to DROP tables, alter schema, or access unrelated tables. This limits the damage if that account's credentials are ever compromised (for example, through a SQL injection vulnerability or a leaked connection string): the attacker is confined to whatever that narrow account can do, rather than gaining full control of the database.

Encrypting Sensitive Columns

Not all data needs the same protection, but fields like national ID numbers, payment card details, or health records often warrant encryption at the column level, in addition to standard access controls. Column-level (or "field-level") encryption means the value is stored in encrypted form in the database, and only application code holding the correct decryption key can turn it back into readable data — so even someone who gains direct read access to the database (through a backup leak, an over-permissioned account, or a misconfigured export) sees ciphertext instead of the raw sensitive value. This is a defense-in-depth measure: it doesn't replace access control, encryption in transit, or least privilege, but adds a further layer specifically for the most sensitive fields.

Examples

A SQL injection vulnerability versus a parameterized fix

SQL
-- VULNERABLE: user input concatenated directly into the query string
-- If username is: admin' --
-- the query becomes: SELECT * FROM users WHERE username = 'admin' --' AND password = '...'
-- The "--" comments out the password check entirely, logging the attacker in as admin.
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";

-- SAFE: parameterized query — the database treats username/password strictly as data
SELECT * FROM users WHERE username = ? AND password = ?;
-- Executed with parameters bound separately, e.g. via a prepared statement:
-- stmt.setString(1, username); stmt.setString(2, password);
Output
Vulnerable version: attacker logs in as admin without knowing the password.
Parameterized version: the literal string "admin' --" is compared as a whole
username value, matches no row, and login correctly fails.

Creating a least-privilege application database user

SQL
-- Instead of using a superuser/admin account in the application, create a scoped one:
CREATE USER app_user WITH PASSWORD 'a-strong-generated-secret';

GRANT SELECT, INSERT, UPDATE, DELETE ON orders, order_items, customers TO app_user;
-- Deliberately no GRANT on other tables, and no DROP/ALTER/CREATE privileges at all.

REVOKE ALL ON payments_ledger FROM app_user;
Output
CREATE ROLE
GRANT
REVOKE
(app_user can now read/write only the tables the application needs, and cannot
touch schema or the restricted payments_ledger table even if compromised.)

Common Mistakes

  • Building SQL queries with string concatenation or string formatting instead of parameterized queries/prepared statements, leaving the application open to SQL injection.
  • Running the application's everyday database connection as a superuser or owner account "for convenience," turning any injection or credential leak into full database compromise.
  • Assuming encrypting the whole disk (encryption at rest) is sufficient for highly sensitive fields, when column-level encryption is what protects against misuse by anyone with ordinary query access.
  • Trusting client-side validation alone to prevent malicious input, when all real protection against injection must happen at the query-construction layer on the server.

Exercise

This exercise uses PostgreSQL roles, so it cannot run in the playground. Create a login role app_reader for a reporting tool that may read the customers and orders tables and nothing else. It must not be able to insert, update or delete. Then show the parameterised form of the query that looks up a customer by email, as an application would send it.

Expected result: connected as app_reader, a SELECT on customers succeeds and a DELETE is refused with a permission error.

SQL · starter code
-- TODO: create the role app_reader with a password, able to log in

-- TODO: grant it read access to customers and orders only

-- TODO: the lookup by email, written with a placeholder instead of the value
Show hints
  • A role that can connect is created with CREATE ROLE name LOGIN PASSWORD '...'.
  • Read access is GRANT SELECT ON table TO role. In PostgreSQL a placeholder is written $1.
Show solution
SQL · solution
CREATE ROLE app_reader LOGIN PASSWORD 'use-a-long-random-password';

GRANT SELECT ON customers, orders TO app_reader;

-- Sent by the application as a prepared statement; the email travels separately as data.
SELECT id, name FROM customers WHERE email = $1;

Quick quiz

1. What causes SQL injection?

  • A. Using too many indexes
  • B. Slow queries
  • C. Building a query by inserting user input directly into the SQL text
  • D. Using transactions
Show answer

C. The input can then change the meaning of the statement.

2. What is the correct defence against SQL injection?

  • A. Parameterised queries, which send values separately from the SQL
  • B. Hiding the error messages
  • C. Checking the length of the input
  • D. Using POST instead of GET
Show answer

A. A value sent as a parameter is always treated as data, never as SQL.

3. What does the principle of least privilege mean for a database account?

  • A. Every account should be an administrator
  • B. It should have only the permissions its task requires
  • C. Passwords should be short
  • D. Accounts should be shared between applications
Show answer

B. If the account is compromised, the attacker can do no more than the account could.

Interview questions

What is SQL injection and how do you prevent it?

Show answer

SQL injection is an attack in which input supplied by a user is treated as part of an SQL statement, because the application built the statement by joining strings. Entering ' OR '1'='1 into a login form, for example, can make the WHERE clause true for every row. The prevention is to use parameterised queries or prepared statements, in which the SQL and the values are sent separately. Least-privilege accounts limit the damage if a flaw remains.

How should passwords be stored in a database?

Show answer

Never as plain text and never with reversible encryption. Each password is hashed with a slow algorithm designed for passwords, such as Argon2, bcrypt or scrypt, with a unique random salt per user, and only the hash is stored. At login the submitted password is hashed in the same way and compared. If the table is stolen, the attacker still has to guess each password separately, and the slowness of the algorithm makes that expensive.

Key Points to Remember

  • Always use parameterized queries/prepared statements instead of string-concatenating user input into SQL — this is the primary defense against SQL injection.
  • Apply least privilege: application database accounts should hold only the permissions they need on only the tables they use.
  • Encrypt especially sensitive columns (payment data, national IDs) at the field level as a defense-in-depth layer beyond access control.
  • Database security requires multiple layers together — parameterization, least privilege, and encryption each cover different failure modes.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering