CodeIgniter 3 IP Blacklist
CodeIgniter 3 IP Blacklist

How to Build a Robust IP Blacklist + Session Visit Counter in CodeIgniter 3 IP Blacklist (step-by-step tutorial)23 min read

  Reading time 32 minutes

In this CodeIgniter 3 IP Blacklist tutorial you’ll learn how to add a production-ready blacklist helper to your CodeIgniter 3 app. The helper detects visitor IP, parses user-agent (device / OS / browser), keeps a DB count of hits per IP, auto-blacklists after configurable session visits, avoids duplicate DB writes during the same request, and provides utilities to check blacklist status. At the end you’ll get the full helper file you can drop into application/helpers/blacklist_helper.php, the SQL table schema, installation steps, usage examples, and troubleshooting tips.



Why you might need CodeIgniter 3 IP Blacklist (use-cases)

  • Prevent automated abuse: block IPs that repeatedly spam actions (cart add, login attempts, contact form).
  • Track suspicious visitors and see device/browser info for investigation.
  • Rate-limit per-session or per-IP without external services.
  • Keep everything inside your app / database, easy to integrate with admin tools.

What this “CodeIgniter 3 IP Blacklist” helper does (features)

  • Detects visitor IP (works behind common proxy headers).
  • Parses user agent to detect device (Mobile/Tablet/Desktop), OS (Windows/macOS/Android/iOS/etc.) and browser (Chrome, Edge, Brave, Safari, Vivaldi, Opera, Samsung Internet, Firefox, etc.).
  • Inserts new blacklist row with count = 1 or updates the existing row:
    • If tracked details changed (browser/OS/UA/reason) → update those fields, increment count by 1 and update added.
    • If all same → increment count by 1 and update added.
  • Session-based counter: session_visit_increment($limit, $reason) increments a per-session counter and calls mark_blacklist() when the limit is exceeded.
  • Avoids duplicate DB writes in the same PHP request (request-guard), so calling helper multiple times in controller + footer won’t double-increment.
  • blacklist_exists() to check whether IP is already in DB.
  • Central single-source-of-truth: mark_blacklist() controls all insert/update and increment logic.

Create a blacklist table with at least these columns. Adjust length/types if you need additional fields.

CREATE TABLE `blacklist` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `ip` VARCHAR(45) NOT NULL,
  `device` VARCHAR(100) DEFAULT NULL,
  `os` VARCHAR(100) DEFAULT NULL,
  `browser` VARCHAR(100) DEFAULT NULL,
  `user_agent` TEXT DEFAULT NULL,
  `reason` VARCHAR(191) DEFAULT NULL,
  `count` INT UNSIGNED NOT NULL DEFAULT 1,
  `added` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  INDEX (`ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

If your MySQL version does not allow DATETIME DEFAULT CURRENT_TIMESTAMP, set added manually in code (the helper already sets added).


Installation for “CodeIgniter 3 IP Blacklist” — step by step

  1. Backup your project and DB (always).
  2. Create the table in your database using above SQL (or adapt to your existing schema).
  3. Copy the helper file (full code below) into: application/helpers/blacklist_helper.php
  4. Load helper where you need it: $this->load->helper('blacklist'); Or autoload in application/config/autoload.php: $autoload['helpers'] = array('blacklist');
  5. Make sure sessions are working. CodeIgniter session library should be loaded (helper auto-loads it when needed).
  6. Use the helper on pages where you want to track or enforce limits.

Key functions & how to use them

mark_blacklist($reason = null, $extra = [], $use_recent_only = true)

Use this to insert/update the blacklist table. It returns the row ID (int) on success or false on failure.

$this->load->helper('blacklist');
$id = mark_blacklist('suspicious_login');
// $id is the inserted/updated blacklist row id, or false
  • $reason — optional string saved in the reason column.
  • $extra — optional associative array of extra columns to save (if present in DB).
  • mark_blacklist() is the single source for DB insert/update/increment behavior — call this whenever you want to record or increment.

session_visit_increment($limit = 20, $reason = null, $session_key = 'bl_visit')

Call this on page hits or actions (e.g., add-to-cart). It maintains a session-local counter and calls mark_blacklist() when the limit is exceeded. Returns an array with status and count.

// run on every request (e.g., in footer or base controller)
// Don't echo this (it returns an array)
session_visit_increment(20, 'too_many_cart_adds');

// Example inspect:
$res = session_visit_increment(20, 'too_many_cart_adds');
if ($res['status'] === 'blacklisted') {
  // take action
}

Behavior:

  • Session object stored under $session_key with keys: ip, count, blacklisted, blacklist_id.
  • When session count goes above $limit it calls mark_blacklist() and sets blacklisted = true.
  • Once blacklisted, subsequent calls to session_visit_increment() still increase the session counter and call mark_blacklist() once per request (mark_blacklist increments DB count), but the helper avoids duplicate writes within the same PHP request.

Important: In views do not use <?= session_visit_increment(...) ?> — that will try to echo an array and produce a PHP Notice. Use plain <?php session_visit_increment(...); ?>.


blacklist_exists($ip = null, $minutes = null)

Quick boolean check whether an IP exists in the blacklist table. If $minutes provided it checks whether row added/updated within that timespan.

if (blacklist_exists('14.192.53.49')) {
    // IP blacklisted
}
if (blacklist_exists(null, 60)) {
    // current IP has an entry in last 60 minutes
}

Full helper file for “CodeIgniter 3 IP Blacklist” (drop-in)

Below is the full helper file. Copy the whole content into application/helpers/blacklist_helper.php. This is the version that uses mark_blacklist() as the single source of truth and includes expanded modern browser & OS tokens.

Note: The file is self-contained and will call $CI->load->database() and load sessions if needed.

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

/**
 * Blacklist helper for CodeIgniter 3
 * - mark_blacklist(): insert/update blacklist row and manage count (single source of truth)
 * - session_visit_increment(): session-based visit counter + auto-blacklist (uses mark_blacklist to change DB)
 * - blacklist_exists(): check if IP exists
 * - lightweight UA parsing with expanded browser/OS tokens
 *
 * Usage:
 *   $this->load->helper('blacklist');
 *   $id_or_false = mark_blacklist('suspicious_login'); // returns int id or false
 */

/* -------------------------------------------------------------------------
 * Primary: mark_blacklist
 * Insert or update a row for the visitor IP and manage count.
 * Returns int row id on success, false on failure.
 * ------------------------------------------------------------------------- */
if (!function_exists('mark_blacklist')) {
    function mark_blacklist($reason = null, $extra = [], $use_recent_only = true)
    {
        $CI =& get_instance();
        if (!isset($CI->db)) $CI->load->database();

        $ip = _blacklist_get_user_ip();
        $ua = isset($_SERVER['HTTP_USER_AGENT']) ? trim($_SERVER['HTTP_USER_AGENT']) : '';
        $parsed = _blacklist_parse_user_agent($ua);

        $new = [
            'ip'         => $ip,
            'device'     => $parsed['device'] ?? 'Unknown',
            'os'         => $parsed['os'] ?? 'Unknown',
            'browser'    => $parsed['browser'] ?? 'Unknown',
            'user_agent' => $ua,
            'reason'     => $reason,
            'added'      => date('Y-m-d H:i:s'),
        ];

        if (is_array($extra) && !empty($extra)) {
            $new = array_merge($new, $extra);
        }

        // find most recent row for this IP
        try {
            $CI->db->where('ip', $ip);
            $CI->db->order_by('added', 'DESC');
            $CI->db->limit(1);
            $existing = $CI->db->get('blacklist')->row_array();
        } catch (Exception $e) {
            return false;
        }

        $CI->db->trans_start();

        if (empty($existing)) {
            $insert_data = $new;
            $insert_data['count'] = 1;
            $ok = $CI->db->insert('blacklist', $insert_data);
            $insert_id = $ok ? (int)$CI->db->insert_id() : false;
            $CI->db->trans_complete();

            if ($ok) return $insert_id;
            _blacklist_log_db_error($CI);
            return false;
        } else {
            // fields to compare
            $fields_to_check = ['device', 'os', 'browser', 'user_agent', 'reason'];
            $changed = false;
            foreach ($fields_to_check as $f) {
                $existing_val = isset($existing[$f]) ? (string)$existing[$f] : '';
                $new_val = isset($new[$f]) ? (string)$new[$f] : '';
                if ($existing_val !== $new_val) {
                    $changed = true;
                    break;
                }
            }

            // compare extras
            if (!$changed && is_array($extra) && !empty($extra)) {
                foreach ($extra as $k => $v) {
                    $existing_val = isset($existing[$k]) ? (string)$existing[$k] : '';
                    $new_val = (string)$v;
                    if ($existing_val !== $new_val) {
                        $changed = true;
                        break;
                    }
                }
            }

            if ($changed) {
                $update_data = [];
                foreach ($fields_to_check as $f) {
                    $update_data[$f] = $new[$f];
                }
                if (is_array($extra) && !empty($extra)) {
                    foreach ($extra as $k => $v) $update_data[$k] = $v;
                }
                $update_data['added'] = $new['added'];

                $CI->db->set('count', 'count + 1', false);
                $CI->db->where('id', (int)$existing['id']);
                $CI->db->update('blacklist', $update_data);
                $ok = $CI->db->affected_rows() >= 0;

                $CI->db->trans_complete();
                if ($ok) return (int)$existing['id'];
                _blacklist_log_db_error($CI);
                return false;
            } else {
                // identical -> increment count and update added only
                $CI->db->set('count', 'count + 1', false);
                $CI->db->set('added', $new['added']);
                $CI->db->where('id', (int)$existing['id']);
                $CI->db->update('blacklist');
                $ok = $CI->db->affected_rows() >= 0;

                $CI->db->trans_complete();
                if ($ok) return (int)$existing['id'];
                _blacklist_log_db_error($CI);
                return false;
            }
        }
    }
}

/* -------------------------------------------------------------------------
 * Utility: get visitor IP (best-effort)
 * ------------------------------------------------------------------------- */
if (!function_exists('_blacklist_get_user_ip')) {
    function _blacklist_get_user_ip()
    {
        $keys = [
            'HTTP_CLIENT_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_FORWARDED',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED',
            'REMOTE_ADDR'
        ];

        foreach ($keys as $key) {
            if (!empty($_SERVER[$key])) {
                $ipList = explode(',', $_SERVER[$key]);
                foreach ($ipList as $ip) {
                    $ip = trim($ip);
                    // prefer public IPs
                    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                        return $ip;
                    }
                }
                // fallback to first token
                return trim($ipList[0]);
            }
        }
        return '0.0.0.0';
    }
}

/* -------------------------------------------------------------------------
 * Utility: log DB error to file
 * ------------------------------------------------------------------------- */
if (!function_exists('_blacklist_log_db_error')) {
    function _blacklist_log_db_error($CI)
    {
        $dberr = $CI->db->error();
        $lastq = $CI->db->last_query();
        @file_put_contents(
            APPPATH . 'logs/blacklist_error.log',
            "[" . date('c') . "] DB Error: " . ($dberr['message'] ?? 'unknown') . " | Q: " . $lastq . PHP_EOL,
            FILE_APPEND
        );
    }
}

/* -------------------------------------------------------------------------
 * Lightweight UA parser with expanded modern browser/OS tokens.
 * Returns: ['device'=>'Mobile|Tablet|Desktop', 'os'=>'...', 'browser'=>'...']
 * ------------------------------------------------------------------------- */
if (!function_exists('_blacklist_parse_user_agent')) {
    function _blacklist_parse_user_agent($ua)
    {
        $ua = (string)$ua;
        $ua_l = strtolower($ua);
        $result = [
            'device'  => 'Desktop',
            'os'      => 'Unknown',
            'browser' => 'Unknown'
        ];

        if ($ua === '') return $result;

        // device
        if (preg_match('/mobile|iphone|ipod|android|blackberry|phone|windows phone/i', $ua)) {
            $result['device'] = 'Mobile';
        } elseif (preg_match('/ipad|tablet|kindle|playbook/i', $ua)) {
            $result['device'] = 'Tablet';
        } else {
            $result['device'] = 'Desktop';
        }

        // platform / OS mapping (includes modern tokens)
        $platforms = [
            'windows nt 11.0' => 'Windows 11',
            'windows nt 10.0' => 'Windows 10',
            'windows nt 6.3'  => 'Windows 8.1',
            'windows nt 6.2'  => 'Windows 8',
            'windows nt 6.1'  => 'Windows 7',
            'mac os x 13'     => 'macOS Ventura',
            'mac os x 12'     => 'macOS Monterey',
            'mac os x 11'     => 'macOS Big Sur',
            'mac os x'        => 'macOS',
            'android 14'      => 'Android 14',
            'android 13'      => 'Android 13',
            'android 12'      => 'Android 12',
            'android'         => 'Android',
            'ios'             => 'iOS',
            'iphone'          => 'iOS',
            'ipad'            => 'iOS',
            'linux'           => 'Linux',
            'ubuntu'          => 'Ubuntu',
            'debian'          => 'Debian',
            'freebsd'         => 'FreeBSD',
            'sunos'           => 'Sun Solaris',
            'symbian'         => 'Symbian OS'
        ];

        foreach ($platforms as $pattern => $name) {
            if (strpos($ua_l, $pattern) !== false) {
                $result['os'] = $name;
                break;
            }
        }

        // browsers (ordered, modern tokens included)
        $browsers = [
            'brave'          => 'Brave',
            'vivaldi'        => 'Vivaldi',
            'whale'          => 'Whale',
            'edg|edge'       => 'Edge',
            'opr|opera'      => 'Opera',
            'samsungbrowser' => 'Samsung Internet',
            'ucbrowser'      => 'UC Browser',
            'qqbrowser'      => 'QQ Browser',
            'yabrowser'      => 'Yandex Browser',
            'chrome'         => 'Chrome',
            'crios'          => 'Chrome (iOS)',
            'firefox'        => 'Firefox',
            'fxios'          => 'Firefox (iOS)',
            'safari'         => 'Safari',
            'msie|trident'   => 'Internet Explorer'
        ];

        foreach ($browsers as $pattern => $name) {
            if (@preg_match("/{$pattern}/i", $ua)) {
                // Safari precedence: skip Safari when Chrome/OPR/Edge/Brave present
                if ($name === 'Safari' && preg_match('/chrome|crios|opr|edg|brave/i', $ua)) {
                    continue;
                }
                $result['browser'] = $name;
                break;
            }
        }

        // prefer more specific engines when chrome matched
        if ($result['browser'] === 'Chrome') {
            if (preg_match('/edg|edge/i', $ua)) $result['browser'] = 'Edge';
            if (preg_match('/opr|opera/i', $ua)) $result['browser'] = 'Opera';
            if (preg_match('/brave/i', $ua)) $result['browser'] = 'Brave';
            if (preg_match('/vivaldi/i', $ua)) $result['browser'] = 'Vivaldi';
        }

        return $result;
    }
}

/* -------------------------------------------------------------------------
 * Check if IP exists in blacklist
 * ------------------------------------------------------------------------- */
if (!function_exists('blacklist_exists')) {
    function blacklist_exists($ip = null, $minutes = null)
    {
        $CI =& get_instance();
        if (!isset($CI->db)) $CI->load->database();

        if (empty($ip)) $ip = _blacklist_get_user_ip();

        if (!filter_var($ip, FILTER_VALIDATE_IP)) return false;

        try {
            $CI->db->from('blacklist');
            $CI->db->where('ip', $ip);
            if (is_int($minutes) && $minutes > 0) {
                $threshold = date('Y-m-d H:i:s', time() - ($minutes * 60));
                $CI->db->where('added >=', $threshold);
            }
            $CI->db->limit(1);
            $row = $CI->db->get()->row_array();
            return !empty($row);
        } catch (Exception $e) {
            @file_put_contents(APPPATH . 'logs/blacklist_error.log', "[".date('c')."] blacklist_exists DB error: ".$e->getMessage().PHP_EOL, FILE_APPEND);
            return false;
        }
    }
}

/* -------------------------------------------------------------------------
 * Session-based visit increment + auto-blacklist
 * - Request-guarded: only first call in a PHP request performs session/db changes.
 * - Uses mark_blacklist() for all DB inserts/updates (single source of truth)
 * - Stores session key (default 'bl_visit'): ['ip','count','blacklisted','blacklist_id']
 * ------------------------------------------------------------------------- */
if (!function_exists('session_visit_increment')) {
    function session_visit_increment($limit = 20, $reason = null, $session_key = 'bl_visit')
    {
        static $called = false;         // request-local guard
        static $last_result = null;     // cached result for subsequent calls in same request

        // If already called in this request, return cached result (do not change session/db again)
        if ($called) {
            return $last_result;
        }

        $called = true;

        $CI =& get_instance();
        if (!isset($CI->session)) $CI->load->library('session');
        if (!isset($CI->db)) $CI->load->database();

        $ip = _blacklist_get_user_ip();
        if (!filter_var($ip, FILTER_VALIDATE_IP)) $ip = '0.0.0.0';

        // get existing session record
        $rec = $CI->session->userdata($session_key);
        if (!is_array($rec)) {
            $rec = [
                'ip' => $ip,
                'count' => 0,
                'blacklisted' => false,
                'blacklist_id' => null
            ];
        }

        // if IP changed, reset
        if (!isset($rec['ip']) || $rec['ip'] !== $ip) {
            $rec = [
                'ip' => $ip,
                'count' => 0,
                'blacklisted' => false,
                'blacklist_id' => null
            ];
        }

        // If already blacklisted in session -> increment session once and call mark_blacklist() to increment DB
        if (!empty($rec['blacklisted'])) {
            $rec['count'] = (int)$rec['count'] + 1;
            $CI->session->set_userdata($session_key, $rec);

            // use mark_blacklist to increment DB (it will update existing row and increment count)
            $updated_id = mark_blacklist($reason);

            $last_result = [
                'status' => 'already_blacklisted',
                'count' => (int)$rec['count'],
                'insert_id' => $updated_id === false ? null : (int)$updated_id
            ];
            return $last_result;
        }

        // not blacklisted yet: increment session count once
        $rec['count'] = (int)$rec['count'] + 1;
        $CI->session->set_userdata($session_key, $rec);

        // if exceeded limit -> mark blacklist (mark_blacklist will insert/update and increment count)
        if ($rec['count'] > (int)$limit) {
            $use_reason = $reason ?? 'visit_limit_exceeded_' . (int)$limit;

            $insert_result = mark_blacklist($use_reason);

            // mark session blacklisted and store id if available
            $rec['blacklisted'] = true;
            if ($insert_result !== false && is_int($insert_result)) {
                $rec['blacklist_id'] = (int)$insert_result;
            }
            $CI->session->set_userdata($session_key, $rec);

            $last_result = [
                'status' => 'blacklisted',
                'count' => (int)$rec['count'],
                'insert_id' => $insert_result === false ? null : (int)$insert_result
            ];
            return $last_result;
        }

        // normal
        $last_result = [
            'status' => 'ok',
            'count' => (int)$rec['count'],
            'insert_id' => null
        ];
        return $last_result;
    }
}

Example: Practical integration of CodeIgniter 3 IP Blacklist

  1. Call in base controller (so it’s executed for every request):
    class MY_Controller extends CI_Controller {
    public function __construct() {
    parent::__construct();
    $this->load->helper('blacklist'); // increment per-session visit;
    do not echo the result session_visit_increment(30, 'too_many_requests'); // optionally block request if IP found in DB if (blacklist_exists()) { // block request once blacklisted (example) show_error('Access denied', 403);
    }
    }
    }
  2. Call only on sensitive actions (e.g., cart add)
    public function add_to_cart() {
    $this->load->helper('blacklist');
    $res = session_visit_increment(10, 'too_many_cart_adds');
    if ($res['status'] === 'blacklisted') {
    echo 'You are temporarily blocked'; return; } // continue with adding to cart
    }

Troubleshooting & tips for : CodeIgniter 3 IP Blacklist

  • Double increments: If you see +2 increments in DB for a single user action, likely the function is being called twice across separate HTTP requests (page + AJAX) or you are echoing the function (use <?php session_visit_increment(...) ?> not <?= ... ?>). The helper includes a request-guard so multiple calls in the same PHP request won’t cause multiple DB writes.
  • Array-to-string notice: Do not use <?= session_visit_increment(...) ?> because that returns an array. Either call it without echoing, or use a wrapper that returns a scalar.
  • High traffic: If your site has heavy traffic, consider throttling DB writes (store counters in Redis and periodically flush to DB). I can add a throttle_seconds option to minimize writes.
  • Privacy: Storing IPs can have legal implications in some jurisdictions. Ensure this aligns with your privacy policy and data retention rules.
  • Session persistence: Make sure CodeIgniter sessions are configured correctly (db or file) and session writes are working. Session-based counting depends on working session storage.

Security considerations for : CodeIgniter 3 IP Blacklist

  • Don’t trust user-supplied headers for critical security decisions. The helper uses common proxy headers to detect IP, but proxies can be spoofed. If you trust a reverse-proxy (e.g., Cloudflare), configure it properly and prefer REMOTE_ADDR.
  • Use prepared statements (CI DB class does that for you).
  • Be cautious about blocking legitimate users behind shared IPs (corporate NAT).
  • Log blacklisting events for admin review; avoid permanent bans unless manually reviewed.

Next steps / optional features you might want in “CodeIgniter 3 IP Blacklist”

  • Add blocked_until and automatic expiry for blacklist entries.
  • Use fail_count_window (e.g., count only within last X minutes) to implement sliding-window rate limit.
  • Add admin UI to view / reset blacklist entries and counts.
  • Add version extraction for browser & OS to store browser_version and os_version.
  • Use Redis for high-performance counters and batch writes to DB.

Final notes for “CodeIgniter 3 IP Blacklist”

  • The helper I provided is intentionally lightweight and uses safe checks for regex errors (uses @preg_match where necessary).
  • It keeps mark_blacklist() the single place that controls DB behavior — easier to maintain and less error-prone.
  • If you want, I can also:
    • provide a short WordPress-ready HTML version of this post (so you can paste into the WP editor),
    • produce a session_visit_increment_echo() wrapper that’s safe to echo in views,
    • add throttling (only increment DB once every N seconds per session),
    • add blocked_until and auto-unblock features.

Would you like the HTML version for WordPress (ready to paste into the WP block editor), or any of the optional features implemented now?

545
1
Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *