Version 4.7.6
Release Date: November 1, 2026
4.7.6 release of CodeIgniter4
SECURITY
Cache:
FileHandler::deleteMatching()now matches the pattern against cache file names only. Previously, a pattern such as../*could delete files outside the cache directory. Patterns can no longer match files in subdirectories of the cache directory, so those files are no longer deleted. Special characters in the cache path, such as[, are no longer interpreted as part of the pattern. See the Security advisory GHSA-9hf2-g3w9-pfmp for more information. See File Cache deleteMatching() No Longer Matches Subdirectories for migration instructions.Model: Fixed a mass assignment vulnerability in
replace(). The method now discards fields not listed in$allowedFieldswhen field protection is enabled, while retaining a supplied primary key. Input emptied by filtering, or reduced to only its primary key, now throws aDataException. Previously, applications passing untrusted input toreplace()could write protected columns. See Upgrading from 4.7.5 to 4.7.6 for migration instructions. See the Security advisory GHSA-jv74-fx4h-m63x for more information.Security Helper: Fixed a path traversal vulnerability in
sanitize_filename(). Backslashes were only removed by a finalstripslashes()call after filtering. When$relativePathwastrue, input such as.\./became../on all platforms. In both modes, repeated backslashes could survive as Windows path separators, and\0could become a NUL byte. Backslashes are now removed during filtering, andstripslashes()is no longer called. Forward slashes are still preserved when$relativePathistrue. The same applies toSecurity::sanitizeFilename(). See Upgrading from 4.7.5 to 4.7.6 and the Security advisory GHSA-9j59-qcxw-q7qm for more information.Database: Fixed a SQL injection vulnerability in named query bindings. When a query combined named binds (
:name:) with a value that was already escaped and rendered into the SQL (for example a subquery passed towhere(),union(),fromSubquery()orselectSubquery(), a customwhere()string built with$db->escape(), theWHEREbinds ofdeleteBatch(), or a manually escaped fragment in$db->query()), a placeholder inside that value was replaced with the bind of the same name, which could break out of the string literal. Placeholders are now ignored inside quoted strings, quoted identifiers and SQL comments, and theWHEREbinds ofdeleteBatch()are substituted only after the batch data has been rendered into the query. Plainwhere(),like(),insert()andupdate()calls were not affected. See the Security advisory GHSA-25rf-99hr-7pmr and Named Binds Are No Longer Replaced Inside Quoted Values for more information.
BREAKING
Model:
replace()now applies field protection and rejects input emptied by filtering. See Model Field Protection for replace() for migration instructions.
Message Changes
Added the
CLI.inputEndedlanguage string.Added the
Format.invalidXMLValuelanguage string.
Changes
Cache: Added an optional
$pathparameter toFileVarExportHandler::__construct()so the cache files can be stored in a custom directory.HTTP: Added
ContentSecurityPolicy::addDefaultSrc()to allow appending sources to thedefault-srcdirective.
Deprecations
Database:
BaseBuilder::convertWhereBindsForBatch()is deprecated and no longer used. The binds of a batch query are now substituted byBaseBuilder::compileBatchBinds()after the batch data has been rendered.
Bugs Fixed
CLI: Fixed a bug where
CLI::promptByMultipleKeys()accepted a key missing from the options when it was below the last key, returning an empty selection, rejected a key above the last one when the options were not in key order, and offered0as the default when the first key was not0.CLI: Fixed a bug where a prompt asked again forever when its input was piped or redirected and ran out before a valid answer:
CLI::prompt()andCLI::promptByKey()whose default fails the validation rules, andCLI::promptByMultipleKeys()whose options have string keys. They now throw aCLIException. The newInputOutput::isAtEnd()tells that end of the input from an empty line. Pressing Ctrl+D at a terminal still asks again.Commands: Fixed a bug where generator commands did not sort the imports of a template with CRLF line endings.
Database: Fixed
reconnect()so a replaced socket does not keep the transaction that died with the old connection.transDepthis cleared and the transaction status is failed before the new connection is opened, so a failed reconnect cannot still report success. OCI8 commit mode returns to autocommit.transComplete()does not report success. This applies whenping()reports the connection lost; PostgreSQL’spg_ping()reconnects a broken connection itself, so it is not covered.Database: Fixed MySQLi errno 1213 (
ER_LOCK_DEADLOCK) so a following write throughquery(),simpleQuery()or the Query Builder on the victim connection does not autocommit, andtransCommit()rolls back and returns false after InnoDB has rolled the victim transaction back. Prepared queries and errno 1205 are unchanged.Format: Fixed
XMLFormatterso that an array key that becomes empty after sanitization no longer triggers aValueError; the fallback tag nameitemis used instead. Values that cannot be converted to a string, such as objects not implementingStringable, now throw aFormatExceptioninstead of a fatalError.Helpers: Fixed a regression in v4.7.5 where
set_value()threw aTypeErrorwhen the default value was not a string or an array, such as an integer, a float,null, or aStringableobject. Such values are now converted to a string.HTTP: Fixed a bug in
ContentSecurityPolicywhere a numeric source such as'0'threw aTypeError, and a space-separated directive string inConfig\\ContentSecurityPolicywas emitted as one unquoted source.HTTP: Fixed a regression in v4.7.0 where calling
ContentSecurityPolicy::reportOnly()after the policy was created did not apply to array sources fromConfig\\ContentSecurityPolicy, so they stayed in the enforcedContent-Security-Policyheader alongside theContent-Security-Policy-Report-Onlyheader.HTTP: Fixed a bug in
ContentSecurityPolicy::setDefaultSrc()where passing an array of URIs resulted in an invalid'Array'directive.HTTP: Fixed a bug where
CURLRequestthrew aTypeErrorwhendecode_contentwas enabled without custom request headers becauseCURLOPT_HTTPHEADERwas set to a string instead of an array.HTTP: Fixed a bug where
IncomingRequestdid not read the request body when PHP’spost_max_sizewas set to0or-1(unlimited).HTTP: Fixed a bug where
IncomingRequest::isSecure()did not recognizehttpsin theX-Forwarded-Protoheader from a trusted proxy when the value contained uppercase letters or surrounding whitespace, or was represented as a single-element array.HTTP: Fixed a bug where
Negotiate::media()threw anErrorExceptionwhen theAcceptheader contained a value without a subtype, such asjsonor an empty list element. Such values are now ignored.HTTP: Fixed a bug where
Negotiatecould select a value that the request header excluded withq=0through a wildcard or a broader range. Aq=0range now excludes every value it matches, such astext/htmlandtext/html;charset=utf-8fortext/*;q=0, oren-USforen;q=0, unless a more specific range in the header accepts the value. For example,Accept-Encoding: gzip;q=0, *no longer selectsgzip.HTTP: Fixed stale
$_REQUESTvalues afterSiteURIFactorynormalizes GET parameters, soValidation::withRequest()reads the normalized values while unrelated REQUEST entries are preserved.I18n: Fixed a regression in v4.7.1 where
TimeandTimeLegacylost their timezone, locale and__toString()format afterunserialize(), so methods such asdifference()andgetTimezoneName()threw anError.Routing: Corrected the PHPDoc callback type of
RouteCollection::group()to preserve the concrete collection type, allowing callbacks typed to aRouteCollectionsubclass to pass static analysis.Security: Fixed a bug where a valid JSON request body that is not an object (for example a JSON array) was corrupted after CSRF verification when the token was sent via the
X-CSRF-TOKENheader.
See the repo’s CHANGELOG.md for a complete list of bugs fixed.