SQL Injection Prevention Guide: Complete Tutorial

1️⃣ Introduction

SQL injection remains one of the most critical security vulnerabilities in web applications. This guide covers comprehensive strategies for preventing SQL injection attacks.

Key areas covered:

  • Prepared Statements
  • Input Validation
  • ORM Security
  • Database Hardening
  • Access Controls
  • Best Practices

2️⃣ Prepared Statements

🔹 JDBC Examples

// Bad Example - Don't do this
public User findUser(String username) {
    // Vulnerable to SQL injection
    String sql = "SELECT * FROM users WHERE username = '" + 
        username + "'";
    Statement stmt = connection.createStatement();
    ResultSet rs = stmt.executeQuery(sql);
    return mapResultToUser(rs);
}

// Good Example - Use PreparedStatement
public User findUserSafely(String username) {
    String sql = "SELECT * FROM users WHERE username = ?";
    
    try (PreparedStatement stmt = 
            connection.prepareStatement(sql)) {
        stmt.setString(1, username);
        ResultSet rs = stmt.executeQuery();
        return mapResultToUser(rs);
    }
}

// Batch Operations Example
public void batchInsertUsers(List users) {
    String sql = "INSERT INTO users (username, email) " +
        "VALUES (?, ?)";
    
    try (PreparedStatement stmt = 
            connection.prepareStatement(sql)) {
        for (User user : users) {
            stmt.setString(1, user.getUsername());
            stmt.setString(2, user.getEmail());
            stmt.addBatch();
        }
        stmt.executeBatch();
    }
}

🔹 JPA/Hibernate Examples

@Repository
public class UserRepository {
    @PersistenceContext
    private EntityManager em;
    
    // Bad Example - Don't do this
    public List findByRoleBad(String role) {
        // Vulnerable to SQL injection
        String jpql = "SELECT u FROM User u WHERE u.role = '" + 
            role + "'";
        return em.createQuery(jpql, User.class)
            .getResultList();
    }
    
    // Good Example - Use Parameter Binding
    public List findByRole(String role) {
        String jpql = "SELECT u FROM User u WHERE u.role = :role";
        return em.createQuery(jpql, User.class)
            .setParameter("role", role)
            .getResultList();
    }
    
    // Using Criteria API (Type-safe)
    public List findByRoleCriteria(String role) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery query = cb.createQuery(User.class);
        Root root = query.from(User.class);
        
        query.where(cb.equal(root.get("role"), role));
        return em.createQuery(query).getResultList();
    }
}

3️⃣ Input Validation

🔹 Validation Strategies

public class SqlInputValidator {
    // Whitelist pattern for simple identifiers
    private static final Pattern IDENTIFIER_PATTERN = 
        Pattern.compile("^[a-zA-Z0-9_]{1,64}$");
    
    // Whitelist pattern for numbers
    private static final Pattern NUMBER_PATTERN = 
        Pattern.compile("^\\d{1,10}$");
    
    public boolean isValidIdentifier(String input) {
        return input != null && 
            IDENTIFIER_PATTERN.matcher(input).matches();
    }
    
    public boolean isValidNumber(String input) {
        return input != null && 
            NUMBER_PATTERN.matcher(input).matches();
    }
    
    // Validate SQL operators
    public boolean isValidOperator(String operator) {
        Set validOperators = 
            Set.of("=", "<", ">", "<=", ">=", "<>");
        return validOperators.contains(operator);
    }
    
    // Validate order by clause
    public boolean isValidOrderBy(String column) {
        Set validColumns = 
            Set.of("id", "username", "email", "created_at");
        return validColumns.contains(column.toLowerCase());
    }
}

🔹 Parameterized Queries

@Service
public class UserService {
    private final JdbcTemplate jdbcTemplate;
    private final SqlInputValidator validator;
    
    // Using JdbcTemplate (Spring)
    public List findUsers(String role, int limit) {
        if (!validator.isValidIdentifier(role)) {
            throw new IllegalArgumentException(
                "Invalid role parameter");
        }
        
        String sql = "SELECT * FROM users WHERE role = ? " +
            "LIMIT ?";
        return jdbcTemplate.query(sql,
            new Object[]{role, limit},
            new UserRowMapper());
    }
    
    // Dynamic query building (safe way)
    public List searchUsers(UserSearchCriteria criteria) {
        StringBuilder sql = new StringBuilder(
            "SELECT * FROM users WHERE 1=1");
        List params = new ArrayList<>();
        
        if (criteria.getUsername() != null) {
            sql.append(" AND username = ?");
            params.add(criteria.getUsername());
        }
        
        if (criteria.getRole() != null) {
            sql.append(" AND role = ?");
            params.add(criteria.getRole());
        }
        
        return jdbcTemplate.query(
            sql.toString(),
            params.toArray(),
            new UserRowMapper());
    }
}



4️⃣ Database Hardening

🔹 Principle of Least Privilege

-- PostgreSQL Example
-- Create application role
CREATE ROLE app_user WITH LOGIN PASSWORD 'strong_password';

-- Grant minimal privileges
GRANT SELECT, INSERT, UPDATE ON users TO app_user;
GRANT SELECT ON audit_log TO app_user;

-- Restrict sensitive columns
REVOKE SELECT ON users (password_hash) FROM app_user;

-- Create stored procedures
CREATE PROCEDURE create_user(
    p_username VARCHAR,
    p_email VARCHAR
) LANGUAGE plpgsql
AS $$
BEGIN
    INSERT INTO users (username, email)
    VALUES (p_username, p_email);
END;
$$;

-- Grant execute permission
GRANT EXECUTE ON PROCEDURE create_user TO app_user;

-- Row Level Security
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY user_access_policy ON users
    USING (created_by = current_user);
    
-- Connection pooling configuration

🔹 Audit Logging

@Aspect
@Component
public class DatabaseAuditAspect {
    private final JdbcTemplate jdbcTemplate;
    
    @Around("@annotation(DatabaseOperation)")
    public Object auditDatabaseOperation(
            ProceedingJoinPoint joinPoint) throws Throwable {
        String operation = joinPoint.getSignature().getName();
        String user = SecurityContextHolder.getContext()
            .getAuthentication().getName();
        Timestamp timestamp = new Timestamp(
            System.currentTimeMillis());
        
        try {
            Object result = joinPoint.proceed();
            
            // Log successful operation
            jdbcTemplate.update(
                "INSERT INTO audit_log " +
                "(operation, user_id, timestamp, status) " +
                "VALUES (?, ?, ?, 'SUCCESS')",
                operation, user, timestamp);
                
            return result;
        } catch (Exception e) {
            // Log failed operation
            jdbcTemplate.update(
                "INSERT INTO audit_log " +
                "(operation, user_id, timestamp, status, error) " +
                "VALUES (?, ?, ?, 'FAILED', ?)",
                operation, user, timestamp, e.getMessage());
                
            throw e;
        }
    }
}

5️⃣ Q&A / Frequently Asked Questions

Common vectors: (1) User input fields. (2) URL parameters. (3) HTTP headers. (4) Cookie values. (5) Form fields. (6) JSON/XML data. (7) File uploads. (8) API parameters.

Prevention steps: (1) Use prepared statements. (2) Input validation. (3) Stored procedures. (4) ORM frameworks. (5) Escape special characters. (6) Parameterize queries. (7) Review and refactor. (8) Security testing.

Warning signs: (1) String concatenation in queries. (2) Direct user input in SQL. (3) Error messages with SQL syntax. (4) Unvalidated input. (5) Dynamic SQL generation. (6) No prepared statements. (7) Outdated frameworks. (8) Missing input sanitization.

7️⃣ Best Practices & Pro Tips 🚀

  • Use prepared statements
  • Input validation
  • Parameterized queries
  • ORM frameworks
  • Least privilege access
  • Regular auditing
  • Error handling
  • Security testing
  • Code reviews
  • Database monitoring
  • Documentation
  • Security training

Read Next 📖

Conclusion

SQL injection prevention requires a comprehensive approach combining prepared statements, input validation, and proper database security measures. By following the techniques and best practices outlined in this guide, you can significantly reduce the risk of SQL injection vulnerabilities in your applications.

Remember to regularly review and update your security measures as new attack vectors are discovered and best practices evolve.