security

object

%audit

/phlo/resources/security/audit.phlo
version 1.1
creator q-ai.nl
summary Audit log for model mutations (opt-in via static idColumn/objAudit). Schema: resources/security/audit.sql
package security
frontend false
backend true
requires @MySQL
tags audit log compliance traceability
method

%audit -> log ($model, $action, $before = [], $after = [], $exclude = [])

line 10
Logs changes made to a model in the audit log, capturing the action type, before and after states, and excluding specified fields.
$class = is_object($model) ? get_class($model) : (string)$model
$pk = (is_string($class) && class_exists($class) && property_exists($class, 'idColumn')) ? $class::$idColumn : 'id'
$id = is_object($model) ? ($model->$pk ?? $model->id ?? null) : null
if ($id === null) return
$before = (array)$before
$after = (array)$after
foreach ($exclude AS $col) unset($before[$col], $after[$col])
$changes = $action === 'update' ? $this->diff($before, $after) : ($action === 'create' ? $after : $before)
%MySQL->query(
	'INSERT INTO audit_log (ts, user, model, record_id, action, changes, ip) VALUES (?, ?, ?, ?, ?, ?, ?)',
	time(),
	isset(%session->user) ? (int)%session->user : null,
	$class,
	(string)$id,
	$action,
	json_encode($changes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
	(string)($_SERVER['REMOTE_ADDR'] ?? null),
)
method

%audit -> diff ($before, $after)

line 31
Compares two arrays, $before and $after, and returns an array of changes where values differ, indicating the old and new values for each changed property.
$changed = []
foreach ($after AS $col => $newVal){
	$oldVal = $before[$col] ?? null
	if ($oldVal !== $newVal) $changed[$col] = ['from' => $oldVal, 'to' => $newVal]
}
return $changed
method

%audit -> history ($model, $recordId, $limit = 50)

line 40
Retrieves the audit history for a specified model and record ID, returning a limited number of entries sorted by timestamp in descending order.
$class = is_object($model) ? get_class($model) : (string)$model
return %MySQL->query(
	'SELECT * FROM audit_log WHERE model=? AND record_id=? ORDER BY ts DESC LIMIT ?',
	$class, (string)$recordId, (int)$limit,
)->fetchAll(\PDO::FETCH_OBJ)
method

%audit -> byUser ($userId, $fromTs = 0, $limit = 100)

line 48
Retrieves audit log entries for a specific user, starting from a given timestamp, with a limit on the number of entries returned.
return %MySQL->query(
	'SELECT * FROM audit_log WHERE user=? AND ts >= ? ORDER BY ts DESC LIMIT ?',
	(int)$userId, (int)$fromTs, (int)$limit,
)->fetchAll(\PDO::FETCH_OBJ)
method

%audit -> purge ($olderThanSeconds = 31536000)

line 55
Deletes entries from the audit_log table that are older than the specified number of seconds.
%MySQL->query('DELETE FROM audit_log WHERE ts < ?', time() - $olderThanSeconds)
object

%creds

/phlo/resources/security/creds.phlo
version 1.0
creator q-ai.nl
summary Credentials resolver from env and ini sources
package security
frontend false
backend true
tags credentials env ini secrets configuration
method

%creds -> __construct (?array $values = null)

line 9
Initializes the creds object, resolving values if not provided, and assigns each value to the corresponding property, creating a new instance of static or SensitiveParameterValue as needed.
$values ??= $this->resolve()
foreach ($values AS $key => $value){
	$this->$key = is_array($value) ? new static($value) : new \SensitiveParameterValue((string)$value)
}
method

%creds -> resolve

line 16
This function resolves and merges credential data from an INI file and environment variables, returning the combined data array.
$data = []
$this->merge($data, $this->loadINI(data.'creds.ini'))
$this->merge($data, $this->envValues(false))
$this->merge($data, $this->envValues(true))
return $data
method

%creds -> loadINI (string $file):array

line 24
Loads configuration settings from an INI file specified by the given file path and returns them as an associative array. If the file does not exist or cannot be parsed, it returns an empty array.
if (!is_file($file)) return []
$ini = parse_ini_file($file, true, INI_SCANNER_RAW)
return is_array($ini) ? $ini : []
method

%creds -> envValues (bool $hostScoped = false):array

line 30
Extracts environment variable values that start with a specified prefix, optionally scoped to the host, and returns them as an associative array.
$out = []
$prefix = $hostScoped ? ('PHLO_'.$this->hostKey().'__') : 'PHLO__'
$sources = []
is_array($_ENV ?? null) && $sources[] = $_ENV
is_array($_SERVER ?? null) && $sources[] = $_SERVER
is_array($env = getenv()) && $sources[] = $env
foreach ($sources AS $source){
	foreach ($source AS $key => $value){
		$key = (string)$key
		if (!str_starts_with($key, $prefix)) continue
		$path = substr($key, strlen($prefix))
		if (!$path) continue
		$this->envAssign($out, explode('__', $path), (string)$value)
	}
}
return $out
method

%creds -> hostKey

line 49
This function processes the host from the request, converting it to uppercase, replacing non-alphanumeric characters with underscores, and trimming any leading or trailing underscores.
$host = strtoupper(%req->host)
$host = preg_replace('/[^A-Z0-9]+/', '_', $host)
return trim($host, '_')
method

%creds -> envAssign (array &$target, array $parts, string $value):void

line 55
Assigns a value to a nested array structure based on the provided parts, creating intermediate arrays as necessary.
$parts = array_values(array_filter(loop($parts, fn($part) => trim($part)), 'strlen'))
if (!$parts) return
$node = &$target
$last = count($parts) - 1
foreach ($parts AS $i => $part){
	if ($i === $last){
		$node[$part] = $value
		return
	}
	if (!isset($node[$part]) || !is_array($node[$part])) $node[$part] = []
	$node = &$node[$part]
}
method

%creds -> merge (array &$base, array $add):void

line 70
Merges the contents of the $add array into the $base array, recursively combining values if both are arrays.
foreach ($add AS $key => $value){
	if (isset($base[$key]) && is_array($base[$key]) && is_array($value)){
		$this->merge($base[$key], $value)
		continue
	}
	$base[$key] = $value
}
method

%creds -> objGet ($key)

line 80
Retrieves the value associated with the specified key from the object, returning sensitive values in a secure manner.
if ($key === 'toArray') return loop($this->objData, fn($value) => is_a($value, 'SensitiveParameterValue') ? $value->getValue() : $value)
if (isset($this->objData[$key]) && is_a($this->objData[$key], '\SensitiveParameterValue')) return $this->objData[$key]->getValue()
method

%creds -> objInfo

line 85
This function processes each item in objData, replacing instances of SensitiveParameterValue with asterisks corresponding to their length, while leaving other values unchanged.
loop($this->objData, fn($value) => is_a($value, '\SensitiveParameterValue') ? str_repeat('*', strlen($value->getValue())) : $value)
object

%CSRF

/phlo/resources/security/CSRF.phlo
version 1.0
creator q-ai.nl
summary Rotating async CSRF protection for Phlo requests
package security
frontend true
backend true
requires @session token payload
provides app.mod.csrf
tags csrf security async forms
view

%CSRF -> view

line 11
Generates a meta tag containing the CSRF token for secure form submissions.
<meta name=csrf content="$this->token">
prop

%CSRF -> token

line 12
Generates a CSRF token of 32 characters if one does not already exist in the session.
%session->csrf ??= token(32)
method

%CSRF -> verify

line 13
Verifies the CSRF token by comparing it with the token received in the HTTP header.
hash_equals($this->token, (string)($_SERVER['HTTP_X_CSRF_TOKEN'] ?? void))
method

%CSRF -> update

line 14
Updates the CSRF token in the session and assigns it to the csrf property.
arr(csrf: $this->token = %session->csrf = token(32))
view

script

line 16
Sets the CSRF token value in the meta tag of the document.
app.mod.csrf = value => obj('meta[name="csrf"]').content = value
object

%JWT

/phlo/resources/security/JWT.phlo
version 1.0
creator q-ai.nl
summary Sign and verify compact HS256 JSON Web Tokens (RFC 7519), secure by default
package security
frontend false
backend true
requires php-ext:hash
tags jwt jws hs256 token auth security
method

%JWT -> __construct (public string $secret, public string $issuer = void, public int $leeway = 30)

line 10
Initializes a JWT instance with a secret, issuer, and leeway for expiration time. It ensures the secret is at least 32 bytes long, throwing an error if this condition is not met.
strlen($this->secret) >= 32 || error('JWT secret must be at least 32 bytes', 500)
method

%JWT -> sign (array $claims, int $ttl = 3600):string

line 14
Generates a JSON Web Token (JWT) by signing the provided claims with a specified time-to-live (TTL). The token includes issued at (iat) and expiration (exp) timestamps, and can optionally include an issuer (iss).
$now = time()
$claims['iat'] = $now
$claims['exp'] = $now + $ttl
if ($this->issuer !== void) $claims['iss'] = $this->issuer
$body = $this->encode(['alg' => 'HS256', 'typ' => 'JWT']).dot.$this->encode($claims)
return $body.dot.$this->sig($body)
method

%JWT -> verify (string $token):array

line 23
Verifies a JSON Web Token (JWT) by checking its structure, signature, and claims such as expiration and issuer.
$token = preg_replace('/^Bearer\s+/i', void, trim($token))
$parts = explode(dot, $token)
count($parts) === 3 || error('JWT malformed', 401)
[$h, $p, $s] = $parts
$header = (array)json_decode((string)$this->decode($h), true)
($header['alg'] ?? void) === 'HS256' || error('JWT algorithm not allowed', 401)
hash_equals($this->sig($h.dot.$p), $s) || error('JWT signature invalid', 401)
$claims = (array)json_decode((string)$this->decode($p), true)
$now = time()
isset($claims['nbf']) && $now + $this->leeway < $claims['nbf'] && error('JWT not yet valid', 401)
isset($claims['exp']) && $now - $this->leeway >= $claims['exp'] && error('JWT expired', 401)
$this->issuer === void || ($claims['iss'] ?? void) === $this->issuer || error('JWT issuer mismatch', 401)
return $claims
method

%JWT -> sig (string $body):string

line 39
Generates a signature for the given body using HMAC with SHA-256 and a secret key.
$this->encode(hash_hmac('sha256', $body, $this->secret, true))
method

%JWT -> encode ($data):string

line 40
Encodes the given data into a JSON Web Token (JWT) format using base64 encoding.
rtrim(strtr(base64_encode(is_string($data) ? $data : (string)json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)), '+/', '-_'), eq)
method

%JWT -> decode (string $data):string

line 41
Decodes a JSON Web Token (JWT) from a base64-encoded string, replacing URL-safe characters with standard base64 characters.
(string)base64_decode(strtr($data, '-_', '+/'))
object

%rate

/phlo/resources/security/rate.phlo
version 1.2
creator q-ai.nl
summary Rate-limit (fixed window) op rate_limit-tabel. Schema: resources/security/rate.sql
package security
frontend false
backend true
requires @MySQL
tags rate limit throttle abuse
method

%rate -> check ($key, $limit, $windowSeconds, $storage = 'db')

line 10
Checks if a rate limit has been exceeded for a given key within a specified time window, storing the data in a database or APCu cache.
if ($storage === 'apcu') return $this->checkApcu($key, $limit, $windowSeconds)
$now = time()
$row = %MySQL->query('SELECT count, window_start FROM rate_limit WHERE rkey=?', $key)->fetchObject('obj') ?: null
if ($row && ($now - (int)$row->window_start) < $windowSeconds){
	if ((int)$row->count >= $limit) return false
	%MySQL->query('UPDATE rate_limit SET count=count+1 WHERE rkey=?', $key)
	return true
}
%MySQL->query('INSERT INTO rate_limit (rkey, count, window_start) VALUES (?,1,?) ON DUPLICATE KEY UPDATE count=1, window_start=VALUES(window_start)', $key, $now)
return true
method

%rate -> checkApcu ($key, $limit, $windowSeconds)

line 23
Checks the rate of occurrences for a given key within a specified time window, using APCu for caching. It increments the count for the key and returns whether the count is within the defined limit.
$window = (int)floor(time() / $windowSeconds) * $windowSeconds
$apcuKey = 'phlo.rate.'.$key.':'.$window
$count = apcu_inc($apcuKey, 1, $success, $windowSeconds)
if (!$success) apcu_store($apcuKey, 1, $windowSeconds)
return $count <= $limit
method

%rate -> status ($key, $limit, $windowSeconds)

line 31
Checks the current rate limit status for a given key, returning the number of requests used, the limit, and the time until the limit resets.
$now = time()
$row = %MySQL->query('SELECT count, window_start FROM rate_limit WHERE rkey=?', $key)->fetchObject('obj') ?: null
if (!$row || ($now - (int)$row->window_start) >= $windowSeconds) return obj(used: 0, limit: $limit, resetIn: 0)
return obj(used: (int)$row->count, limit: $limit, resetIn: max(0, ((int)$row->window_start + $windowSeconds) - $now))
method

%rate -> reset ($key)

line 38
Resets the rate limit for a specified key by deleting the corresponding entry from the rate_limit table in the MySQL database.
%MySQL->query('DELETE FROM rate_limit WHERE rkey=?', $key)
method

%rate -> purge ($olderThanSeconds = 604800)

line 39
Deletes entries from the rate_limit table in MySQL where the window_start timestamp is older than the specified number of seconds.
%MySQL->query('DELETE FROM rate_limit WHERE window_start < ?', time() - $olderThanSeconds)
object

%security

/phlo/resources/security/security.phlo
version 1.0
creator q-ai.nl
summary Generic security resource
package security
frontend true
backend true
requires @session token
tags security csp nonce headers
prop

%security -> whitelist

line 10
Defines a whitelist for security purposes, allowing only specified entries.
[]
method

%security -> setNonce

line 12
Sets a nonce value for the application using a generated token of specified length.
%app->nonce = token(8)
method

%security -> frameProtect ($mode = 'DENY')

line 14
Sets the X-Frame-Options header to control whether the page can be displayed in a frame, with the default mode being 'DENY'.
%res->header('X-Frame-Options', $mode)
method

%security -> frameWhitelist

line 15
Generates a string of whitelisted frame origins for security purposes, allowing only specified domains to embed the content in an iframe.
$this->whitelist ? ' '.implode(space, array_map(fn($d) => "https://*.$d", (array)$this->whitelist)) : void
method

%security -> strict

line 17
Sets strict security headers for the response, including Cache-Control and Content-Security-Policy, to enhance protection against various web vulnerabilities.
$this->base
if (%req->async) return
%res->header('Cache-Control', 'no-store')
$nonce = $this->setNonce
%res->header('Content-Security-Policy', "default-src 'self'; script-src 'nonce-$nonce'; style-src 'self' 'nonce-$nonce'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; object-src 'none'; frame-src 'self'$this->frameWhitelist; frame-ancestors 'none'; base-uri 'self'")
method

%security -> basic

line 24
Sets the Content Security Policy (CSP) headers for the response, defining allowed sources for various content types based on the request's async status and debug mode.
$this->base
if (%req->async) return
%res->header('Content-Security-Policy', "default-src 'self'; script-src 'self'".(debug ? " 'unsafe-inline'" : '')."; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; object-src 'none'; frame-src 'self'$this->frameWhitelist; frame-ancestors 'none'; base-uri 'self'")
method

%security -> marketing

line 29
Sets the Content Security Policy (CSP) headers for the response, defining allowed sources for various content types to enhance security.
$this->base
if (%req->async) return
%res->header('Content-Security-Policy', "default-src 'self'; script-src 'self'".(debug ? " 'unsafe-inline'" : '')."; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; form-action 'self'; object-src 'none'; frame-src 'self'$this->frameWhitelist; frame-ancestors 'none'; base-uri 'self'")
method

%security -> api

line 34
Sets security-related HTTP headers for the API response, including Content-Security-Policy, X-Content-Type-Options, and Referrer-Policy to enhance security.
%res->header('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'")
%res->header('X-Content-Type-Options', 'nosniff')
%res->header('Referrer-Policy', 'no-referrer')
method

%security -> base

line 40
Sets various security-related HTTP headers for the response, including Referrer-Policy, X-Content-Type-Options, Cross-Origin policies, and X-Frame-Options, to enhance security against cross-origin attacks.
%res->header('Referrer-Policy', 'strict-origin-when-cross-origin')
%res->header('X-Content-Type-Options', 'nosniff')
%req->async || %res->header('Cross-Origin-Opener-Policy', 'same-origin')
%req->async || %res->header('Cross-Origin-Resource-Policy', 'same-origin')
%req->async || %res->header('Access-Control-Allow-Origin', %req->base)
%req->async || %res->header('X-Frame-Options', 'DENY')

Functions

function

decrypt($encrypted, $key):string|false

/phlo/resources/security/encryption.phlo line 10
Decrypts the given base64-encoded encrypted string using the provided key, returning the original data or false if decryption fails.
($d = base64_decode($encrypted, true)) !== false && strlen($d) >= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ? sodium_crypto_secretbox_open(substr($d, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), substr($d, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES), hash('sha256', $key, true)) : false
function

encrypt($data, $key):string

/phlo/resources/security/encryption.phlo line 8
Encrypts the given data using a secret key and returns the base64-encoded result.
base64_encode(($nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES)).sodium_crypto_secretbox($data, $nonce, hash('sha256', $key, true)))
function

token(int $length = 8, ?string $input = null)

/phlo/resources/security/token.phlo line 8
Generates a random token of specified length using a defined alphabet, optionally seeded with an input string for added randomness.
	$length || error('Token must have a minimum length above 0', 500)
	$alphabet = 'abcdefghijklmnopqrstuvwxyz'
	$alphabetLength = strlen($alphabet)
	$limit = intdiv(256, $alphabetLength) * $alphabetLength
	$token = void
	$buffer = void
	$state = is_null($input) ? null : hash('sha256', (string)$input, true)
	while (strlen($token) < $length){
		if ($buffer === void){
			if (is_null($state)) $buffer = random_bytes(32)
			else {
				$state = hash('sha256', $state, true)
				$buffer = $state
			}
		}
		$byte = ord($buffer[0])
		$buffer = substr($buffer, 1)
		if ($byte >= $limit) continue
		$token .= $alphabet[$byte % $alphabetLength]
	}
	return $token

We use essential cookies to make this site work. With your permission we also use analytics to improve the site.