Refactoring de la mise en sécurité des textes dans ecrire et public #5271

Merged
marcimat merged 15 commits from refactor_texte_safety into master 1 month ago
  1. 2
      CHANGELOG.md
  2. 206
      ecrire/inc/lien.php
  3. 262
      ecrire/inc/modeles.php
  4. 11
      ecrire/inc/texte.php
  5. 141
      ecrire/inc/texte_mini.php
  6. 1
      prive/modeles/dist.html

2
CHANGELOG.md

@ -8,10 +8,12 @@
### Added
- #5271 Fonction `is_html_safe()`
- #4877 La balise `#TRI` permet d'alterner le sens du critère `tri`
### Changed
- #5271 Refactoring de la mise en sécurité des textes dans ecrire et public
- #5189 Ne plus forcer l'Engine MySQL à l'installation
- #5273 Le critère `par_ordre_liste` rejette à la fin les éléments de la boucle absents de la liste
- #5016 Généralisation du traitement des balises dynamiques dans un modèle

206
ecrire/inc/lien.php

@ -15,13 +15,26 @@ if (!defined('_ECRIRE_INC_VERSION')) {
}
include_spip('base/abstract_sql');
include_spip('inc/modeles');
//
// Production de la balise A+href a partir des raccourcis [xxx->url] etc.
// Note : complique car c'est ici qu'on applique typo(),
// et en plus on veut pouvoir les passer en pipeline
//
/**
* Production de la balise a+href à partir des raccourcis `[xxx->url]` etc.
*
* @note
* Compliqué car c'est ici qu'on applique typo(),
* et en plus, on veut pouvoir les passer en pipeline
*
* @see typo()
* @param string $lien
* @param string $texte
* @param string $class
* @param string $title
* @param string $hlang
* @param string $rel
* @param string $connect
* @param array $env
* @return string
*/
function inc_lien_dist(
$lien,
$texte = '',
@ -151,43 +164,73 @@ function liens_implicite_glose_dist($texte, $id, $type, $args, $ancre, string $c
return $url;
}
function traiter_lien_implicite($ref, $texte = '', $pour = 'url', string $connect = '') {
$url = null;
/**
* Transformer un lien raccourci art23 en son URL
* Par defaut la fonction produit une url prive si on est dans le prive
* ou publique si on est dans le public.
* La globale lien_implicite_cible_public permet de forcer un cas ou l'autre :
* $GLOBALS['lien_implicite_cible_public'] = true;
* => tous les liens raccourcis pointent vers le public
* $GLOBALS['lien_implicite_cible_public'] = false;
* => tous les liens raccourcis pointent vers le prive
* unset($GLOBALS['lien_implicite_cible_public']);
* => retablit le comportement automatique
*
* @param string $ref
* @param string $texte
* @param string $pour
* @param string $connect
* @return array|bool|string
*/
function traiter_lien_implicite($ref, $texte = '', $pour = 'url', $connect = '') {
$cible = $GLOBALS['lien_implicite_cible_public'] ?? null;
if (!($match = typer_raccourci($ref))) {
return false;
}
[$type, , $id, , $args, , $ancre] = array_pad($match, 7, null);
// attention dans le cas des sites le lien doit pointer non pas sur
// la page locale du site, mais directement sur le site lui-meme
# attention dans le cas des sites le lien doit pointer non pas sur
# la page locale du site, mais directement sur le site lui-meme
$url = '';
if ($f = charger_fonction("implicite_$type", 'liens', true)) {
$url = $f($texte, $id, $type, $args, $ancre, $connect);
}
if (!$url) {
$url = generer_objet_url($id, $type, $args ?? '', $ancre ?? '', null, '', $connect);
$url = generer_objet_url($id, $type, $args ?? '', $ancre ?? '', $cible, '', $connect ?? '');
}
if (!$url) {
return false;
}
if (is_array($url)) {
[$type, $id] = array_pad($url, 2, null);
$url = generer_objet_url($id, $type, $args ?? '', $ancre ?? '', null, '', $connect);
$url = generer_objet_url($id, $type, $args ?? '', $ancre ?? '', $cible, '', $connect ?? '');
}
if ($pour === 'url') {
return $url;
}
$r = traiter_raccourci_titre($id, $type, $connect);
if ($r) {
$r['class'] = ($type == 'site') ? 'spip_out' : 'spip_in';
}
if ($texte = trim($texte)) {
$r['titre'] = $texte;
}
if (!@$r['titre']) {
$r['titre'] = _T($type) . " $id";
}
if ($pour == 'titre') {
return $r['titre'];
}
$r['url'] = $url;
// dans le cas d'un lien vers un doc, ajouter le type='mime/type'
@ -218,6 +261,7 @@ function typer_raccourci($lien) {
if (!preg_match(_RACCOURCI_URL, $lien, $match)) {
return [];
}
$f = $match[1];
// valeur par defaut et alias historiques
if (!$f) {
@ -239,14 +283,15 @@ function typer_raccourci($lien) {
$f = 'document';
} else {
if (preg_match('/^br..?ve$/S', $f)) {
$f = 'breve';
$f = 'breve'; # accents :(
}
}
}
}
}
}
} # accents :(
}
$match[0] = $f;
return $match;
@ -266,18 +311,24 @@ function typer_raccourci($lien) {
function traiter_raccourci_titre($id, $type, $connect = null) {
$trouver_table = charger_fonction('trouver_table', 'base');
$desc = $trouver_table(table_objet($type));
if (!($desc and $s = $desc['titre'])) {
return [];
}
$_id = $desc['key']['PRIMARY KEY'];
$r = sql_fetsel($s, $desc['table'], "$_id=$id", '', '', '', '', $connect);
if (!$r) {
return [];
}
$r['titre'] = supprimer_numero($r['titre']);
if (!$r['titre'] and !empty($r['surnom'])) {
$r['titre'] = $r['surnom'];
}
if (!isset($r['lang'])) {
$r['lang'] = '';
}
@ -285,133 +336,6 @@ function traiter_raccourci_titre($id, $type, $connect = null) {
return $r;
}
// traite les modeles (dans la fonction typo), en remplacant
// le raccourci <modeleN|parametres> par la page calculee a
// partir du squelette modeles/modele.html
// Le nom du modele doit faire au moins trois caracteres (evite <h2>)
// Si $doublons==true, on repere les documents sans calculer les modeles
// mais on renvoie les params (pour l'indexation par le moteur de recherche)
define(
'_RACCOURCI_MODELE',
'(<([a-z_-]{3,})' # <modele
. '\s*([0-9]*)\s*' # id
. '([|](?:<[^<>]*>|[^>])*?)?' # |arguments (y compris des tags <...>)
. '\s*/?' . '>)' # fin du modele >
. '\s*(<\/a>)?' # eventuel </a>
);
define('_RACCOURCI_MODELE_DEBUT', '@^' . _RACCOURCI_MODELE . '@isS');
function traiter_modeles($texte, $doublons = false, $echap = '', string $connect = '', $liens = null, $env = []) {
// preserver la compatibilite : true = recherche des documents
if ($doublons === true) {
$doublons = ['documents' => ['doc', 'emb', 'img']];
}
// detecter les modeles (rapide)
if (
strpos($texte, '<') !== false and
preg_match_all('/<[a-z_-]{3,}\s*[0-9|]+/iS', $texte, $matches, PREG_SET_ORDER)
) {
include_spip('public/assembler');
$wrap_embed_html = charger_fonction('wrap_embed_html', 'inc', true);
foreach ($matches as $match) {
// Recuperer l'appel complet (y compris un eventuel lien)
$a = strpos($texte, (string) $match[0]);
preg_match(
_RACCOURCI_MODELE_DEBUT,
substr($texte, $a),
$regs
);
$regs[] = ''; // s'assurer qu'il y a toujours un 5e arg, eventuellement vide
[, $mod, $type, $id, $params, $fin] = $regs;
if (
$fin and
preg_match(
'/<a\s[^<>]*>\s*$/i',
substr($texte, 0, $a),
$r
)
) {
$lien = [
'href' => extraire_attribut($r[0], 'href'),
'class' => extraire_attribut($r[0], 'class'),
'mime' => extraire_attribut($r[0], 'type'),
'title' => extraire_attribut($r[0], 'title'),
'hreflang' => extraire_attribut($r[0], 'hreflang')
];
$n = strlen($r[0]);
$a -= $n;
$cherche = $n + strlen($regs[0]);
} else {
$lien = false;
$cherche = strlen($mod);
}
// calculer le modele
# hack indexation
if ($doublons) {
$texte .= preg_replace(',[|][^|=]*,s', ' ', $params);
} # version normale
else {
// si un tableau de liens a ete passe, reinjecter le contenu d'origine
// dans les parametres, plutot que les liens echappes
if (!is_null($liens)) {
$params = str_replace($liens[0], $liens[1], $params);
}
$modele = inclure_modele($type, $id, $params, $lien, $connect, $env);
// en cas d'echec,
// si l'objet demande a une url,
// creer un petit encadre vers elle
if ($modele === false) {
if (!$lien) {
$lien = traiter_lien_implicite("$type$id", '', 'tout', $connect);
}
if ($lien) {
$modele = '<a href="'
. $lien['url']
. '" class="spip_modele'
. '">'
. sinon($lien['titre'], _T('ecrire:info_sans_titre'))
. '</a>';
} else {
$modele = '';
if (test_espace_prive()) {
$modele = entites_html(substr($texte, $a, $cherche));
if (!is_null($liens)) {
$modele = '<pre>' . str_replace($liens[0], $liens[1], $modele) . '</pre>';
}
}
}
}
// le remplacer dans le texte
if ($modele !== false) {
$modele = protege_js_modeles($modele);
if ($wrap_embed_html) {
$modele = $wrap_embed_html($mod, $modele);
}
$rempl = code_echappement($modele, $echap);
$texte = substr($texte, 0, $a)
. $rempl
. substr($texte, $a + $cherche);
}
}
// hack pour tout l'espace prive
if (((!_DIR_RESTREINT) or ($doublons)) and ($id)) {
foreach ($doublons ?: ['documents' => ['doc', 'emb', 'img']] as $quoi => $modeles) {
if (in_array($type, $modeles)) {
$GLOBALS["doublons_{$quoi}_inclus"][] = $id;
}
}
}
}
}
return $texte;
}
//
// Raccourcis ancre [#ancre<-]
//

262
ecrire/inc/modeles.php

@ -0,0 +1,262 @@
<?php
/***************************************************************************\
* SPIP, Système de publication pour l'internet *
* *
* Copyright © avec tendresse depuis 2001 *
* Arnaud Martin, Antoine Pitrou, Philippe Rivière, Emmanuel Saint-James *
* *
* Ce programme est un logiciel libre distribué sous licence GNU/GPL. *
* Pour plus de détails voir le fichier COPYING.txt ou l'aide en ligne. *
\***************************************************************************/
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
// traite les modeles (dans la fonction typo), en remplacant
// le raccourci <modeleN|parametres> par la page calculee a
// partir du squelette modeles/modele.html
// Le nom du modele doit faire au moins trois caracteres (evite <h2>)
// Si $doublons==true, on repere les documents sans calculer les modeles
// mais on renvoie les params (pour l'indexation par le moteur de recherche)
define(
'_PREG_MODELE',
'(<([a-z_-]{3,})' # <modele
. '\s*([0-9]*)\s*' # id
. '([|](?:<[^<>]*>|[^>])*?)?' # |arguments (y compris des tags <...>)
. '\s*/?' . '>)' # fin du modele >
);
define(
'_RACCOURCI_MODELE',
_PREG_MODELE
. '\s*(<\/a>)?' # eventuel </a>
);
define('_RACCOURCI_MODELE_DEBUT', '@^' . _RACCOURCI_MODELE . '@isS');
/**
* Detecter et collecter les modeles d'un texte dans un tableau descriptif
* qui pourra servir a leurs traitements ou echappement selon le besoin
* @param string $texte
* @param bool $collecter_liens
* @return array
*/
function modeles_collecter($texte, bool $collecter_liens = true) {
$modeles = [];
// detecter les modeles (rapide)
if (
strpos($texte, '<') !== false
and preg_match_all('/<[a-z_-]{3,}\s*[0-9|]+/iS', $texte, $matches, PREG_SET_ORDER)
) {
$pos = 0;
// Recuperer l'appel complet (y compris un eventuel lien)
foreach ($matches as $match) {
$a = strpos($texte, (string)$match[0], $pos);
if (preg_match(_RACCOURCI_MODELE_DEBUT, substr($texte, $a), $regs)) {
// s'assurer qu'il y a toujours un 5e arg, eventuellement vide
while (count($regs) < 6) {
$regs[] = '';
}
[, $mod, $type, $id, $params, $fermeture_lien] = $regs;
if (
$collecter_liens
and $fermeture_lien
and $before = substr($texte, $pos, $a - $pos)
and stripos($before, '<a') !== false
and preg_match('/<a\s[^<>]*>\s*$/i', $before, $r)
) {
$lien = [
'href' => extraire_attribut($r[0], 'href'),
'class' => extraire_attribut($r[0], 'class'),
'mime' => extraire_attribut($r[0], 'type'),
'title' => extraire_attribut($r[0], 'title'),
'hreflang' => extraire_attribut($r[0], 'hreflang')
];
$n = strlen($r[0]);
$a -= $n;
$longueur = $n + strlen($regs[0]);
} else {
if ($fermeture_lien) {
$mod = rtrim(substr($mod, 0, -strlen($fermeture_lien)));
}
$lien = false;
$longueur = strlen($mod);
}
$modele = [
'modele' => $mod,
'pos' => $a,
'length' => $longueur,
'type' => $type,
'id' => $id,
'params' => $params,
'lien' => $lien,
];
$modeles[] = $modele;
}
$pos = $a + strlen((string)$match[0]);
}
}
return $modeles;
}
/**
* Echapper les raccourcis modeles pour ne pas les casser via safehtml par exemple
*
* @see modele_retablir_raccourcis_echappes()
* @param string $texte
* @param bool $collecter_liens
* @return array
* texte, marqueur utilise pour echapper les modeles
*/
function modeles_echapper_raccourcis($texte, bool $collecter_liens = false) {
$modeles = modeles_collecter($texte, $collecter_liens);
$markid = '';
if (!empty($modeles)) {
// generer un marqueur qui n'est pas dans le texte
do {
$markid = substr(md5(creer_uniqid()), 0, 7);
$markid = "@|MODELE$markid|";
} while (strpos($texte, $markid) !== false);
$offset_pos = 0;
foreach ($modeles as $m) {
$rempl = $markid . base64_encode($m['modele']) . '|@';
$texte = substr_replace($texte, $rempl, $m['pos'] + $offset_pos, $m['length']);
$offset_pos += strlen($rempl) - $m['length'];
}
}
return [$texte, $markid];
}
/**
* Retablir les modeles echappes par la fonction modeles_echapper_raccourcis()
*
* @see modeles_echapper_raccourcis()
* @param string $texte
* @param string $markid
* @return string
*/
function modele_retablir_raccourcis_echappes(string $texte, string $markid) {
if ($markid) {
$pos = 0;
while (
($p = strpos($texte, $markid, $pos)) !== false
and $end = strpos($texte, '|@', $p + 16)
) {
$base64 = substr($texte, $p + 16, $end - ($p + 16));
if ($modele = base64_decode($base64, true)) {
$texte = substr_replace($texte, $modele, $p, $end + 2 - $p);
$pos = $p + strlen($modele);
}
else {
$pos = $end;
}
}
}
return $texte;
}
/**
* Traiter les modeles d'un texte
* @param string $texte
* @param bool|array $doublons
* @param string $echap
* @param string $connect
* @param ?array $liens
* @param array $env
* @return string
*/
function traiter_modeles($texte, $doublons = false, $echap = '', string $connect = '', $liens = null, $env = []) {
// preserver la compatibilite : true = recherche des documents
if ($doublons === true) {
$doublons = ['documents' => ['doc', 'emb', 'img']];
}
$modeles = modeles_collecter($texte, true);
if (!empty($modeles)) {
include_spip('public/assembler');
$wrap_embed_html = charger_fonction('wrap_embed_html', 'inc', true);
$offset_pos = 0;
foreach ($modeles as $m) {
// calculer le modele
# hack indexation
if ($doublons) {
$texte .= preg_replace(',[|][^|=]*,s', ' ', $m['params']);
} # version normale
else {
// si un tableau de liens a ete passe, reinjecter le contenu d'origine
// dans les parametres, plutot que les liens echappes
$params = $m['params'];
if (!is_null($liens)) {
$params = str_replace($liens[0], $liens[1], $params);
}
$modele = inclure_modele($m['type'], $m['id'], $params, $m['lien'], $connect ?? '', $env);
// en cas d'echec,
// si l'objet demande a une url,
// creer un petit encadre vers elle
if ($modele === false) {
$modele = $m['modele'];
if (!is_null($liens)) {
$modele = str_replace($liens[0], $liens[1], $modele);
}
$contexte = array_merge($env, ['id' => $m['id'], 'type' => $m['type'], 'modele' => $modele]);
if (!empty($m['lien'])) {
# un eventuel guillemet (") sera reechappe par #ENV
$contexte['lien'] = str_replace('&quot;', '"', $m['lien']['href']);
$contexte['lien_class'] = $m['lien']['class'];
$contexte['lien_mime'] = $m['lien']['mime'];
$contexte['lien_title'] = $m['lien']['title'];
$contexte['lien_hreflang'] = $m['lien']['hreflang'];
}
$modele = recuperer_fond('modeles/dist', $contexte, [], $connect ?? '');
}
// le remplacer dans le texte
if ($modele !== false) {
$modele = protege_js_modeles($modele);
if ($wrap_embed_html) {
$modele = $wrap_embed_html($m['modele'], $modele);
}
$rempl = code_echappement($modele, $echap);
$texte = substr_replace($texte, $rempl, $m['pos'] + $offset_pos, $m['length']);
$offset_pos += strlen($rempl) - $m['length'];
}
}
// hack pour tout l'espace prive
if ((test_espace_prive() or ($doublons)) and !empty($m['id'])) {
$type = strtolower($m['type']);
foreach ($doublons ?: ['documents' => ['doc', 'emb', 'img']] as $quoi => $type_modeles) {
if (in_array($type, $modeles)) {
$GLOBALS["doublons_{$quoi}_inclus"][] = $m['id'];
}
}
}
}
}
return $texte;
}

11
ecrire/inc/texte.php

@ -252,7 +252,7 @@ function typo($letexte, $echapper = true, $connect = null, $env = []) {
// NOTE : propre() ne passe pas par ici mais directement par corriger_typo
// cf. inc/lien
$letexte = traiter_modeles($mem = $letexte, false, $echapper ? 'TYPO' : '', $connect, null, $env);
$letexte = traiter_modeles($mem = $letexte, false, $echapper ? 'TYPO' : '', $connect ?? '', null, $env);
if ($letexte != $mem) {
$echapper = true;
}
@ -279,7 +279,7 @@ function typo($letexte, $echapper = true, $connect = null, $env = []) {
$GLOBALS['filtrer_javascript'] == -1
or (isset($env['espace_prive']) and $env['espace_prive'] and $GLOBALS['filtrer_javascript'] <= 0)
) {
$letexte = echapper_html_suspect($letexte);
$letexte = echapper_html_suspect($letexte, [], $connect, $env);
}
return $letexte;
@ -425,6 +425,7 @@ function propre($t, $connect = null, $env = []) {
if (is_null($connect)) {
$connect = '';
$interdire_script = true;
$env['espace_prive'] = true;
}
if (!$t) {
@ -439,10 +440,10 @@ function propre($t, $connect = null, $env = []) {
if (
$interdire_script
or $GLOBALS['filtrer_javascript'] == -1
or (isset($env['espace_prive']) and $env['espace_prive'] and $GLOBALS['filtrer_javascript'] <= 0)
or (isset($env['wysiwyg']) and $env['wysiwyg'] and $GLOBALS['filtrer_javascript'] <= 0)
or (!empty($env['espace_prive']) and $GLOBALS['filtrer_javascript'] <= 0)
or (!empty($env['wysiwyg']) and $env['wysiwyg'] and $GLOBALS['filtrer_javascript'] <= 0)
) {
$t = echapper_html_suspect($t, false);
$t = echapper_html_suspect($t, ['strict' => false], $connect, $env);
}
$t = echappe_html($t);
$t = expanser_liens($t, $connect, $env);

141
ecrire/inc/texte_mini.php

@ -103,12 +103,12 @@ function code_echappement($rempl, $source = '', $no_transform = false, $mode = n
// Echapper les <html>...</ html>
function traiter_echap_html_dist($regs) {
function traiter_echap_html_dist($regs, $options = []) {
return $regs[3];
}
// Echapper les <pre>...</ pre>
function traiter_echap_pre_dist($regs) {
function traiter_echap_pre_dist($regs, $options = []) {
// echapper les <code> dans <pre>
$pre = $regs[3];
@ -129,7 +129,7 @@ function traiter_echap_pre_dist($regs) {
}
// Echapper les <code>...</ code>
function traiter_echap_code_dist($regs) {
function traiter_echap_code_dist($regs, $options = []) {
[, , $att, $corps] = $regs;
$echap = spip_htmlspecialchars($corps); // il ne faut pas passer dans entites_html, ne pas transformer les &#xxx; du code !
@ -153,7 +153,7 @@ function traiter_echap_code_dist($regs) {
}
// Echapper les <cadre>...</ cadre> aka <frame>...</ frame>
function traiter_echap_cadre_dist($regs) {
function traiter_echap_cadre_dist($regs, $options = []) {
$echap = trim(entites_html($regs[3]));
// compter les lignes un peu plus finement qu'avec les \n
$lignes = explode("\n", trim($echap));
@ -167,11 +167,11 @@ function traiter_echap_cadre_dist($regs) {
return $echap;
}
function traiter_echap_frame_dist($regs) {
function traiter_echap_frame_dist($regs, $options = []) {
return traiter_echap_cadre_dist($regs);
}
function traiter_echap_script_dist($regs) {
function traiter_echap_script_dist($regs, $options = []) {
// rendre joli (et inactif) si c'est un script language=php
if (preg_match(',<script\b[^>]+php,ims', $regs[0])) {
return highlight_string($regs[0], true);
@ -193,6 +193,7 @@ define('_PROTEGE_BLOCS', ',<(html|pre|code|cadre|frame|script|style)(\b[^>]*)?>(
* @param bool $no_transform
* @param string $preg
* @param string $callback_prefix
* @param array $callback_options
* @return string|string[]
*/
function echappe_html(
@ -200,7 +201,8 @@ function echappe_html(
$source = '',
$no_transform = false,
$preg = '',
$callback_prefix = ''
$callback_prefix = '',
$callback_options = []
) {
if (!is_string($letexte) or !strlen($letexte)) {
return $letexte;
@ -216,11 +218,16 @@ function echappe_html(
$echap = $regs[0];
} // sinon les traiter selon le cas
else {
$callback_secure_prefix = ($callback_options['secure_prefix'] ?? '');
if (
function_exists($f = $callback_prefix . 'traiter_echap_' . strtolower($regs[1]))
function_exists($f = $callback_prefix . $callback_secure_prefix . 'traiter_echap_' . strtolower($regs[1]))
or function_exists($f = $f . '_dist')
or ($callback_secure_prefix and (
function_exists($f = $callback_prefix . 'traiter_echap_' . strtolower($regs[1]))
or function_exists($f = $f . '_dist')
))
) {
$echap = $f($regs);
$echap = $f($regs, $callback_options);
}
}
@ -474,11 +481,18 @@ function echapper_faux_tags($letexte) {
* on l'echappe
* si safehtml ne renvoie pas la meme chose on echappe les < en &lt; pour montrer le contenu brut
*
* @use wrap()
*
* @param string $texte
* @param bool $strict
* @param array $options
* bool strict : etre strict ou non sur la detection
* string wrap_suspect : si le html est suspect, on wrap l'affichage avec la balise indiquee dans cette option via la fonction wrap()
* string texte_source_affiche : si le html est suspect, on utilise ce texte pour l'affichage final et pas le texte utilise pour la detection
* @param string $connect
* @param array $env
* @return string
*/
function echapper_html_suspect($texte, $strict = true) {
function echapper_html_suspect($texte, $options = [], $connect = null, $env = []) {
static $echapper_html_suspect;
if (!$texte or !is_string($texte)) {
return $texte;
@ -489,9 +503,23 @@ function echapper_html_suspect($texte, $strict = true) {
}
// si fonction personalisee, on delegue
if ($echapper_html_suspect) {
return $echapper_html_suspect($texte, $strict);
// on collecte le tableau d'arg minimal pour ne pas casser un appel a une fonction inc_echapper_html_suspect() selon l'ancienne signature
$args = [$texte, $options];
if ($connect or !empty($env)) {
$args[] = $connect;
}
if (!empty($env)) {
$args[] = $env;
}
return $echapper_html_suspect(...$args);
}
if (is_bool($options)) {
$options = ['strict' => $options];
}
$strict = $options['strict'] ?? true;
// pas de balise html ou pas d'attribut sur les balises ? c'est OK
if (
strpos($texte, '<') === false
or strpos($texte, '=') === false
@ -499,24 +527,63 @@ function echapper_html_suspect($texte, $strict = true) {
return $texte;
}
// quand c'est du texte qui passe par propre on est plus coulant tant qu'il y a pas d'attribut du type onxxx=
// car sinon on declenche sur les modeles ou ressources
if (
!$strict and
(strpos($texte, 'on') === false or !preg_match(",<\w+.*\bon\w+\s*=,UimsS", $texte))
) {
return $texte;
// dans le prive, on veut afficher tout echappé pour la moderation
if (!isset($env['espace_prive'])) {
// conserver le comportement historique en cas d'appel court sans env
$env['espace_prive'] = test_espace_prive();
}
if (!empty($env['espace_prive']) or !empty($env['wysiwyg'])) {
// on teste sur strlen car safehtml supprime le contenu dangereux
// mais il peut aussi changer des ' en " sur les attributs html,
// donc un test d'egalite est trop strict
if (strlen(safehtml($texte)) !== strlen($texte)) {
$texte = str_replace('<', '&lt;', $texte);
if (!function_exists('attribut_html')) {
include_spip('inc/filtres');
// quand c'est du texte qui passe par propre on est plus coulant tant qu'il y a pas d'attribut du type onxxx=
// car sinon on declenche sur les modeles ou ressources
if (
!$strict and
(strpos($texte, 'on') === false or !preg_match(",<\w+.*\bon\w+\s*=,UimsS", $texte))
) {
return $texte;
}
[$texte, $markid] = modeles_echapper_raccourcis($texte, false);
$texte = echappe_js($texte);
$texte_to_check = $texte;
// si les raccourcis liens vont etre interprétés, il faut les expanser avant de vérifier que le html est safe
// car un raccourci peut etre utilisé pour faire un lien malin
// et un raccourci est potentiellement modifié par safehtml, ce qui fait un faux positif dans is_html_safe
if (!empty($options['expanser_liens'])) {
$texte_to_check = expanser_liens($texte_to_check, $env['connect'] ?? '', $env['env'] ?? []);
}
if (!is_html_safe($texte_to_check)) {
$texte = $options['texte_source_affiche'] ?? $texte;
$texte = preg_replace(",<(/?\w+\b[^>]*>),", "<tt>&lt;\\1</tt>", $texte);
$texte = str_replace('<', '&lt;', $texte);
$texte = str_replace('&lt;tt>', '<tt>', $texte);
$texte = str_replace('&lt;/tt>', '</tt>', $texte);
if (!function_exists('attribut_html')) {
include_spip('inc/filtres');
}
if (!empty($options['wrap_suspect'])) {
$texte = wrap($texte, $options['wrap_suspect']);
}
$texte = "<mark class='danger-js' title='" . attribut_html(_T('erreur_contenu_suspect')) . "'></mark> " . $texte;
}
$texte = modele_retablir_raccourcis_echappes($texte, $markid);
}
// si on est là dans le public c'est le mode parano
// on veut donc un rendu propre et secure, et virer silencieusement ce qui est dangereux
else {
$markid = null;
if (!empty($options['expanser_liens'])) {
$texte = expanser_liens($texte, $env['connect'] ?? '', $env['env'] ?? '');
}
else {
[$texte, $markid] = modeles_echapper_raccourcis($texte, false);
}
$texte = safehtml($texte);
if ($markid) {
$texte = modele_retablir_raccourcis_echappes($texte, $markid);
}
$texte = "<mark class='danger-js' title='" . attribut_html(_T('erreur_contenu_suspect')) . "'></mark> " . $texte;
}
return $texte;
@ -567,6 +634,26 @@ function safehtml($t) {
}
/**
* Detecter si un texte est "safe" ie non modifie significativement par safehtml()
*/
function is_html_safe(string $texte): bool {
if ($is_html_safe = charger_fonction('is_html_safe', 'inc', true)) {
return $is_html_safe($texte);
}
// simplifier les retour ligne pour etre certain de ce que l'on compare
$texte = str_replace("\r\n", "\n", $texte);
// safehtml reduit aussi potentiellement les &nbsp;
$texte = str_replace("&nbsp;", " ", $texte);
$texte_safe = safehtml($texte);
// on teste sur strlen car safehtml supprime le contenu dangereux
// mais il peut aussi changer des ' en " sur les attributs html,
// donc un test d'egalite est trop strict
return strlen($texte_safe) === strlen($texte);
}
/**
* Supprime les modèles d'image d'un texte
*

1
prive/modeles/dist.html

@ -0,0 +1 @@
<tt>#ENV{modele}</tt>
Loading…
Cancel
Save