mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-10-02 15:45:09 -04:00
* fix(security): report unwritable .env.lock, make throttle limits configurable
envFileIsWritable() previously checked is_writable(.env) (the file), which passes in Docker even when the mutex file is root-owned by a prior env:provision run. It now checks the real write path: the directory (to create .env.tmp.* + .env.lock) and any existing .env.lock must be writable, so the app throws the clear 'run env:provision' error instead of crashing on 'Unable to open .env.lock'.
The Throttle filter now reads throttle.capacity / throttle.seconds from env (default 5 per 60s); capacity <= 0 disables throttling, so operators serving sequential HTTP clients (e.g. Zabbix) are not caught by the lockout.
* fix(security): address CodeRabbit review findings
- Throttle: validate throttle.capacity as an integer before treating a non-positive value as 'disabled', so a non-numeric value (e.g. 'five') falls back to the default instead of silently bypassing the lockout. Apply the same validation to throttle.seconds.
- envFileIsWritable(): also reject an existing non-writable .env on Windows (where rename() cannot replace a read-only destination); keep the check Windows-only since POSIX rename() replaces a read-only dest when the directory is writable.
- Tests: restore the prior throttle.capacity env state in ThrottleTest (capture/restore instead of delete); add an invalid-capacity fallback case; skip the not-writable fixtures when running as root (where is_writable() is bypassed).
* fix(migration): guard ConvertToCI4 key-write branches with envFileIsWritable()
The migration's 'no key' and 'CI3 key' branches called rotateEncryptionKey()/rotateEncryptionKeyTransaction() directly, bypassing the envFileIsWritable() guard that checkEncryption() uses. On a fresh Docker/Compose install where the web runtime cannot write /app/.env.lock, this produced a raw 'fopen(/app/.env.lock): Permission denied' error instead of the actionable 'run php spark env:provision' message.
A valid CI4 key now short-circuits to checkEncryption() (no write); every write branch is gated on envFileIsWritable() first.
* fix: read provisioned encryption.key so config sees it
env:provision persists the key as 'encryption.key' in .env (matching the
throttle path and .env.example), but Config\Encryption only read the
ENCRYPTION_KEY env var. On a fresh Docker instance the provisioned key was
invisible to config('Encryption')->key, so the app believed no key existed
and tried to write one -- hitting the .env.lock permission wall.
Read encryption.key (via $_SERVER/$_ENV/getenv) first, then fall back to
ENCRYPTION_KEY for Docker '-e' usage. Mirrors checkThrottleEncryption().
* fix: cascade encryption.key lookup past empty-string sources
* refactor: extract Encryption::resolveKey() and test it without global env mutation
* fix(security): decode fallback-selected encryption key like BaseConfig
A key picked in the constructor fallback (notably ENCRYPTION_KEY, which
BaseConfig never inspects) was assigned verbatim, bypassing the
hex2bin:/base64: decode the parent applies to `encryption.key`. Route the
selected key through a parseKey() helper mirroring BaseConfig's
parseEncryptionKey() so prefixed values decrypt consistently, and add a
pure regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: remove advisory ID from comments and tighten verbose comments
Per review: treat GHSA advisory IDs like secrets (drop from code) and
replace the multi-paragraph comments with concise one-liners that keep
the non-obvious "why". No logic changes.
---------
Co-authored-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
159 lines
5.0 KiB
PHP
159 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace Config;
|
|
|
|
use CodeIgniter\Config\BaseConfig;
|
|
|
|
/**
|
|
* Encryption configuration.
|
|
*
|
|
* These are the settings used for encryption, if you don't pass a parameter
|
|
* array to the encrypter for creation/initialization.
|
|
*/
|
|
class Encryption extends BaseConfig
|
|
{
|
|
/**
|
|
* --------------------------------------------------------------------------
|
|
* Encryption Key Starter
|
|
* --------------------------------------------------------------------------
|
|
*
|
|
* If you use the Encryption class you must set an encryption key (seed).
|
|
* You need to ensure it is long enough for the cipher and mode you plan to use.
|
|
* See the user guide for more info.
|
|
*/
|
|
public string $key = '';
|
|
|
|
/**
|
|
* --------------------------------------------------------------------------
|
|
* Previous Encryption Keys
|
|
* --------------------------------------------------------------------------
|
|
*
|
|
* When rotating encryption keys, add old keys here to maintain ability
|
|
* to decrypt data encrypted with previous keys. Encryption always uses
|
|
* the current $key. Decryption tries current key first, then falls back
|
|
* to previous keys if decryption fails.
|
|
*
|
|
* In .env file, use comma-separated string:
|
|
* encryption.previousKeys = hex2bin:9be8c64fcea509867...,hex2bin:3f5a1d8e9c2b7a4f6...
|
|
*
|
|
* @var list<string>|string
|
|
*/
|
|
public array|string $previousKeys = '';
|
|
|
|
/**
|
|
* --------------------------------------------------------------------------
|
|
* Encryption Driver to Use
|
|
* --------------------------------------------------------------------------
|
|
*
|
|
* One of the supported encryption drivers.
|
|
*
|
|
* Available drivers:
|
|
* - OpenSSL
|
|
* - Sodium
|
|
*/
|
|
public string $driver = 'OpenSSL';
|
|
|
|
/**
|
|
* --------------------------------------------------------------------------
|
|
* SodiumHandler's Padding Length in Bytes
|
|
* --------------------------------------------------------------------------
|
|
*
|
|
* This is the number of bytes that will be padded to the plaintext message
|
|
* before it is encrypted. This value should be greater than zero.
|
|
*
|
|
* See the user guide for more information on padding.
|
|
*/
|
|
public int $blockSize = 16;
|
|
|
|
/**
|
|
* --------------------------------------------------------------------------
|
|
* Encryption digest
|
|
* --------------------------------------------------------------------------
|
|
*
|
|
* HMAC digest to use, e.g. 'SHA512' or 'SHA256'. Default value is 'SHA512'.
|
|
*/
|
|
public string $digest = 'SHA512';
|
|
|
|
/**
|
|
* Whether the cipher-text should be raw. If set to false, then it will be base64 encoded.
|
|
* This setting is only used by OpenSSLHandler.
|
|
*
|
|
* Set to false for CI3 Encryption compatibility.
|
|
*/
|
|
public bool $rawData = false;
|
|
|
|
/**
|
|
* Encryption key info.
|
|
* This setting is only used by OpenSSLHandler.
|
|
*
|
|
* Set to 'encryption' for CI3 Encryption compatibility.
|
|
*/
|
|
public string $encryptKeyInfo = '';
|
|
|
|
/**
|
|
* Authentication key info.
|
|
* This setting is only used by OpenSSLHandler.
|
|
*
|
|
* Set to 'authentication' for CI3 Encryption compatibility.
|
|
*/
|
|
public string $authKeyInfo = '';
|
|
|
|
/**
|
|
* Cipher to use.
|
|
* This setting is only used by OpenSSLHandler.
|
|
*
|
|
* Set to 'AES-128-CBC' to decrypt encrypted data that encrypted
|
|
* by CI3 Encryption default configuration.
|
|
*/
|
|
public string $cipher = 'AES-256-CTR';
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
|
|
if ($this->key === '') {
|
|
// Fallback sources (notably the ENCRYPTION_KEY Docker var, which the
|
|
// parent never reads) were not decode-parsed, so run them through
|
|
// the same parser to keep hex2bin:/base64: keys consistent.
|
|
$this->key = self::parseKey(self::resolveKey(
|
|
(string) ($_SERVER['encryption.key'] ?? ''),
|
|
(string) ($_ENV['encryption.key'] ?? ''),
|
|
(string) getenv('encryption.key'),
|
|
(string) getenv('ENCRYPTION_KEY'),
|
|
));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decode a key's `hex2bin:`/`base64:` prefix, mirroring
|
|
* BaseConfig::parseEncryptionKey(); kept static so it is unit-testable.
|
|
*/
|
|
public static function parseKey(string $key): string
|
|
{
|
|
if (str_starts_with($key, 'hex2bin:')) {
|
|
return (string) hex2bin(substr($key, 8));
|
|
}
|
|
|
|
if (str_starts_with($key, 'base64:')) {
|
|
return (string) base64_decode(substr($key, 7), true);
|
|
}
|
|
|
|
return $key;
|
|
}
|
|
|
|
/**
|
|
* Return the first non-empty source (highest precedence first). Cascading
|
|
* past empty strings (vs `??`) avoids a blank value shadowing the real key.
|
|
*/
|
|
public static function resolveKey(string ...$sources): string
|
|
{
|
|
foreach ($sources as $source) {
|
|
if ($source !== '') {
|
|
return $source;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
}
|