OASIS_MOBILE/src/backend/sanitizeHtml.js
oasis-bot c332afd6ea upstream: integrar Oasis 1.3.1
Base del merge a tres bandas: git 0.9.5 de epsylon, esta rama y el src de la
APK oficial 1.3.1 (la variante movil: OASIS_MOBILE, regex sin \p{}, audio y
video de Android). Corre en el mismo nodejs-mobile 18.20.4 que ya usabamos.

Se conserva de la rama: Karvan, identidades y multicuenta, topbar y barra
inferior, borrado de EXIF sin sharp, filtro hot del foro, RTC sin STUN y los
modulos apagados en movil (wallet, banking, torrents).

Se toma de upstream lo que ya hace de serie: respuesta citada en chats, ssb-lan
en movil, configuracion en ~/.ssb/oasis (sobrevive a reinstalar el backend).
El asistente de bienvenida deja elegir workflow tambien en movil.
2026-10-18 02:52:17 +02:00

62 lines
2.5 KiB
JavaScript

"use strict";
const SAFE_SRC = /^\/(?:blob|image)\/[^\s"'<>]+$/i;
const SAFE_HREF = /^(?:https?:\/\/|\/(?!\/)|#|mailto:)/i;
const SAFE_CLASS = /^(?:rt-[a-z0-9-]+|styled-link|mention|tag-link|zoom-link|pm-quote|wiki-link(?:-missing)?|internal-link(?:-id|-kind)?|lightbox(?:-[a-z]+)?|post-(?:audio|image|pdf|video))$/;
let purifier = null;
const getPurify = () => {
if (purifier) return purifier;
const { JSDOM } = require('../server/node_modules/jsdom');
const DOMPurify = require('../server/node_modules/dompurify');
const purify = DOMPurify(new JSDOM('').window);
purify.addHook('uponSanitizeAttribute', (node, data) => {
const name = String(data.attrName || '').toLowerCase();
const value = String(data.attrValue || '').trim();
if (name === 'src' && !SAFE_SRC.test(value)) data.keepAttr = false;
if (name === 'href' && !SAFE_HREF.test(value)) data.keepAttr = false;
if (name === 'class') {
const kept = value.split(/\s+/).filter(c => SAFE_CLASS.test(c));
if (kept.length) data.attrValue = kept.join(' '); else data.keepAttr = false;
}
});
purifier = purify;
return purifier;
};
const dropControlChars = (html) => String(html).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, '');
const stripDangerousTags = (input) => {
if (typeof input !== 'string') return '';
return dropControlChars(getPurify().sanitize(input, {
ALLOWED_TAGS: [
'p', 'br',
'b', 'strong', 'i', 'em', 'u',
'ul', 'ol', 'li',
'blockquote',
'code', 'pre'
],
ALLOWED_ATTR: [],
FORBID_TAGS: ['svg', 'math', 'iframe', 'object', 'embed', 'form', 'input', 'textarea', 'select', 'button', 'script'],
FORBID_ATTR: ['style']
}));
};
const sanitizeHtml = (input) => {
if (typeof input !== 'string') return '';
return dropControlChars(getPurify().sanitize(input, {
ALLOWED_TAGS: [
'p', 'br', 'hr',
'b', 'strong', 'i', 'em', 'u', 's', 'del',
'ul', 'ol', 'li',
'blockquote', 'code', 'pre',
'a', 'span', 'div',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'img', 'video', 'audio',
'table', 'thead', 'tbody', 'tr', 'th', 'td'
],
ALLOWED_ATTR: ['href', 'class', 'target', 'rel', 'src', 'alt', 'title', 'controls'],
FORBID_TAGS: ['svg', 'math', 'iframe', 'object', 'embed', 'form', 'input', 'textarea', 'select', 'button', 'script', 'style', 'link', 'meta'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onsubmit', 'onchange', 'style']
}));
};
module.exports = { stripDangerousTags, sanitizeHtml };