What is SQL Injection and Why Is It Dangerous?

SQL injection (SQLi) is a code injection technique that exploits vulnerabilities in an application's software by inserting malicious SQL statements into input fields or URL parameters. When user-supplied data is concatenated directly into a database query without proper sanitization, attackers can manipulate the query logic to bypass authentication, extract sensitive data, modify or delete records, and in severe cases take full control of the database server.

Consider a basic login form where a user enters a username and password. A naive query might look like this: SELECT * FROM users WHERE username = '$username' AND password = '$password'. If an attacker inputs ' OR '1'='1 for the username, the query becomes SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '', which bypasses authentication entirely and grants access to the first user in the database—often an administrator account.

The consequences of SQL injection are severe. According to the OWASP Top 10, injection attacks (including SQLi) consistently rank among the most critical web application security risks. In real-world incidents, SQL injection has led to breaches exposing millions of user records and costing organizations billions of dollars in damages. For CodeIgniter 4 applications that process dynamic data from forms, APIs, or user interactions, ignoring this vulnerability can be catastrophic.

There are three primary types of SQL injection:

  • Classic (In-Band) SQL Injection: The attacker directly manipulates input fields and receives results in the application's response. This is the most common and easiest to exploit.
  • Blind SQL Injection: The application does not display query results, but the attacker infers data by observing differences in behavior (true/false responses or HTTP status codes).
  • Time-Based Blind SQL Injection: The attacker uses database delay functions (e.g., SLEEP(5)) to extract information based on response timing.

The fundamental principle of preventing SQLi is separating data from code. This means never concatenating user input directly into SQL strings. Modern frameworks like CodeIgniter 4 facilitate this through parameterized queries, automatic escaping in Query Builder, and input validation — all of which we will implement step by step in this tutorial.

If you are building authentication systems in CodeIgniter 4, also check out our CodeIgniter 4 Authentication Tutorial which covers login and registration with Shield. For understanding role-based access in CI4, see our guide on Role-Based Login Systems in CodeIgniter 4.



How to Secure Your CodeIgniter 4 Application from SQL Injection

Table Of Content

1 Prerequisites

Before starting this tutorial, make sure you have the following installed and configured on your development machine:

  • PHP 8.1 or higher with PDO and the MySQLi extension enabled.
  • Composer installed globally for dependency management.
  • MySQL 5.7+ or MariaDB 10.3+ as the database server.
  • A code editor such as VS Code, PhpStorm, or Sublime Text.
  • Basic understanding of CodeIgniter 4 MVC architecture and PHP OOP concepts.

If this is your first time working with CodeIgniter 4, we recommend reading our How to Build a Simple Blog in CodeIgniter 4 tutorial first to familiarize yourself with the framework structure.

2 Introduction

CodeIgniter 4, the latest major version of this lightweight yet powerful PHP framework, includes robust built-in security features designed to mitigate SQL injection and other common vulnerabilities. Its Query Builder class automatically escapes all values passed through its methods, treating user input as data rather than executable code. Additionally, CI4 supports prepared statements with query bindings for cases where raw SQL is necessary, ensuring parameters are safely bound to the query.

In this comprehensive tutorial, we will build a complete user management system — including registration, login, search, and profile views — that demonstrates every major SQL injection prevention technique available in CodeIgniter 4. Each code file includes inline comments explaining the security rationale behind every decision.

Here is a summary of the security layers we will implement:

  • Query Builder for all standard CRUD operations (auto-escapes inputs).
  • Prepared statements with positional bindings for custom SQL queries.
  • Server-side validation rules enforced in the Model layer.
  • Password hashing with password_hash() and password_verify().
  • CSRF protection enabled globally via filters.
  • Output escaping with esc() in all views to prevent XSS.

This article is written for intermediate PHP developers familiar with CodeIgniter, but beginners can follow along with the provided code snippets. For performance optimization tips when building CI4 applications, check out our article on Boosting CodeIgniter 4 Performance.

3 Create / Install a Codeigniter 4 Project

We will start by creating a fresh CodeIgniter 4 project and configuring the database connection. This ensures we have a clean environment to implement all security best practices from scratch.

3.1 Install Codeigniter 4 Project

First, make sure your computer has a composer.
Use the following command to install new Codeigniter Project.

composer create-project codeigniter4/appstarter ci4-usersecure-app

Then, navigate to your project directory:

cd ci4-usersecure-app

3.2 Configure Environment and MySql Database

Rename the env file to .env and set the development mode in the .env file also configure mysql:

# CI_ENVIRONMENT = production
CI_ENVIRONMENT = development


database.default.hostname = localhost
database.default.database = secure_db
database.default.username = your_db_user
database.default.password = your_db_pass
database.default.DBDriver = MySQLi

Create the database secure_db

4 Create Migration and Model

Database Schema (SQL)

Execute this in your MySQL database to set up the users table:

    
        CREATE DATABASE IF NOT EXISTS secure_db;
        USE secure_db;

        CREATE TABLE IF NOT EXISTS `users` (
            `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
            `username` VARCHAR(50) NOT NULL UNIQUE,
            `email` VARCHAR(100) NOT NULL UNIQUE,
            `password` VARCHAR(255) NOT NULL,
            `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (`id`)
        );

        -- Insert a test admin user (password: admin123 hashed)
        INSERT INTO `users` (`username`, `email`, `password`) VALUES 
        ('admin', 'admin@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'); -- password_verify('admin123', this)

Note that we store the password as a bcrypt hash, never in plain text. The UNIQUE constraints on username and email add a database-level safeguard against duplicate entries.

Now create the Model for the users table:


php spark make:model UserModel

Edit app/Models/UserModel.php to configure the model. Pay close attention to the security comments in each method — they explain why each approach prevents SQL injection:


<?php

namespace App\Models;

use CodeIgniter\Model;

class UserModel extends Model
{
    protected $table = 'users';
    protected $primaryKey = 'id';
    protected $useAutoIncrement = true;
    protected $returnType = 'array';
    protected $useSoftDeletes = false;
    protected $protectFields = true;
    protected $allowedFields = ['username', 'email', 'password'];

    // Timestamps
    protected $useTimestamps = true;
    protected $createdField = 'created_at';
    protected $updatedField = '';
    protected $deletedField = '';

    // Validation rules applied automatically on insert/update
    protected $validationRules = [
        'username' => 'required|min_length[3]|max_length[50]|alpha_numeric_punct|is_unique[users.username]',
        'email'    => 'required|valid_email|max_length[100]|is_unique[users.email]',
        'password' => 'required|min_length[8]',
    ];
    protected $validationMessages = [
        'username' => [
            'is_unique' => 'Username already taken.',
        ],
        'email' => [
            'is_unique' => 'Email already registered.',
        ],
    ];

    /**
     * Secure login using Query Builder.
     * Query Builder auto-escapes the $username value via PDO bindings,
     * so injecting ' OR '1'='1 is treated as a literal string.
     */
    public function login(string $username, string $password): ?array
    {
        $builder = $this->db->table($this->table);
        $builder->where('username', $username); // Auto-escaped by Query Builder
        $user = $builder->get()->getRowArray();

        if ($user && password_verify($password, $user['password'])) {
            return $user;
        }
        return null;
    }

    /**
     * Secure search using prepared statements + escapeLikeString().
     * escapeLikeString() neutralizes special LIKE chars (%, _)
     * that could be used for pattern injection attacks.
     */
    public function searchUsers(string $searchTerm): array
    {
        // Escape special LIKE characters before binding
        $escapedTerm = $this->db->escapeLikeString($searchTerm);

        $builder = $this->db->table($this->table);
        $builder->select('id, username, email');
        $builder->groupStart()
            ->like('username', $escapedTerm)
            ->orLike('email', $escapedTerm)
        ->groupEnd();
        $builder->orderBy('username', 'ASC');
        $builder->limit(10);

        return $builder->get()->getResultArray();
    }

    /**
     * Secure insert with password hashing.
     * Uses CI4 Model::insert() which internally uses Query Builder.
     */
    public function createSecureUser(array $data): bool
    {
        if (!isset($data['password'])) {
            return false;
        }
        $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
        return (bool) $this->insert($data);
    }
}

Key security points in this model:

  • The login() method uses Query Builder's where() which automatically binds parameters via PDO, preventing injection.
  • The searchUsers() method uses escapeLikeString() to sanitize the search term before passing it to like(). Without this, an attacker could use % or _ wildcards to perform pattern injection.
  • The $allowedFields array acts as a whitelist — only username, email, and password can be mass-assigned, preventing attackers from injecting extra fields like is_admin.
  • Validation rules include alpha_numeric_punct for the username, which restricts input to safe characters.

5 Add Custom Rules

CodeIgniter 4 allows you to define reusable validation rule groups in the app/Config/Validation.php config file. These rule groups can be referenced by name in controllers, keeping validation logic centralized and consistent.

Edit app/Config/Validation.php and add a rule group for user registration:


<?php

namespace Config;

use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Validation\StrictRules\CreditCardRules;
use CodeIgniter\Validation\StrictRules\FileRules;
use CodeIgniter\Validation\StrictRules\FormatRules;
use CodeIgniter\Validation\StrictRules\Rules;

class Validation extends BaseConfig
{
    // Rule processors
    public array $ruleSets = [
        Rules::class,
        FormatRules::class,
        FileRules::class,
        CreditCardRules::class,
    ];

    // Custom rule group for user registration
    public array $userRegistration = [
        'username' => 'required|min_length[3]|max_length[50]|alpha_numeric_punct|is_unique[users.username]',
        'email'    => 'required|valid_email|max_length[100]|is_unique[users.email]',
        'password' => 'required|min_length[8]',
    ];

    // Custom error messages for registration
    public array $userRegistration_errors = [
        'username' => [
            'is_unique' => 'This username is already taken. Please choose another.',
            'alpha_numeric_punct' => 'Username may only contain letters, numbers, and basic punctuation.',
        ],
        'email' => [
            'is_unique' => 'This email address is already registered.',
        ],
        'password' => [
            'min_length' => 'Password must be at least 8 characters long.',
        ],
    ];
}

Why this matters for security: By defining $userRegistration as a named rule group, the controller can call $validation->run($data, 'userRegistration') to validate all input before it ever reaches a database query. The alpha_numeric_punct rule on username ensures only safe characters are accepted, which adds a defense-in-depth layer on top of Query Builder's automatic escaping. The is_unique rule uses Query Builder internally, so the uniqueness check itself is also protected against injection.

6 Enable CSRF for Extra Security

CSRF (Cross-Site Request Forgery) protection is essential alongside SQL injection prevention. While SQLi targets your database queries, CSRF targets your form submissions. CodeIgniter 4 provides built-in CSRF filtering.

Edit app/Config/Filters.php to enable CSRF globally:


<?php

namespace Config;

use CodeIgniter\Config\Filters as BaseFilters;

class Filters extends BaseFilters
{
    public array $aliases = [
        'csrf'         => \CodeIgniter\Filters\CSRF::class,
        'toolbar'      => \CodeIgniter\Filters\DebugToolbar::class,
        'honeypot'     => \CodeIgniter\Filters\Honeypot::class,
        'invalidchars' => \CodeIgniter\Filters\InvalidChars::class,
    ];

    public array $globals = [
        'before' => [
            'csrf' => ['except' => ['api/*']], // Enable CSRF globally except API routes
            'honeypot',
            'invalidchars',
        ],
        'after' => [
            'toolbar',
        ],
    ];

    // Additional filter configurations remain at default
}

How CSRF protection complements SQLi prevention: Even if all your database queries are perfectly parameterized, an attacker could still trick an authenticated user into submitting a forged form (e.g., changing their email). The CSRF token ensures that every form submission originates from your application. In the view files, we use form_open() which automatically injects the CSRF token field.

7 Create UserController

Create a new controller named UserController. This controller handles registration, login, search, and profile display — all using secure methods.


php spark make:controller UserController

Edit app/Controllers/UserController.php:


<?php

namespace App\Controllers;

use App\Models\UserModel;
use CodeIgniter\Controller;

class UserController extends Controller
{
    protected UserModel $userModel;

    public function __construct()
    {
        $this->userModel = new UserModel();
    }

    /**
     * Secure Registration
     * Uses named validation rule group defined in Config\Validation
     */
    public function register()
    {
        if ($this->request->getMethod() === 'post') {
            $validation = \Config\Services::validation();

            $data = [
                'username' => $this->request->getPost('username'),
                'email'    => $this->request->getPost('email'),
                'password' => $this->request->getPost('password'),
            ];

            // Validate using the named rule group from Config\Validation
            if ($validation->run($data, 'userRegistration')) {
                if ($this->userModel->createSecureUser($data)) {
                    return redirect()->to('/login')->with('success', 'Registration successful!');
                } else {
                    return redirect()->back()->with('error', 'Registration failed. Please try again.');
                }
            } else {
                return view('auth/register', ['errors' => $validation->getErrors()]);
            }
        }

        return view('auth/register');
    }

    /**
     * Secure Login
     * NEVER concatenate user input into SQL strings.
     * The commented code below shows a VULNERABLE example — do NOT use it.
     */
    public function login()
    {
        if ($this->request->getMethod() === 'post') {
            $username = $this->request->getPost('username');
            $password = $this->request->getPost('password');

            // VULNERABLE — DO NOT USE:
            // $db = \Config\Database::connect();
            // $sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
            // $result = $db->query($sql); // Direct concatenation = SQLi risk!

            // SECURE — Uses Query Builder with auto-escaping:
            $user = $this->userModel->login($username, $password);

            if ($user) {
                session()->set('user_id', $user['id']);
                return redirect()->to('/profile/' . $user['id'])->with('success', 'Login successful!');
            } else {
                return redirect()->back()->with('error', 'Invalid credentials.');
            }
        }

        return view('auth/login');
    }

    /**
     * Secure User Search
     * Uses Model method with escapeLikeString + Query Builder
     */
    public function search()
    {
        $searchTerm = $this->request->getGet('q');
        $users = [];

        if ($searchTerm) {
            $users = $this->userModel->searchUsers($searchTerm);
        }

        return view('users/search', ['users' => $users, 'searchTerm' => $searchTerm]);
    }

    /**
     * Profile View — Secure Fetch by ID
     * Uses Query Builder via a fresh DB connection.
     * The where() clause auto-escapes $id.
     */
    public function profile($id)
    {
        $db = \Config\Database::connect();
        $builder = $db->table('users');
        $builder->select('id, username, email, created_at');
        $builder->where('id', (int) $id); // Cast to int + auto-escaped
        $user = $builder->get()->getRowArray();

        if (!$user) {
            throw new \CodeIgniter\Exceptions\PageNotFoundException('User not found.');
        }

        return view('users/profile', ['user' => $user]);
    }

    /**
     * Logout — Destroy session safely
     */
    public function logout()
    {
        session()->destroy();
        return redirect()->to('/login');
    }
}

Critical fixes in this controller:

  • The profile() method now uses \Config\Database::connect() instead of the non-existent $this->db property. Controllers in CodeIgniter 4 do not have a $db property by default — you must either inject the database service or connect manually.
  • The $id parameter is cast to (int) as an extra safety measure, ensuring only numeric values reach the query.
  • The login redirect now sends the user to their specific profile page instead of a generic /profile route.

8 Create Views

Create app/Views/auth/register.php
    
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Register - Secure CI4</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
        </head>
        <body class="container mt-5">
            <div class="row justify-content-center">
                <div class="col-md-6">
                    <h2>Register</h2>
                    <?php if (session()->getFlashdata('success')): ?>
                        <div class="alert alert-success"><?= session()->getFlashdata('success') ?></div>
                    <?php endif; ?>
                    <?php if (isset($errors)): ?>
                        <div class="alert alert-danger">
                            <ul class="mb-0">
                                <?php foreach ($errors as $error): ?>
                                    <li><?= esc($error) ?></li>
                                <?php endforeach; ?>
                            </ul>
                        </div>
                    <?php endif; ?>
                    <?= form_open('/register') ?>
                        <div class="mb-3">
                            <label for="username" class="form-label">Username</label>
                            <input type="text" class="form-control" id="username" name="username" required>
                        </div>
                        <div class="mb-3">
                            <label for="email" class="form-label">Email</label>
                            <input type="email" class="form-control" id="email" name="email" required>
                        </div>
                        <div class="mb-3">
                            <label for="password" class="form-label">Password</label>
                            <input type="password" class="form-control" id="password" name="password" required minlength="8">
                        </div>
                        <button type="submit" class="btn btn-primary">Register</button>
                        <a href="/login" class="btn btn-link">Already have an account?</a>
                    <?= form_close() ?>
                </div>
            </div>
            <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
        </body>
        </html>
    
Create app/Views/auth/login.php
    
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Login - Secure CI4</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
        </head>
        <body class="container mt-5">
            <div class="row justify-content-center">
                <div class="col-md-6">
                    <h2>Login</h2>
                    <?php if (session()->getFlashdata('error')): ?>
                        <div class="alert alert-danger"><?= session()->getFlashdata('error') ?></div>
                    <?php endif; ?>
                    <?= form_open('/login') ?>
                        <div class="mb-3">
                            <label for="username" class="form-label">Username</label>
                            <input type="text" class="form-control" id="username" name="username" required>
                        </div>
                        <div class="mb-3">
                            <label for="password" class="form-label">Password</label>
                            <input type="password" class="form-control" id="password" name="password" required>
                        </div>
                        <button type="submit" class="btn btn-primary">Login</button>
                        <a href="/register" class="btn btn-link">Need an account?</a>
                    <?= form_close() ?>
                </div>
            </div>
            <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
        </body>
        </html>
    
Create app/Views/users/search.php
    
       <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Search Users - Secure CI4</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
        </head>
        <body class="container mt-5">
            <h2>Search Users</h2>
            <?= form_open('/search') ?>
                <div class="input-group mb-3">
                    <input type="text" class="form-control" name="q" placeholder="Search by username or email" value="<?= esc($searchTerm ?? '') ?>">
                    <button class="btn btn-outline-secondary" type="submit">Search</button>
                </div>
            <?= form_close() ?>

            <?php if (!empty($users)): ?>
                <table class="table">
                    <thead>
                        <tr><th>ID</th><th>Username</th><th>Email</th><th>Actions</th></tr>
                    </thead>
                    <tbody>
                        <?php foreach ($users as $user): ?>
                            <tr>
                                <td><?= esc($user['id']) ?></td>
                                <td><?= esc($user['username']) ?></td>
                                <td><?= esc($user['email']) ?></td>
                                <td><a href="/profile/<?= $user['id'] ?>" class="btn btn-sm btn-primary">View</a></td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            <?php elseif ($searchTerm): ?>
                <p>No users found for "<?= esc($searchTerm) ?>".</p>
            <?php endif; ?>

            <a href="/login" class="btn btn-secondary">Back to Login</a>
            <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
        </body>
        </html>
    
Create app/Views/users/profile.php
    
       <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Profile - Secure CI4</title>
            <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
        </head>
        <body class="container mt-5">
            <h2>User Profile</h2>
            <div class="card">
                <div class="card-body">
                    <p><strong>ID:</strong> <?= esc($user['id']) ?></p>
                    <p><strong>Username:</strong> <?= esc($user['username']) ?></p>
                    <p><strong>Email:</strong> <?= esc($user['email']) ?></p>
                    <p><strong>Created:</strong> <?= esc($user['created_at']) ?></p>
                </div>
            </div>
            <a href="/search" class="btn btn-secondary mt-3">Search Users</a>
            <a href="/logout" class="btn btn-danger mt-3">Logout</a>
            <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
        </body>
        </html>
    

9 Define a Route

Open app/Config/Routes.php and define the application routes. Notice that the profile route uses (:num) — this is a route constraint that only accepts numeric values, adding a routing-level defense against injection through URL parameters.


use CodeIgniter\Router\RouteCollection;

/**
 * @var RouteCollection $routes
 */
$routes->get('/', 'Home::index');

// Authentication routes
$routes->get('register', 'UserController::register');
$routes->post('register', 'UserController::register');
$routes->get('login', 'UserController::login');
$routes->post('login', 'UserController::login');
$routes->get('logout', 'UserController::logout');

// User routes ((:num) only allows numeric IDs)
$routes->get('search', 'UserController::search');
$routes->get('profile/(:num)', 'UserController::profile/$1');

The (:num) placeholder ensures that only URLs like /profile/1 or /profile/42 are matched. A request to /profile/1' OR '1'='1 would return a 404 error before the controller is ever invoked.

10 Folder Structure

11 Run and Test

Start the built-in development server to test all security features:


php spark serve

Visit http://localhost:8080/ in your browser and test each feature:

  1. Test Registration: Navigate to /register. Enter a unique username and email with a password of at least 8 characters. The server-side validation will reject invalid input before any database query is executed.
  2. Test Login: Go to /login and sign in with admin / admin123 (the test user we inserted in the migration).
  3. Test Search: Visit /search?q=admin — this uses Query Builder with escapeLikeString() to safely handle the LIKE pattern.
  4. Test Profile: Visit /profile/1 — the ID is cast to integer and passed through Query Builder's auto-escaping.
  5. SQL Injection Test: Try entering ' OR '1'='1 in the login username field. The login will fail because Query Builder treats the entire string as a literal value, not as SQL code.
  6. LIKE Injection Test: Try searching for % in the search box. Thanks to escapeLikeString(), the wildcard is escaped and treated as a literal percent character.

How Each Security Layer Works:

  • Query Builder: Used in login(), profile(), and insert(). Automatically escapes all values via PDO parameter bindings, ensuring user input is never interpreted as SQL code.
  • escapeLikeString(): Used in searchUsers() to neutralize LIKE-specific wildcards (% and _) before they reach the query. This is a defense layer that Query Builder's standard escaping does not cover.
  • Server-Side Validation: Enforced in both the Model (via $validationRules) and the Validation config. Input that fails validation is rejected before any database interaction occurs.
  • Password Hashing: Passwords are hashed with password_hash() using bcrypt. Authentication uses password_verify() to compare hashes, so raw passwords never appear in queries.
  • CSRF Protection: Enabled globally via filters. The form_open() helper in views automatically generates and includes the CSRF token, preventing cross-site form submission attacks.
  • Output Escaping: All dynamic data in views is wrapped in esc() to prevent Cross-Site Scripting (XSS) attacks — a complementary vulnerability to SQLi.
  • Result Limiting: The search query uses LIMIT 10 to prevent potential denial-of-service through queries that return massive result sets.

For more on building secure APIs with CodeIgniter 4, see our CodeIgniter 4 REST API CRUD Tutorial.

12 Conclusion

Securing your CodeIgniter 4 application from SQL injection is not a single action but a layered defense strategy. In this tutorial, we implemented multiple security measures working together: Query Builder with automatic escaping for standard operations, escapeLikeString() for safe LIKE searches, named validation rule groups for input sanitization, bcrypt password hashing, CSRF token protection, and output escaping in views.

The key takeaway is to never concatenate user input directly into SQL strings. Always use Query Builder methods or prepared statements with bindings. Even when CodeIgniter 4 handles escaping automatically, adding validation and type casting creates defense-in-depth — if one layer fails, others still protect your application.

As your application grows, consider these additional security practices:

  • Use whitelist-based ordering for dynamic ORDER BY clauses — never pass user input directly to orderBy().
  • Implement rate limiting on login and search endpoints to mitigate brute-force and enumeration attacks.
  • Run regular security audits using tools like SQLMap for automated SQL injection testing.
  • Keep CodeIgniter 4 and all dependencies updated via Composer to patch known vulnerabilities.
  • Use Content Security Policy (CSP) headers and HTTPS in production environments.

By following these practices, you can build CodeIgniter 4 applications that are resilient against SQL injection and other common web vulnerabilities. For related tutorials, explore our CI4 Authentication with Shield guide and our comparison of CodeIgniter 4 vs Laravel 12 for Small Projects.

Continue strengthening your CodeIgniter 4 skills with these related tutorials from our site:

Revathi M - PHP and CodeIgniter Developer

Written by Revathi M

PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel

Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.

Frequently Asked Questions

escapeLikeString() is a method that escapes special characters used in SQL LIKE patterns — specifically the percent sign (%) and underscore (_). Without escaping, an attacker could search for % to match all records or use _ as a single-character wildcard. Always use $db->escapeLikeString() before passing user input to like() or orLike() methods.

Never pass user input directly to the orderBy() method. Instead, use a whitelist approach: define an array of allowed column names and sort directions, then check the user input against this whitelist before applying it. For example: $allowed = ["username", "email", "created_at"]; if (in_array($sortBy, $allowed)) { $builder->orderBy($sortBy, $direction); }

SQL injection is a security vulnerability where attackers insert malicious SQL code through user inputs (forms, URLs, APIs) to manipulate database queries. In CodeIgniter 4, this is prevented by using Query Builder (which auto-escapes values), prepared statements with query bindings, escapeLikeString() for LIKE searches, and server-side validation rules.

CodeIgniter 4's Query Builder automatically escapes all values passed to methods like where(), like(), and insert(). It uses PDO parameter bindings internally, which means user input is treated as data — never as executable SQL code. This separation of data and code is the core principle of SQL injection prevention.

Raw SQL queries should be avoided whenever possible. If you must use them, always use prepared statements with positional (?) or named (:param) bindings. Never concatenate variables directly into SQL strings. CodeIgniter 4's $db->query($sql, $bindings) method supports safe prepared statements.

No, input validation alone is not sufficient. Validation ensures data meets format rules (e.g., valid email, minimum length), but it does not guarantee SQL-safe output. You must combine validation with parameterized queries or Query Builder escaping. Think of validation as the first line of defense and escaping as the last line before the database.

You can test for SQL injection using automated tools like SQLMap, which scans your application endpoints for injection vulnerabilities. For manual testing, try entering payloads like ' OR '1'='1, ' UNION SELECT NULL--, or '; DROP TABLE users;-- into form fields and URL parameters. Always test in a development environment, never in production.