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
countof 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 intoapplication/helpers/blacklist_helper.php, the SQL table schema, installation steps, usage examples, and troubleshooting tips.
Table of Contents
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 = 1or updates the existing row:- If tracked details changed (browser/OS/UA/reason) → update those fields, increment
countby 1 and updateadded. - If all same → increment
countby 1 and updateadded.
- If tracked details changed (browser/OS/UA/reason) → update those fields, increment
- Session-based counter:
session_visit_increment($limit, $reason)increments a per-session counter and callsmark_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.
Database schema (recommended)
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
- Backup your project and DB (always).
- Create the table in your database using above SQL (or adapt to your existing schema).
- Copy the helper file (full code below) into:
application/helpers/blacklist_helper.php - Load helper where you need it:
$this->load->helper('blacklist');Or autoload inapplication/config/autoload.php:$autoload['helpers'] = array('blacklist'); - Make sure sessions are working. CodeIgniter session library should be loaded (helper auto-loads it when needed).
- 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 thereasoncolumn.$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_keywith keys:ip,count,blacklisted,blacklist_id. - When session count goes above
$limitit callsmark_blacklist()and setsblacklisted = true. - Once blacklisted, subsequent calls to
session_visit_increment()still increase the session counter and callmark_blacklist()once per request (mark_blacklist increments DBcount), 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
- 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 resultsession_visit_increment(30, 'too_many_requests'); // optionallyblock request if IP found in DB if (blacklist_exists()) { // block request once blacklisted (example) show_error('Access denied', 403);
}
}} - 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_secondsoption 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_untiland 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_versionandos_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_matchwhere 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 toechoin views, - add throttling (only increment DB once every N seconds per session),
- add
blocked_untiland 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?