idk
This commit is contained in:
1 parent
e68d357177
commit
3e3382df57
65 files changed
+1836
-42
No files matched your search
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
// ─── HTML sanitisation ───────────────────────────────────────────────────────
|
||||
// Admin-authored rich text (Summernote) and the Google-Maps embed are rendered
|
||||
// raw on the public site. To keep that from being a stored-XSS vector if an
|
||||
// admin/staff account is ever misused, every such value is passed through this
|
||||
// allowlist sanitiser at OUTPUT time (covers legacy rows too, not just new saves).
|
||||
//
|
||||
// Dependency-free: uses ext-dom (libxml). Falls back to a strip_tags allowlist
|
||||
// if DOM is unavailable. Disallowed elements are dropped wholesale; on every
|
||||
// surviving element only whitelisted attributes remain, and href/src schemes
|
||||
// are validated (no javascript:/data:/vbscript:).
|
||||
//
|
||||
// clean_richtext($html) — modalidades.descricao, events.description, …
|
||||
// clean_map_embed($html) — fitme3070_info.google_maps (single Google iframe)
|
||||
|
||||
// Tags allowed in general rich text (no attributes unless listed).
|
||||
const HTML_RICHTEXT_TAGS = [
|
||||
'p' => [], 'br' => [], 'hr' => [], 'div' => [], 'span' => [],
|
||||
'strong' => [], 'b' => [], 'em' => [], 'i' => [], 'u' => [], 's' => [],
|
||||
'ul' => [], 'ol' => [], 'li' => [],
|
||||
'h2' => [], 'h3' => [], 'h4' => [], 'h5' => [],
|
||||
'blockquote' => [], 'a' => ['href', 'target', 'rel'],
|
||||
];
|
||||
|
||||
// Elements removed entirely (content and all) when they appear anywhere.
|
||||
const HTML_DANGEROUS_TAGS = [
|
||||
'script', 'style', 'iframe', 'object', 'embed', 'form', 'input', 'button',
|
||||
'textarea', 'select', 'option', 'link', 'meta', 'base', 'svg', 'math',
|
||||
'audio', 'video', 'source', 'frame', 'frameset', 'applet',
|
||||
];
|
||||
|
||||
function clean_richtext(?string $html): string {
|
||||
return html_sanitize((string) $html, HTML_RICHTEXT_TAGS);
|
||||
}
|
||||
|
||||
// The maps field holds a single Google Maps <iframe> embed. Allow exactly that.
|
||||
function clean_map_embed(?string $html): string {
|
||||
$allowed = [
|
||||
'iframe' => ['src', 'width', 'height', 'style', 'loading',
|
||||
'allowfullscreen', 'referrerpolicy', 'title'],
|
||||
];
|
||||
return html_sanitize((string) $html, $allowed, true);
|
||||
}
|
||||
|
||||
// Core sanitiser. $allowed maps tag => list of permitted attributes.
|
||||
// $allowMapsIframe gates iframe src to Google-Maps embed URLs only.
|
||||
function html_sanitize(string $html, array $allowed, bool $allowMapsIframe = false): string {
|
||||
$html = trim($html);
|
||||
if ($html === '') return '';
|
||||
|
||||
if (!class_exists('DOMDocument')) {
|
||||
// Conservative fallback: keep a safe inline tag set, drop ALL attributes
|
||||
// (strip_tags removes attributes with the tags it keeps's contents intact).
|
||||
return strip_tags($html, '<p><br><strong><b><em><i><u><ul><ol><li><h2><h3><h4><blockquote><span><a>');
|
||||
}
|
||||
|
||||
$doc = new DOMDocument();
|
||||
$prev = libxml_use_internal_errors(true);
|
||||
// The XML encoding hint forces UTF-8; NOIMPLIED/NODEFDTD keep it a fragment
|
||||
// (no <html>/<body>/doctype wrappers added).
|
||||
$doc->loadHTML(
|
||||
'<?xml encoding="utf-8"?><div id="__root__">' . $html . '</div>',
|
||||
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NONET
|
||||
);
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($prev);
|
||||
|
||||
$root = $doc->getElementById('__root__');
|
||||
if (!$root) {
|
||||
// getElementById can miss without a DTD; fall back to first div.
|
||||
$root = $doc->getElementsByTagName('div')->item(0);
|
||||
}
|
||||
if (!$root) return '';
|
||||
|
||||
_html_sanitize_children($root, $allowed, $allowMapsIframe);
|
||||
|
||||
$out = '';
|
||||
foreach (iterator_to_array($root->childNodes) as $child) {
|
||||
$out .= $doc->saveHTML($child);
|
||||
}
|
||||
return trim($out);
|
||||
}
|
||||
|
||||
// Recursively scrub a node's children in place.
|
||||
function _html_sanitize_children(DOMNode $node, array $allowed, bool $allowMapsIframe): void {
|
||||
foreach (iterator_to_array($node->childNodes) as $child) {
|
||||
if ($child->nodeType === XML_COMMENT_NODE) {
|
||||
$node->removeChild($child);
|
||||
continue;
|
||||
}
|
||||
if ($child->nodeType !== XML_ELEMENT_NODE) {
|
||||
continue; // plain text — safe (serialised escaped)
|
||||
}
|
||||
|
||||
$tag = strtolower($child->nodeName);
|
||||
|
||||
// Disallowed → remove the element and everything inside it.
|
||||
if (!isset($allowed[$tag])) {
|
||||
$node->removeChild($child);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip every attribute that isn't explicitly permitted for this tag,
|
||||
// and validate URL-bearing ones.
|
||||
if ($child->hasAttributes()) {
|
||||
foreach (iterator_to_array($child->attributes) as $attr) {
|
||||
$name = strtolower($attr->name);
|
||||
if (!in_array($name, $allowed[$tag], true)) {
|
||||
$child->removeAttribute($attr->name); // drops on*, style (rich text), etc.
|
||||
continue;
|
||||
}
|
||||
if (($name === 'href' || $name === 'src')
|
||||
&& !_html_url_is_safe($attr->value, $tag === 'iframe' && $allowMapsIframe)) {
|
||||
$child->removeAttribute($attr->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Harden links that open in a new tab.
|
||||
if ($tag === 'a' && strtolower($child->getAttribute('target')) === '_blank') {
|
||||
$child->setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
|
||||
_html_sanitize_children($child, $allowed, $allowMapsIframe);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow http(s)/mailto/tel/anchors/relative; for iframes, only Google-Maps
|
||||
// embeds. Always reject javascript:/data:/vbscript: and other schemes.
|
||||
function _html_url_is_safe(string $url, bool $isMapsIframe): bool {
|
||||
$url = trim($url);
|
||||
if ($url === '') return false;
|
||||
if (preg_match('/^\s*(javascript|data|vbscript|file):/i', $url)) return false;
|
||||
|
||||
if ($isMapsIframe) {
|
||||
// Any Google-owned Maps URL (www.google.com/maps/embed, maps.google.pt/…).
|
||||
return (bool) preg_match('#^https://([a-z0-9-]+\.)*google\.[a-z.]+/maps#i', $url);
|
||||
}
|
||||
if (preg_match('#^(https?:|mailto:|tel:|/|\#|\?|\.)#i', $url)) return true;
|
||||
// No scheme at all → relative path (e.g. "home/contactos") → safe.
|
||||
return !preg_match('#^[a-z][a-z0-9+.\-]*:#i', $url);
|
||||
}
|
||||
Reference in new issue
Block a user