A humble PHP framework & CMS
Humblee enforces security at the framework level wherever possible. This page documents the mechanisms in place and the patterns every custom controller and model must follow.
Every state-changing POST — whether a form submission or an AJAX call — must carry a valid HMAC token pair. This is not optional; there is no way to disable it. The Xhr base class (require_hmac()) and the form helper (Crypto::get_hmac_pair()) make it straightforward to add this to any endpoint.
Crypto::get_hmac_pair() generates a one-time key/token pair:
base64(hash_hmac('sha256', key, csrf_session_token))$_SESSION[session_key]['csrf_token']Both values are included in the request (as hidden form fields or request body fields named hmac_key and hmac_token).
The server validates with Crypto::check_hmac_pair(), which uses a timing-safe comparison.
Tokens are single-use. After each successful AJAX action, the server returns a fresh pair so the next request can proceed. If validation fails, the request is rejected with HTTP 401.
<?php $hmac = \Humblee\Model\Crypto::get_hmac_pair() ?>
<form method="post">
<input type="hidden" name="hmac_key" value="<?php echo $hmac['key'] ?>">
<input type="hidden" name="hmac_token" value="<?php echo $hmac['token'] ?>">
<!-- your form fields -->
</form>
public function save(): void
{
$this->require_hmac(); // 401 + exit if invalid
$this->require_role('admin'); // 403 + exit if unauthorized
// safe to process POST data now
}
After every successful action, return a fresh pair so the Svelte frontend can make the next request:
Core::json([
'status' => 'ok',
'data' => $result,
'csrf' => \Humblee\Model\Crypto::get_hmac_pair(),
]);
On the Svelte side, update the stored csrf object when the response includes one:
const data = await res.json()
if (data.csrf) {
csrf = data.csrf
}
In any XHR endpoint, always check in this order. Never reverse it.
public function myAction(): void
{
$this->require_hmac(); // 1. Validate CSRF — always first
$this->require_role('admin'); // 2. Check role
// 3. Read and validate input
// 4. Perform the action
}
Checking roles before HMAC is incorrect — the CSRF token must be validated regardless of whether the role check would also fail. This prevents timing-based information leakage.
Validate all data at the system boundary, before it reaches the ORM or any business logic.
if (!preg_match('/^[\w\-\.]+$/', $slug)) {
http_response_code(400);
exit;
}
$page = \ORM::for_table(_table_pages)->where('slug', $slug)->find_one();
$id = (int) Package::current()->get('id');
if ($id <= 0) {
Core::json(['error' => 'Invalid ID'], 400);
}
Only read the fields you expect. Do not pass $_POST or Package::current()->all() directly to ORM methods.
$package = Package::current();
$record->name = trim((string) $package->get('name', ''));
$record->sort = (int) $package->get('sort', 0);
$record->save();
htmlspecialchars($value, ENT_QUOTES, 'UTF-8')json_encode() escapes by default; no additional escaping is needed// Safe
<span><?php echo htmlspecialchars($user->name, ENT_QUOTES, 'UTF-8') ?></span>
// Also safe — Draw::content() renders stored HTML
<?php Draw::content($content, 'pagebody') ?>
All queries use the Idiorm ORM, which parameterizes every value. Never concatenate user input into a query string.
// Correct — parameterized
$record = \ORM::for_table(_table_pages)
->where('slug', $slug)
->find_one();
// Forbidden — string concatenation
$record = \ORM::for_table(_table_pages)
->where_raw("slug = '" . $slug . "'") // never do this
->find_one();
The only place raw SQL appears in the framework is in Tools::CRUD() column names, which are validated against an explicit whitelist before use.
Use Crypto::encrypt() and Crypto::decrypt() for any data you want to protect on disk. The framework uses this for media files — the same API is available for your own data.
use Humblee\Model\Crypto;
// Encrypt
$encrypted = Crypto::encrypt($plaintext);
if ($encrypted === false) {
// handle failure
}
// Decrypt
$plaintext = Crypto::decrypt($encrypted);
if ($plaintext === false) {
// handle failure — wrong key, corrupted data, or tampered ciphertext
}
humblee/configuration/crypto/key.php (never committed to version control)decrypt() returns false if authentication fails (tampered ciphertext, wrong key, or data corruption). Always check the return value.
Files uploaded to the media library are stored outside the web root (/storage/) and are never served directly by the web server. All file access goes through Humblee\Controller\Media, which enforces per-file role restrictions and handles decryption on the fly. See Media Manager for details.
The following patterns are prohibited in this codebase and will be rejected in code review:
| Pattern | Risk |
|---|---|
eval() |
Code injection |
preg_replace('/e', ...) |
Code execution via regex callback |
include($_GET[...]) or require($userInput) |
Path traversal / local file inclusion |
| Concatenating user input into SQL | SQL injection |
| Hardcoded secrets, API keys, or credentials | Secret exposure in version control |
MD5 or SHA1 for passwords |
Broken — use Argon2ID |
serialize() on untrusted input |
PHP object injection |
Reading $_POST / $_GET directly instead of Package |
Bypasses normalization |
| Checking roles before HMAC in XHR endpoints | CSRF token left unvalidated |