security
object
%audit
/phlo/resources/security/audit.phlo
method
%audit -> log ($model, $action, $before = [], $after = [], $exclude = [])
line 10
记录对模型所做的更改到审计日志中,捕获操作类型、前后状态,并排除指定字段。
$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
比较两个数组 $before 和 $after,并返回一个变化数组,指示每个更改属性的旧值和新值。
$changed = []
foreach ($after AS $col => $newVal){
$oldVal = $before[$col] ?? null
if ($oldVal !== $newVal) $changed[$col] = ['from' => $oldVal, 'to' => $newVal]
}
return $changedmethod
%audit -> history ($model, $recordId, $limit = 50)
line 40
检索指定模型和记录ID的审计历史,返回按时间戳降序排序的有限条目。
$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
检索特定用户的审计日志条目,从给定时间戳开始,并限制返回的条目数量。
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
删除审计日志表中超过指定秒数的条目。
%MySQL->query('DELETE FROM audit_log WHERE ts < ?', time() - $olderThanSeconds)object
%creds
/phlo/resources/security/creds.phlo
method
%creds -> __construct (?array $values = null)
line 9
初始化creds对象,如果未提供值则解析值,并将每个值分配给相应的属性,根据需要创建static或SensitiveParameterValue的新实例。
$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
此函数解析并合并来自INI文件和环境变量的凭据数据,返回合并后的数据数组。
$data = []
$this->merge($data, $this->loadINI(data.'creds.ini'))
$this->merge($data, $this->envValues(false))
$this->merge($data, $this->envValues(true))
return $datamethod
%creds -> loadINI (string $file):array
line 24
从指定的文件路径加载INI文件中的配置设置,并将其作为关联数组返回。如果文件不存在或无法解析,则返回一个空数组。
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
提取以指定前缀开头的环境变量值,选项上限于主机,并将其作为关联数组返回。
$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 $outmethod
%creds -> hostKey
line 49
此函数处理请求中的主机,将其转换为大写,替换非字母数字字符为下划线,并修剪任何前导或尾随的下划线。
$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
根据提供的部分将值分配给嵌套数组结构,必要时创建中间数组。
$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
将$add数组的内容合并到$base数组中,如果两个都是数组,则递归地组合值。
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
从对象中检索与指定键关联的值,以安全的方式返回敏感值。
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
此函数处理objData中的每个项目,将SensitiveParameterValue的实例替换为与其长度相对应的星号,同时保持其他值不变。
loop($this->objData, fn($value) => is_a($value, '\SensitiveParameterValue') ? str_repeat('*', strlen($value->getValue())) : $value)object
%CSRF
/phlo/resources/security/CSRF.phlo
view
%CSRF -> view
line 11
生成一个包含CSRF令牌的meta标签,用于安全的表单提交。
<meta name=csrf content="$this->token">prop
%CSRF -> token
line 12
如果会话中尚不存在,则生成一个32个字符的CSRF令牌。
%session->csrf ??= token(32)method
%CSRF -> verify
line 13
通过将其与在HTTP头中接收到的令牌进行比较来验证CSRF令牌。
hash_equals($this->token, (string)($_SERVER['HTTP_X_CSRF_TOKEN'] ?? void))method
%CSRF -> update
line 14
更新会话中的CSRF令牌并将其分配给csrf属性。
arr(csrf: $this->token = %session->csrf = token(32))view
script
line 16
在文档的meta标签中设置CSRF令牌值。
app.mod.csrf = value => obj('meta[name="csrf"]').content = valueobject
%JWT
/phlo/resources/security/JWT.phlo
method
%JWT -> __construct (public string $secret, public string $issuer = void, public int $leeway = 30)
line 10
使用密钥、发行者和过期时间的宽限期初始化JWT实例。它确保密钥至少为32个字节长,如果不满足此条件,则抛出错误。
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
通过使用指定的生存时间(TTL)对提供的声明进行签名来生成 JSON Web Token(JWT)。该令牌包括签发时间(iat)和到期时间(exp)时间戳,并可以选择性地包含发行者(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
通过检查其结构、签名和声明(如过期时间和发行者)来验证 JSON Web Token (JWT)。
$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 $claimsmethod
%JWT -> sig (string $body):string
line 39
使用HMAC和SHA-256及秘密密钥为给定主体生成签名。
$this->encode(hash_hmac('sha256', $body, $this->secret, true))method
%JWT -> encode ($data):string
line 40
使用base64编码将给定数据编码为JSON Web Token (JWT)格式。
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
从 base64 编码的字符串解码 JSON Web Token (JWT),将 URL 安全字符替换为标准 base64 字符。
(string)base64_decode(strtr($data, '-_', '+/'))object
%rate
/phlo/resources/security/rate.phlo
method
%rate -> check ($key, $limit, $windowSeconds, $storage = 'db')
line 10
检查在指定时间窗口内,给定键的速率限制是否被超出,数据存储在数据库或APCu缓存中。
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 truemethod
%rate -> checkApcu ($key, $limit, $windowSeconds)
line 23
检查在指定时间窗口内给定键的发生率,使用 APCu 进行缓存。它增加该键的计数并返回计数是否在定义的限制内。
$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 <= $limitmethod
%rate -> status ($key, $limit, $windowSeconds)
line 31
检查给定键的当前速率限制状态,返回已使用的请求数量、限制和重置限制的时间。
$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
通过从MySQL数据库中的rate_limit表中删除相应条目来重置指定键的速率限制。
%MySQL->query('DELETE FROM rate_limit WHERE rkey=?', $key)method
%rate -> purge ($olderThanSeconds = 604800)
line 39
从MySQL的rate_limit表中删除window_start时间戳早于指定秒数的条目。
%MySQL->query('DELETE FROM rate_limit WHERE window_start < ?', time() - $olderThanSeconds)object
%security
/phlo/resources/security/security.phlo
prop
%security -> whitelist
line 10
定义一个安全白名单,仅允许指定的条目。
[]method
%security -> setNonce
line 12
使用指定长度的生成令牌为应用程序设置一个随机数值。
%app->nonce = token(8)method
%security -> frameProtect ($mode = 'DENY')
line 14
设置 X-Frame-Options 头部以控制页面是否可以在框架中显示,默认模式为 'DENY'。
%res->header('X-Frame-Options', $mode)method
%security -> frameWhitelist
line 15
生成一个用于安全目的的白名单框架来源字符串,仅允许指定的域在iframe中嵌入内容。
$this->whitelist ? ' '.implode(space, array_map(fn($d) => "https://*.$d", (array)$this->whitelist)) : voidmethod
%security -> strict
line 17
为响应设置严格的安全头,包括Cache-Control和Content-Security-Policy,以增强对各种网络漏洞的保护。
$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
为响应设置内容安全策略(CSP)头,根据请求的异步状态和调试模式定义各种内容类型的允许源。
$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
为响应设置内容安全策略(CSP)头,定义各种内容类型的允许源,以增强安全性。
$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
为API响应设置与安全相关的HTTP头,包括Content-Security-Policy、X-Content-Type-Options和Referrer-Policy,以增强安全性。
%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
为响应设置各种与安全相关的HTTP头,包括Referrer-Policy、X-Content-Type-Options、跨源策略和X-Frame-Options,以增强对跨源攻击的防护。
%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
使用提供的密钥解密给定的 base64 编码的加密字符串,返回原始数据或在解密失败时返回 false。
($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)) : falsefunction
encrypt($data, $key):string
/phlo/resources/security/encryption.phlo line 8
使用秘密密钥加密给定数据并返回 base64 编码的结果。
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
使用定义的字母表生成指定长度的随机令牌,可选地用输入字符串作为种子以增加随机性。
$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