Skip to content

SQL Security: Injection, Roles & Grants

Một câu SQL injection có thể xóa sạch database của bạn. Bài này sẽ dạy cách tấn công để hiểu cách phòng thủ.


1️⃣ SQL Injection: Hiểu để Phòng

Cơ chế tấn công

SQL Injection xảy ra khi user input được nối trực tiếp vào câu SQL mà không được escape.

Code ính lỗ hổng:

python
# SECURITY: Never hardcode credentials in production code
# Use environment variables or secure credential management systems
# ❌ NGUY HIỂM: Nối string trực tiếp
username = request.GET['username']  # User nhập: admin' --
password = request.GET['password']  # User nhập: anything

query = f"SELECT * FROM users WHERE username = '{username}' AND password = os.getenv("DB_PASSWORD")"
# Kết quả:
# SELECT * FROM users WHERE username = 'admin' --' AND password = os.getenv("DB_PASSWORD")
# Phần sau -- bị comment hóa -> Chỉ cần đúng username!

Các kiểu tấn công phổ biến

1. Authentication Bypass

-- SECURITY: Never hardcode credentials in production code -- Use environment variables or secure credential management systems -- Input: username = ' OR '1'='1 -- Password: anything SELECT * FROM users WHERE username = '' OR '1'='1' AND password = ENV["DB_PASSWORD"]; -- '1'='1' luôn TRUE -> Trả về TẤT CẢ users! SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'anything'; -- '1'='1' luôn TRUE -> Trả về TẤT CẢ users!


#### 2. Data Exfiltration (UNION Attack)

```sql
-- Giả sử app hiển thị kết quả tìm kiếm sản phẩm
-- Input: ' UNION SELECT id, username, password FROM users --

SELECT name, price FROM products WHERE name LIKE '%' UNION SELECT id, username, password FROM users --%';
-- Attacker thấy được toàn bộ users table!

3. Database Destruction

sql
-- Input: '; DROP TABLE users; --
SELECT * FROM users WHERE id = ''; DROP TABLE users; --';
-- Nếu app cho phép multiple statements -> Xóa sạch bảng users!

4. Blind SQL Injection (Khi không thấy output)

sql
-- Attacker đoán dần dần
-- Input: ' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin') = 'a' --
-- Nếu response khác -> ký tự đầu của password là 'a'
-- Lặp lại 1000 lần -> Có được password

Cách phòng thủ: Prepared Statements

Prepared Statements (Parameterized Queries) TÁCH BIỆT code SQL và data. Database biết đâu là lệnh, đâu là giá trị.

python
# ✅ AN TOÀN: Prepared Statement với placeholder
username = request.GET['username']  # User nhập: admin' --
password = request.GET['password']

# Python với psycopg2
cursor.execute(
    "SELECT * FROM users WHERE username = %s AND password = %s",
    (username, password)  # Parameters truyền riêng!
)
# Database nhận:
# Query: SELECT * FROM users WHERE username = $1 AND password = $2
# Params: ["admin' --", "anything"]
# -> Tìm user có username là "admin' --" (đúng literally) -> Không tìm thấy!
javascript
// Node.js với pg
const result = await client.query(
    'SELECT * FROM users WHERE username = $1 AND password = $2',
    [username, password]
);
java
// Java với JDBC
PreparedStatement stmt = connection.prepareStatement(
    "SELECT * FROM users WHERE username = ? AND password = ?"
);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();

Prepared Statements hoạt động thế nào?

⚠️ Prepared Statements KHÔNG bảo vệ mọi thứ

  • Table/Column names: Không thể parameterize SELECT * FROM $1 -> Phải whitelist.
  • ORDER BY: ORDER BY $1 không hoạt động -> Validate và whitelist.
  • LIMIT/OFFSET: Một số driver cho phép, một số không.

2️⃣ RBAC: Role-Based Access Control

Concept

Thay vì cấp quyền trực tiếp cho user, bạn tạo Role và gán user vào role. Một user có thể có nhiều roles.

[User] --member of--> [Role] --has--> [Permissions]

Tạo Roles và Users

sql
-- Tạo roles
CREATE ROLE readonly;           -- Role chỉ đọc
CREATE ROLE readwrite;          -- Role đọc/ghi
CREATE ROLE admin;              -- Role admin
CREATE ROLE app_backend;        -- Role cho backend service

-- Tạo user (login role)
CREATE USER analyst WITH PASSWORD 'secure_password';
CREATE USER backend_service WITH PASSWORD 'another_secure_password';

-- Gán role cho user
GRANT readonly TO analyst;
GRANT readwrite TO backend_service;

GRANT & REVOKE Permissions

sql
-- Cấp quyền SELECT cho role readonly
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;

-- Cấp quyền CRUD cho role readwrite
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO readwrite;

-- Cấp quyền cụ thể trên từng bảng
GRANT SELECT ON users TO readonly;
GRANT SELECT, INSERT ON orders TO readwrite;
GRANT ALL PRIVILEGES ON products TO admin;  -- SELECT, INSERT, UPDATE, DELETE, TRUNCATE, etc.

-- Cấp quyền trên sequence (cho auto-increment)
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO readwrite;

-- Thu hồi quyền
REVOKE DELETE ON orders FROM readwrite;
REVOKE ALL PRIVILEGES ON users FROM readonly;

Column-level Permissions

sql
-- Chỉ cho phép đọc một số cột (ẩn sensitive data)
GRANT SELECT (id, email, created_at) ON users TO readonly;
-- readonly KHÔNG thể SELECT password, phone, address

Row-level Security (PostgreSQL)

sql
-- Bật RLS cho bảng
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Policy: User chỉ thấy orders của chính mình
CREATE POLICY user_orders_policy ON orders
    FOR ALL
    TO readwrite
    USING (user_id = current_setting('app.current_user_id')::INT);

-- Khi query, set context
SET app.current_user_id = '42';
SELECT * FROM orders;  -- Chỉ trả về orders có user_id = 42

3️⃣ Principle of Least Privilege

Vấn đề: App User là postgres hoặc root

Nhiều developer dùng superuser cho app vì "tiện". Đây là sai lầm nghiêm trọng.

Nếu app bị SQL Injection và user là postgres:
→ Attacker có thể DROP DATABASE
→ Attacker có thể đọc pg_shadow (hashed passwords)
→ Attacker có thể tạo user mới
→ Attacker có thể COPY TO PROGRAM (chạy shell command!)

Cách setup đúng

sql
-- 1. Tạo schema riêng cho app
CREATE SCHEMA app_data;

-- 2. Tạo role cho từng môi trường/service
CREATE ROLE app_api;        -- API backend
CREATE ROLE app_worker;     -- Background jobs
CREATE ROLE app_readonly;   -- Reporting/Analytics

-- 3. Cấp quyền TỐI THIỂU
-- API chỉ cần CRUD trên một số bảng
GRANT USAGE ON SCHEMA app_data TO app_api;
GRANT SELECT, INSERT, UPDATE ON app_data.users TO app_api;
GRANT SELECT, INSERT, UPDATE ON app_data.orders TO app_api;
GRANT SELECT ON app_data.products TO app_api;  -- Chỉ đọc products

-- Worker cần quyền khác
GRANT USAGE ON SCHEMA app_data TO app_worker;
GRANT SELECT, UPDATE ON app_data.orders TO app_worker;  -- Xử lý orders
GRANT INSERT ON app_data.job_logs TO app_worker;

-- Readonly cho reporting
GRANT USAGE ON SCHEMA app_data TO app_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA app_data TO app_readonly;

-- 4. Tạo users từ roles
CREATE USER api_user WITH PASSWORD 'xxx' IN ROLE app_api;
CREATE USER worker_user WITH PASSWORD 'yyy' IN ROLE app_worker;
CREATE USER report_user WITH PASSWORD 'zzz' IN ROLE app_readonly;

-- 5. Deny nguy hiểm
REVOKE CREATE ON SCHEMA public FROM PUBLIC;  -- Không ai tạo table lung tung
REVOKE ALL ON pg_catalog.pg_shadow FROM app_api;  -- Không đọc password hashes

Connection String trong App

python
# Development (có thể dùng superuser, TUY NHIÊN vẫn nên tập thói quen)
DATABASE_URL = "postgresql://postgres:password@localhost/mydb"

# Production: PHẢI dùng restricted user
DATABASE_URL = "postgresql://api_user:secure_password@db.example.com/production_db"

Audit: Kiểm tra quyền hiện tại

sql
-- Xem quyền của một role
SELECT 
    table_schema,
    table_name,
    privilege_type
FROM information_schema.role_table_grants
WHERE grantee = 'app_api';

-- Xem members của một role
SELECT 
    r.rolname AS role,
    m.rolname AS member
FROM pg_auth_members am
JOIN pg_roles r ON r.oid = am.roleid
JOIN pg_roles m ON m.oid = am.member;

4️⃣ Security Checklist

Application Layer

Hành động
Dùng Prepared Statements cho MỌI query có user input
Whitelist table/column names nếu cần dynamic
Validate và sanitize input (length, type, format)
Log failed queries để detect injection attempts

Database Layer

Hành động
Tạo app-specific user, KHÔNG dùng superuser
Cấp quyền TỐI THIỂU cần thiết
Enable SSL/TLS cho connection
Đặt password policy mạnh
Regular audit quyền của các users
Backup và test restore thường xuyên

Network Layer

Hành động
Database không expose ra public internet
Chỉ cho phép connection từ app servers (whitelist IP)
Dùng VPC/Private Network

Tổng kết

ThreatMitigation
SQL InjectionPrepared Statements, Input Validation
Privilege EscalationLeast Privilege, Role-based Access
Data BreachColumn/Row-level Security, Encryption
Unauthorized AccessStrong Passwords, Network Isolation

💡 HPN Golden Rule

"Assume every user input is malicious.
Assume every app account will be compromised.
Design your database security like the attacker already has your app credentials."