security
object
%audit
/phlo/resources/security/audit.phlo
static
audit :: log ($model, $action, $before = [], $after = [], $exclude = [])
line 9
Logt wijzigingen die aan een model zijn aangebracht in het auditlogboek, waarbij details zoals de gebruiker, het type actie en de wijzigingen voor en na de aanpassing worden vastgelegd.
$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' ? static::diff($before, $after) : ($action === 'create' ? $after : $before)
$class::DB()->query(
'INSERT INTO audit_log (ts, '.$class::DB()->quoteId('user').', model, record_id, action, changes, ip) VALUES (?, ?, ?, ?, ?, ?, ?)',
time(),
class_exists('session') && 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),
)static
audit :: diff ($before, $after)
line 30
Berechnet de verschillen tussen twee datasets en retourneert een array van gewijzigde eigenschappen met hun oude en nieuwe waarden.
$changed = []
foreach ($after AS $col => $newVal){
$oldVal = $before[$col] ?? null
if ($oldVal !== $newVal) $changed[$col] = ['from' => $oldVal, 'to' => $newVal]
}
return $changedstatic
audit :: history ($model, $recordId, $limit = 50)
line 39
Haal de auditgeschiedenis op voor een specifiek model en record-ID uit de audit_log-tabel, gesorteerd op tijdstempel in aflopende volgorde, met een limiet op het aantal resultaten.
$class = is_object($model) ? get_class($model) : (string)$model
return $class::DB()->query(
'SELECT * FROM audit_log WHERE model=? AND record_id=? ORDER BY ts DESC LIMIT ?',
$class, (string)$recordId, (int)$limit,
)->fetchAll(\PDO::FETCH_OBJ)static
audit :: byUser ($model, $userId, $fromTs = 0, $limit = 100)
line 47
Haal auditlogboekvermeldingen op voor een specifieke gebruiker, gefilterd op een tijdstempel en beperkt tot een bepaald aantal resultaten.
return $model::DB()->query(
'SELECT * FROM audit_log WHERE '.$model::DB()->quoteId('user').'=? AND ts >= ? ORDER BY ts DESC LIMIT ?',
(int)$userId, (int)$fromTs, (int)$limit,
)->fetchAll(\PDO::FETCH_OBJ)static
audit :: purge ($model, $olderThanSeconds = 31536000)
line 54
Verwijdert vermeldingen uit de audit_log-tabel die ouder zijn dan een opgegeven aantal seconden.
$model::DB()->query('DELETE FROM audit_log WHERE ts < ?', time() - $olderThanSeconds)object
%captcha
/phlo/resources/security/captcha.phlo
static
captcha :: W
line 10
Genereert een CAPTCHA-afbeelding met een breedte van 300 pixels.
300static
captcha :: H
line 11
Deze constante vertegenwoordigt de hoogte van de CAPTCHA-afbeelding in pixels.
180static
captcha :: P
line 12
captcha::$P is een constante die de waarde van de captcha-parameter bevat die wordt gebruikt voor validatie in Phlo-toepassingen.
56static
captcha :: tol
line 13
captcha::$tol retourneert het tolerantieniveau voor het CAPTCHA-validatieproces, dat bepaalt hoe soepel het systeem is bij het beoordelen van gebruikersreacties.
8static
captcha :: ttl
line 14
Stelt de time-to-live (TTL) voor de captcha in seconden in.
600static
captcha :: issue
line 16
Genereert een willekeurige kloof voor een captcha-uitdaging en slaat de kloofwaarden op in de sessie met een vervaltijd.
$gapX = random_int(static::$P + 14, static::$W - static::$P - 6)
$gapY = random_int(12, static::$H - static::$P - 12)
%session->captcha = arr(gap: $gapX, exp: time() + static::$ttl)
return arr(gapX: $gapX, gapY: $gapY)static
captcha :: images ($gapX, $gapY)
line 23
Genereert een CAPTCHA-afbeelding en een puzzelstuk, en retourneert deze als base64-gecodeerde PNG-gegevens. De achtergrondafbeelding heeft een verloop en willekeurige ellipsen, terwijl het stuk een bijgesneden sectie van de achtergrond is.
$w = static::$W
$h = static::$H
$p = static::$P
$bg = imagecreatetruecolor($w, $h)
$r0 = random_int(50, 170)
$g0 = random_int(50, 170)
$b0 = random_int(50, 170)
for ($x = 0; $x < $w; $x++){
$col = imagecolorallocate($bg, (int)($r0 + $x * 0.5) % 256, (int)($g0 + $x * 0.2 + 30) % 256, (int)($b0 + ($w - $x) * 0.4) % 256)
imagefilledrectangle($bg, $x, 0, $x, $h, $col)
}
imagealphablending($bg, true)
for ($i = 0; $i < 7; $i++){
$col = imagecolorallocatealpha($bg, random_int(0, 255), random_int(0, 255), random_int(0, 255), random_int(55, 95))
imagefilledellipse($bg, random_int(0, $w), random_int(0, $h), random_int(50, 130), random_int(50, 130), $col)
}
$piece = imagecreatetruecolor($p, $p)
imagecopy($piece, $bg, 0, 0, $gapX, $gapY, $p, $p)
imagerectangle($piece, 0, 0, $p - 1, $p - 1, imagecolorallocate($piece, 245, 245, 245))
imagefilledrectangle($bg, $gapX, $gapY, $gapX + $p - 1, $gapY + $p - 1, imagecolorallocatealpha($bg, 0, 0, 0, 95))
imagerectangle($bg, $gapX, $gapY, $gapX + $p - 1, $gapY + $p - 1, imagecolorallocate($bg, 255, 255, 255))
ob_start()
imagepng($bg)
$bgData = base64_encode(ob_get_clean())
ob_start()
imagepng($piece)
$pieceData = base64_encode(ob_get_clean())
return arr(bg: 'data:image/png;base64,'.$bgData, piece: 'data:image/png;base64,'.$pieceData)static
captcha :: verify ($x, $telemetry)
line 55
Verifieert een captcha door sessiegegevens, telemetrie en verschillende voorwaarden te controleren om de geldigheid van de reactie te waarborgen.
$c = %session->captcha ?? null
if (!$c || time() > (int)($c['exp'] ?? 0)) return false
if (abs((int)round((float)$x) - (int)($c['gap'] ?? -999)) > static::$tol) return false
$t = json_decode((string)$telemetry, true)
if (!is_array($t)) return false
$d = (int)($t['d'] ?? 0)
$n = (int)($t['n'] ?? 0)
$xs = is_array($t['x'] ?? null) ? array_values($t['x']) : []
if ($d < 200 || $d > 60000) return false
if ($n < 5 || count($xs) < 4) return false
$deltas = []
for ($i = 1; $i < count($xs); $i++) $deltas[] = (float)$xs[$i] - (float)$xs[$i - 1]
if (array_sum(array_map('abs', $deltas)) < 30) return false
$mean = array_sum($deltas) / count($deltas)
$var = 0
foreach ($deltas AS $dd) $var += ($dd - $mean) ** 2
if (sqrt($var / count($deltas)) < 0.5) return false
return truestatic
captcha :: consume
line 76
Verwijdert de captcha-gegevens uit de sessie, waardoor deze effectief wordt geconsumeerd.
unset(%session->captcha)method
%captcha -> widget
line 80
Genereert een CAPTCHA-widget door een uitdaging uit te geven en een afbeelding te maken met gespecificeerde parameters.
$ch = static::issue()
$img = static::images($ch['gapX'], $ch['gapY'])
return (string)$this->field($img['bg'], $img['piece'], $ch['gapY'], static::$W, static::$H, static::$P)view
%captcha -> field ($bg, $piece, $gapY, $w, $h, $p)
line 86
Genereert een CAPTCHA-weergave die een slepen-schuifknop bevat waarmee gebruikers een puzzel kunnen voltooien, wat de beveiliging tegen geautomatiseerde inzendingen verbetert.
<div#captcha data-w="$w" data-h="$h" data-p="$p" data-gapy="$gapY">
<div.stage>
<img.bg src="{{ $bg }}" alt="">
<img.piece src="{{ $piece }}" alt="" draggable="false">
</div>
<div.track>
<div.fill></div>
<button.thumb type="button" tabindex="-1" aria-label="{{ en('Drag the slider until the piece fits') }}">››</button>
</div>
<p.hint>{en: Drag the slider until the piece snaps into place}</p>
<input type=hidden name=captcha_x>
<input type=hidden name=captcha_t>
</div>view
style
line 101
Definieert de CSS-stijlen voor de captcha-component, inclusief lay-out, kleuren en interactieve elementen.
#captcha {
margin: 14px 0 4px
max-width: 300px
}
#captcha .stage {
border-radius: 8px
line-height: 0
overflow: hidden
position: relative
touch-action: none
user-select: none
width: 100%
}
#captcha .bg {
display: block
height: auto
pointer-events: none
width: 100%
}
#captcha .piece {
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25), 0 2px 6px rgba(0, 0, 0, 0.4)
left: 6px
pointer-events: none
position: absolute
top: 0
}
#captcha .track {
background: #e7e5e4
border-radius: 8px
height: 42px
margin-top: 10px
position: relative
touch-action: none
}
#captcha .fill {
background: #fed7aa
border-radius: 8px
height: 100%
left: 0
position: absolute
top: 0
width: 0
}
#captcha .thumb {
align-items: center
appearance: none
background: #ea580c
border: 0
border-radius: 8px
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3)
color: #fff
cursor: grab
display: flex
font-family: inherit
font-size: 18px
font-weight: 700
height: 42px
justify-content: center
left: 0
margin: 0
padding: 0
position: absolute
top: 0
touch-action: none
width: 42px
}
#captcha .thumb:active: cursor: grabbing
#captcha .thumb:hover: background: #c2410c
#captcha .hint {
color: #78716c
font-size: 12px
margin: 6px 0 0
}view
script
line 177
const captcha = {
scale: 1, startCss: 6, travelMax: 0, maxThumb: 0,
dragging: false, t0: 0, samples: [],
cap: null, bg: null, piece: null, thumb: null, track: null, fill: null, xField: null, tField: null, submit: null,
grab(){
this.cap = obj('#captcha')
if (!this.cap) return false
this.bg = obj('.bg', this.cap)
this.piece = obj('.piece', this.cap)
this.thumb = obj('.thumb', this.cap)
this.track = obj('.track', this.cap)
this.fill = obj('.fill', this.cap)
this.xField = obj('[name=captcha_x]', this.cap)
this.tField = obj('[name=captcha_t]', this.cap)
this.submit = obj('#registerSubmit')
return true
},
layout(){
if (!this.grab()) return
const Wr = this.bg.clientWidth || +this.cap.dataset.w
this.scale = Wr / +this.cap.dataset.w
const pieceCss = +this.cap.dataset.p * this.scale
this.piece.style.width = pieceCss + 'px'
this.piece.style.height = pieceCss + 'px'
this.piece.style.top = (+this.cap.dataset.gapy * this.scale) + 'px'
this.startCss = 6 * this.scale
this.piece.style.left = this.startCss + 'px'
this.travelMax = Wr - pieceCss - this.startCss
this.maxThumb = this.track.clientWidth - this.thumb.clientWidth
if (this.submit) this.submit.disabled = true
},
set(tx){
tx = Math.max(0, Math.min(this.maxThumb, tx))
this.thumb.style.left = tx + 'px'
this.fill.style.width = (tx + this.thumb.clientWidth) + 'px'
const frac = this.maxThumb > 0 ? tx / this.maxThumb : 0
const pl = this.startCss + frac * (this.travelMax - this.startCss)
this.piece.style.left = pl + 'px'
return pl / this.scale
},
down(e){
if (!this.grab()) return
this.dragging = true
this.t0 = performance.now()
this.samples = []
e.preventDefault()
},
move(e){
if (!this.dragging) return
const px = e.touches ? e.touches[0].clientX : e.clientX
const rect = this.track.getBoundingClientRect()
const nx = this.set(px - rect.left - this.thumb.clientWidth / 2)
this.samples.push({x: nx, t: performance.now() - this.t0})
},
up(){
if (!this.dragging) return
this.dragging = false
const xs = this.samples.map(s => Math.round(s.x))
this.xField.value = xs.length ? xs[xs.length - 1] : 0
const step = Math.max(1, Math.ceil(xs.length / 24))
this.tField.value = JSON.stringify({d: Math.round(this.samples.length ? this.samples[this.samples.length - 1].t : 0), n: this.samples.length, x: xs.filter((_, i) => i % step === 0)})
if (this.submit) this.submit.disabled = false
}
}
onExist('#captcha', () => captcha.layout())
on('load', '#captcha .bg', () => captcha.layout())
on('resize', window, () => captcha.layout())
on('mousedown touchstart', '#captcha .thumb', (el, e) => captcha.down(e))
on('mousemove touchmove', window, (el, e) => last(captcha.move(e), false))
on('mouseup touchend', window, (el, e) => captcha.up())object
%creds
/phlo/resources/security/creds.phlo
method
%creds -> __construct (?array $values = null)
line 9
Initialiseert het creds-object, waarbij waarden worden opgelost als deze niet zijn opgegeven, en wijst elke waarde toe aan de bijbehorende eigenschap, waarbij indien nodig een nieuwe instantie van static of SensitiveParameterValue wordt gemaakt.
$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
Deze functie lost en voegt inloggegevens samen vanuit een INI-bestand en omgevingsvariabelen, en retourneert de gecombineerde gegevensarray.
$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
Laadt configuratie-instellingen uit een INI-bestand dat is opgegeven door het gegeven bestandspad en retourneert deze als een associatieve array. Als het bestand niet bestaat of niet kan worden geparsed, retourneert het een lege 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
Haalt omgevingsvariabele waarden op die beginnen met een opgegeven voorvoegsel, optioneel beperkt tot de host, en retourneert deze als een associatieve 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 $outmethod
%creds -> hostKey
line 49
Deze functie verwerkt de host van de aanvraag, zet deze om naar hoofdletters, verwijdert niet-alfanumerieke tekens en trimt eventuele leidende of volgende scheidingstekens.
$host = strtoupper(%req->host)
$host = preg_replace('/[^A-Z0-9]+/', us, $host)
return trim($host, us)method
%creds -> envAssign (array &$target, array $parts, string $value):void
line 55
Wijst een waarde toe aan een geneste arraystructuur op basis van de opgegeven delen, waarbij indien nodig tussenliggende arrays worden aangemaakt.
$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
Voegt de inhoud van de $add-array samen met de $base-array, waarbij waarden recursief worden gecombineerd als beide arrays zijn.
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
Haal de waarde op die aan de opgegeven sleutel is gekoppeld aan het object, en retourneer gevoelige waarden op een veilige manier.
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
Deze functie verwerkt elk item in objData, vervangt instanties van SensitiveParameterValue door sterren die overeenkomen met hun lengte, terwijl andere waarden ongewijzigd blijven.
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
Genereert een meta-tag met de CSRF-token voor veilige formulierindieningen.
<meta name=csrf content="$this->token">prop
%CSRF -> token
line 12
Genereert een CSRF-token van 32 tekens als er nog geen in de sessie bestaat.
%session->csrf ??= token(32)method
%CSRF -> verify
line 13
Verifieert het CSRF-token door het te vergelijken met het token dat in de HTTP-header is ontvangen.
hash_equals($this->token, (string)($_SERVER['HTTP_X_CSRF_TOKEN'] ?? void))method
%CSRF -> update
line 14
Werk de CSRF-token in de sessie bij en wijs deze toe aan de csrf-eigenschap.
arr(csrf: $this->token = %session->csrf = token(32))view
script
line 16
Stelt de CSRF-tokenwaarde in de meta-tag van het document in.
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
Maakt een JWT-instantie met een opgegeven geheim, uitgever en spelingstijd. Het zorgt ervoor dat het geheim minimaal 32 bytes lang is en geeft een foutmelding als aan deze voorwaarde niet wordt voldaan.
strlen($this->secret) >= 32 || error('JWT secret must be at least 32 bytes', 500)method
%JWT -> sign (array $claims, int $ttl = 3600):string
line 12
Genereert een JSON Web Token (JWT) door de opgegeven claims te ondertekenen met een gespecificeerde tijdsduur (TTL). De token bevat tijdstempels voor uitgifte (iat) en vervaldatum (exp), en kan optioneel een uitgever (iss) bevatten.
$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 21
Verifieert een JSON Web Token (JWT) door de structuur, handtekening en claims zoals vervaldatum en uitgever te controleren.
$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 37
Genereert een handtekening voor de gegeven body met HMAC met SHA-256 en een geheime sleutel.
$this->encode(hash_hmac('sha256', $body, $this->secret, true))method
%JWT -> encode ($data):string
line 38
Codeert de gegeven gegevens in een JSON Web Token (JWT) formaat met behulp van base64-codering.
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 39
Decodeert een JSON Web Token (JWT) van een base64-gecodeerde string, waarbij URL-veilige tekens worden vervangen door standaard base64-tekens.
(string)base64_decode(strtr($data, '-_', '+/'))object
%OAuth2
/phlo/resources/security/OAuth2.phlo
static
OAuth2 :: authorizeUrl ($endpoint, array $params)
line 10
Genereert de autorisatie-URL voor de OAuth2-stroom door queryparameters aan de eindpunt toe te voegen.
$endpoint.(str_contains($endpoint, '?') ? '&' : '?').http_build_query($params)static
OAuth2 :: token ($tokenUrl, $clientId, $clientSecret, $grantType, array $extra = [])
line 12
Haal een OAuth2-token op door een verzoek te sturen naar de opgegeven token-URL met de benodigde inloggegevens en parameters, waarbij fouten worden afgehandeld en de reactie als een associatieve array wordt geretourneerd.
$body = ['grant_type' => $grantType, 'client_id' => (string)$clientId, 'client_secret' => (string)$clientSecret]
foreach ($extra AS $key => $value){
if ($value !== null && $value !== void) $body[$key] = $value
}
try {
$res = HTTP($tokenUrl, ['Content-Type: application/x-www-form-urlencoded', 'Accept: application/json'], POST: http_build_query($body))
}
catch (\Throwable $e){
return ['error' => $e->getMessage()]
}
return json_decode((string)$res, true) ?: ['error' => 'Invalid token response']static
OAuth2 :: exchangeCode ($tokenUrl, $clientId, $clientSecret, $code, $redirectUri = null, array $extra = [])
line 26
Ruilt een autorisatiecode in voor een toegangstoken met behulp van het OAuth2-protocol.
static::token($tokenUrl, $clientId, $clientSecret, 'authorization_code', ['code' => $code, 'redirect_uri' => $redirectUri] + $extra)static
OAuth2 :: refresh ($tokenUrl, $clientId, $clientSecret, $refreshToken, array $extra = [])
line 28
Deze methode ververst een OAuth2-toegangstoken met behulp van een opgegeven verversingstoken en aanvullende parameters.
static::token($tokenUrl, $clientId, $clientSecret, 'refresh_token', ['refresh_token' => $refreshToken] + $extra)object
%rate
/phlo/resources/security/rate.phlo
static
rate :: check ($key, $limit, $windowSeconds, $storage = 'db')
line 10
Controleert de snelheidlimiet voor een gegeven sleutel door de telling in een database te verhogen en ervoor te zorgen dat deze niet boven een opgegeven limiet binnen een gedefinieerd tijdsvenster uitkomt.
if ($storage === 'apcu') return static::checkApcu($key, $limit, $windowSeconds)
$now = time()
// One atomic upsert: increment within the window or reset to a new one, and carry the
// resulting per-request count out via LAST_INSERT_ID so the check needs no separate
// read-then-write (which let parallel requests both pass the limit).
%MySQL->query('INSERT INTO rate_limit (rkey, count, window_start) VALUES (?, LAST_INSERT_ID(1), ?) ON DUPLICATE KEY UPDATE count = LAST_INSERT_ID(IF(? - window_start < ?, count + 1, 1)), window_start = IF(? - window_start < ?, window_start, ?)', $key, $now, $now, $windowSeconds, $now, $windowSeconds, $now)
return (int)%MySQL->query('SELECT LAST_INSERT_ID()')->fetchColumn() <= $limitstatic
rate :: checkApcu ($key, $limit, $windowSeconds)
line 20
Controleert of de snelheidslimiet voor een specifieke sleutel is overschreden met behulp van APCu-caching.
$window = (int)floor(time() / $windowSeconds) * $windowSeconds
$apcuKey = 'phlo.rate.'.$key.':'.$window
apcu_add($apcuKey, 0, $windowSeconds)
return apcu_inc($apcuKey, 1) <= $limitstatic
rate :: status ($key, $limit, $windowSeconds)
line 27
Haal de huidige status van de rate limit op, inclusief het aantal gebruikte verzoeken, de limiet en de tijd tot de limiet opnieuw instelt.
$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))static
rate :: reset ($key)
line 34
Deze methode reset de rate limit door de bijbehorende invoer uit de rate_limit-tabel in de MySQL-database te verwijderen.
%MySQL->query('DELETE FROM rate_limit WHERE rkey=?', $key)static
rate :: purge ($olderThanSeconds = 604800)
line 35
Verwijdert vermeldingen uit de rate_limit-tabel waar window_start ouder is dan de opgegeven tijd in seconden.
%MySQL->query('DELETE FROM rate_limit WHERE window_start < ?', time() - $olderThanSeconds)object
%security
/phlo/resources/security/security.phlo
prop
%security -> whitelist
line 10
Definieert een whitelist voor beveiligingsdoeleinden, waardoor alleen gespecificeerde vermeldingen zijn toegestaan.
[]method
%security -> setNonce
line 12
Stelt een nonce-waarde in voor de applicatie met behulp van een gegenereerde token van de opgegeven lengte.
%app->nonce = token(8)method
%security -> frameProtect ($mode = 'DENY')
line 14
Stelt de X-Frame-Options-header in om te bepalen of de pagina in een frame kan worden weergegeven, met de standaardmodus 'DENY'.
%res->header('X-Frame-Options', $mode)method
%security -> frameWhitelist
line 15
Genereert een string van goedgekeurde frame-oorsprongen voor beveiligingsdoeleinden, waardoor alleen opgegeven domeinen de inhoud in een iframe kunnen insluiten.
$this->whitelist ? ' '.implode(space, array_map(fn($d) => "https://*.$d", (array)$this->whitelist)) : voidmethod
%security -> strict
line 17
Stelt strikte beveiligingsheaders in voor HTTP-responses, waaronder Cache-Control en Content-Security-Policy, om de beveiliging tegen verschillende aanvallen te verbeteren.
$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'; worker-src 'self'; 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 26
Stelt basisbeveiligingsheaders in voor de respons, inclusief een Content-Security-Policy die het laden van bronnen beperkt tot dezelfde oorsprong en toegestane bronnen voor scripts, stijlen, afbeeldingen, lettertypen en verbindingen specificeert.
$this->base
if (%req->async) return
%res->header('Content-Security-Policy', "default-src 'self'; script-src 'self'".(debug ? " 'unsafe-inline'" : void)."; 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 31
Stelt de headers voor het Content Security Policy in voor de respons, waarbij toegestane bronnen voor verschillende contenttypes worden gedefinieerd op basis van de async-status van het verzoek en de debugmodus.
$this->base
if (%req->async) return
%res->header('Content-Security-Policy', "default-src 'self'; script-src 'self'".(debug ? " 'unsafe-inline'" : void)."; 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 36
Stelt beveiligingsgerelateerde HTTP-headers in voor de API-respons, waardoor de bescherming tegen verschillende webkwetsbaarheden wordt verbeterd.
%res->api = true
%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 43
Stelt verschillende beveiligingsgerelateerde HTTP-headers in voor de respons, waaronder Referrer-Policy, X-Content-Type-Options, Cross-Origin-beleid en X-Frame-Options, om de beveiliging tegen cross-origin aanvallen te verbeteren.
%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')object
%social
/phlo/resources/security/social.phlo
static
social :: providers
line 11
arr(
google: arr(
label: 'Google',
authorize: 'https://accounts.google.com/o/oauth2/v2/auth',
token: 'https://oauth2.googleapis.com/token',
jwks: 'https://www.googleapis.com/oauth2/v3/certs',
scope: 'openid email profile',
issuer: 'https://accounts.google.com',
),
microsoft: arr(
label: 'Microsoft',
authorize: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
token: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
jwks: 'https://login.microsoftonline.com/common/discovery/v2.0/keys',
scope: 'openid email profile',
issuer: 'https://login.microsoftonline.com/',
),
apple: arr(
label: 'Apple',
authorize: 'https://appleid.apple.com/auth/authorize',
token: 'https://appleid.apple.com/auth/token',
jwks: 'https://appleid.apple.com/auth/keys',
scope: 'name email',
issuer: 'https://appleid.apple.com',
),
)static
social :: config ($provider)
line 38
$p = static::providers()[$provider] ?? null
if (!$p) return null
$section = %creds->{$provider} ?? null
$creds = is_object($section) ? (array)$section->toArray : []
return $p + arr(
client_id: (string)($creds['client_id'] ?? void),
client_secret: (string)($creds['client_secret'] ?? void),
redirect_uri: (string)($creds['redirect_uri'] ?? ('https://'.%req->host.'/auth/'.$provider.'/callback')),
)static
social :: configured ($provider)
line 50
Controleert of de sociale configuratie voor een gegeven provider is ingesteld en of de client-ID niet leeg is.
$c = static::config($provider)
return $c && $c['client_id'] !== voidstatic
social :: authUrl ($provider, $state, $nonce = void)
line 55
$c = static::config($provider)
$params = arr(
client_id: $c['client_id'],
redirect_uri: $c['redirect_uri'],
response_type: 'code',
scope: $c['scope'],
state: $state,
)
if ($nonce !== void) $params['nonce'] = $nonce
if ($provider === 'apple') $params['response_mode'] = 'form_post'
return OAuth2::authorizeUrl($c['authorize'], $params)static
social :: profile ($provider, $code, $nonce = void)
line 69
$cfg = static::config($provider)
if (!$cfg || $cfg['client_id'] === void) return null
$secret = $provider === 'apple' ? static::appleSecret() : $cfg['client_secret']
$token = OAuth2::exchangeCode($cfg['token'], $cfg['client_id'], $secret, $code, $cfg['redirect_uri'])
$jwt = (string)($token['id_token'] ?? void)
if (!static::verifySignature($provider, $jwt)) return null
$claims = static::decodeIdToken($jwt)
if (!$claims || !static::verifyClaims($provider, $cfg, $claims, $nonce)) return null
$profile = static::normalize($provider, $claims)
if ($provider === 'apple' && !$profile['name']){
$u = json_decode((string)(%payload->user ?? void), true)
if (is_array($u) && !empty($u['name'])) $profile['name'] = trim(((string)($u['name']['firstName'] ?? void)).space.((string)($u['name']['lastName'] ?? void)))
}
return $profilestatic
social :: decodeIdToken ($jwt)
line 86
$parts = explode(dot, (string)$jwt)
if (count($parts) < 2) return null
$claims = json_decode(static::b64urlDecode($parts[1]), true)
return is_array($claims) ? $claims : nullstatic
social :: algs
line 93
arr(RS256: OPENSSL_ALGO_SHA256)static
social :: verifySignature ($provider, $jwt)
line 95
$parts = explode(dot, (string)$jwt)
if (count($parts) !== 3) return false
$header = json_decode(static::b64urlDecode($parts[0]), true)
if (!is_array($header)) return false
$alg = static::algs()[(string)($header['alg'] ?? void)] ?? null
$kid = (string)($header['kid'] ?? void)
if (!$alg || !$kid) return false
$pem = static::key($provider, $kid)
if (!$pem) return false
return openssl_verify($parts[0].dot.$parts[1], static::b64urlDecode($parts[2]), $pem, $alg) === 1static
social :: jwks ($provider)
line 108
$cached = %req->socialJwks ?? []
if (isset($cached[$provider])) return $cached[$provider]
$url = (string)(static::providers()[$provider]['jwks'] ?? void)
$keys = []
if ($url){
$data = json_decode((string)HTTP($url, ['Accept: application/json']), true)
if (is_array($data) && is_array($data['keys'] ?? null)) $keys = $data['keys']
}
$cached[$provider] = $keys
%req->socialJwks = $cached
return $keysstatic
social :: key ($provider, $kid)
line 122
foreach (static::jwks($provider) AS $jwk){
if ((string)($jwk['kid'] ?? void) === $kid) return static::jwkToPem((array)$jwk)
}
return voidstatic
social :: jwkToPem (array $jwk)
line 129
if ((string)($jwk['kty'] ?? void) !== 'RSA') return void
$n = static::b64urlDecode((string)($jwk['n'] ?? void))
$e = static::b64urlDecode((string)($jwk['e'] ?? void))
if (!$n || !$e) return void
$key = static::der(0x30, static::derInt($n).static::derInt($e))
$bits = static::der(0x03, "\x00".$key)
$algo = static::der(0x30, "\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00")
$spki = static::der(0x30, $algo.$bits)
return '-----BEGIN PUBLIC KEY-----'.lf.chunk_split(base64_encode($spki), 64, lf).'-----END PUBLIC KEY-----'.lfstatic
social :: der ($tag, $content)
line 141
$len = strlen($content)
if ($len < 0x80) return chr($tag).chr($len).$content
$bytes = ltrim(pack('N', $len), "\x00")
return chr($tag).chr(0x80 | strlen($bytes)).$bytes.$contentstatic
social :: derInt ($bytes)
line 148
$bytes = ltrim((string)$bytes, "\x00")
if ($bytes === void) $bytes = "\x00"
if (ord($bytes[0]) & 0x80) $bytes = "\x00".$bytes
return static::der(0x02, $bytes)static
social :: b64urlDecode ($data)
line 155
$data = strtr((string)$data, '-_', '+/')
$pad = strlen($data) % 4
if ($pad) $data .= str_repeat(eq, 4 - $pad)
return (string)base64_decode($data)static
social :: verifyClaims ($provider, array $cfg, array $claims, $nonce = void)
line 162
if ((int)($claims['exp'] ?? 0) <= time()) return false
$aud = $claims['aud'] ?? void
if (!in_array((string)($cfg['client_id'] ?? void), array_map('strval', is_array($aud) ? $aud : [$aud]), true)) return false
if ($nonce !== void && !hash_equals((string)$nonce, (string)($claims['nonce'] ?? void))) return false
return static::verifyIssuer($provider, $claims)static
social :: verifyIssuer ($provider, array $claims)
line 170
$iss = (string)($claims['iss'] ?? void)
if ($provider === 'microsoft'){
$tid = (string)($claims['tid'] ?? void)
return $tid !== void && $iss === 'https://login.microsoftonline.com/'.$tid.'/v2.0'
}
$expected = (string)(static::providers()[$provider]['issuer'] ?? void)
if ($expected === void) return false
return $iss === $expected || ($provider === 'google' && $iss === 'accounts.google.com')static
social :: normalize ($provider, array $claims)
line 181
$ev = $claims['email_verified'] ?? null
$verified = $ev === true || $ev === 'true'
if ($provider === 'microsoft'){
$edov = $claims['xms_edov'] ?? null
$verified = $edov === true || $edov === 'true' || $edov === 1 || $edov === '1'
}
$name = (string)($claims['name'] ?? void)
if (!$name) $name = trim(((string)($claims['given_name'] ?? void)).space.((string)($claims['family_name'] ?? void)))
return arr(
provider: $provider,
uid: (string)($claims['sub'] ?? void),
email: strtolower(trim((string)($claims['email'] ?? void))),
verified: $verified,
name: trim($name),
)static
social :: b64url ($data)
line 199
Codeert de gegeven data in een base64 URL-veilige indeling door '+' te vervangen door '-', '/' door '_' en eventuele trailing gelijktekens te verwijderen.
rtrim(strtr(base64_encode((string)$data), '+/', '-_'), eq)static
social :: appleSecret
line 201
$section = %creds->apple ?? null
$creds = is_object($section) ? (array)$section->toArray : []
$teamId = (string)($creds['team_id'] ?? void)
$keyId = (string)($creds['key_id'] ?? void)
$clientId = (string)($creds['client_id'] ?? void)
$keyData = (string)($creds['private_key'] ?? void)
if (!$keyData && !empty($creds['key_file']) && is_file((string)$creds['key_file'])) $keyData = (string)file_get_contents((string)$creds['key_file'])
if (!$teamId || !$keyId || !$clientId || !$keyData) return void
$key = openssl_pkey_get_private($keyData)
if (!$key) return void
$now = time()
$input = static::b64url(json_encode(arr(alg: 'ES256', kid: $keyId))).dot.static::b64url(json_encode(arr(iss: $teamId, iat: $now, exp: $now + 15552000, aud: 'https://appleid.apple.com', sub: $clientId)))
$der = void
if (!openssl_sign($input, $der, $key, OPENSSL_ALGO_SHA256)) return void
return $input.dot.static::b64url(static::derToJose($der))static
social :: derToJose ($der)
line 219
Extraheert de 'r' en 's' waarden uit een DER-gecodeerde handtekening en vult deze aan tot 32 bytes.
$rlen = ord($der[3])
$r = substr((string)$der, 4, $rlen)
$slen = ord($der[4 + $rlen + 1])
$s = substr((string)$der, 4 + $rlen + 2, $slen)
return static::pad32($r).static::pad32($s)static
social :: pad32 ($x)
line 227
Deze methode voegt null-bytes aan de linkerkant van een string toe om ervoor te zorgen dat deze 32 tekens lang is, waarbij eventuele leidende null-bytes voor het opvullen worden verwijderd.
$x = ltrim((string)$x, "\x00")
return str_pad($x, 32, "\x00", STR_PAD_LEFT)Functions
function
decrypt($encrypted, $key):string|false
/phlo/resources/security/encryption.phlo line 11
Decrypt de gegeven base64-gecodeerde versleutelde string met de opgegeven sleutel en retourneer de oorspronkelijke gegevens of false als de decryptie mislukt.
($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 9
Versleutelt de gegeven data met een geheime sleutel en retourneert het base64-gecodeerde resultaat.
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 9
Genereert een willekeurig token van de opgegeven lengte met een gedefinieerd alfabet, optioneel gezaaid met een invoerstring voor extra willekeurigheid.
$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