fix(security): eliminate XSS vectors and restore zoom accessibility

- Extract shared esc() utility (public/utils/html.js) replacing 8
  duplicate escHtml() functions across all page modules
- Apply HTML escaping to all user-controlled data in innerHTML
  templates: titles, names, locations, descriptions, colors, notes
  content, weather data, autocomplete suggestions
- Remove user-scalable=no and maximum-scale=1 from viewport meta
  tag, restoring pinch-to-zoom for WCAG 1.4.4 compliance
- Bump version to 0.7.1
This commit is contained in:
Ulas
2026-04-04 06:25:28 +02:00
parent 87186c03c0
commit 6bc4c46f03
13 changed files with 145 additions and 170 deletions
+9 -14
View File
@@ -8,6 +8,7 @@ import { api } from '/api.js';
import { openModal as openSharedModal, closeModal, btnError } from '/components/modal.js';
import { stagger, vibrate } from '/utils/ux.js';
import { t } from '/i18n.js';
import { esc } from '/utils/html.js';
// --------------------------------------------------------
// Konstanten
@@ -31,7 +32,7 @@ let _container = null;
function renderMarkdownLight(text) {
if (!text) return '';
return escHtml(text)
return esc(text)
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/^- (.+)$/gm, '• $1')
@@ -54,7 +55,7 @@ export async function render(container, { user }) {
<i data-lucide="search" class="notes-toolbar__search-icon" aria-hidden="true"></i>
<input type="search" id="notes-search" class="notes-toolbar__search-input"
placeholder="${t('notes.searchPlaceholder')}" autocomplete="off"
value="${escHtml(state.filterQuery)}">
value="${esc(state.filterQuery)}">
</div>
<button class="btn btn--primary" id="notes-add-btn">
<i data-lucide="plus" style="width:16px;height:16px;margin-right:4px;" aria-hidden="true"></i>
@@ -155,18 +156,18 @@ function renderNoteCard(note) {
return `
<div class="note-card ${note.pinned ? 'note-card--pinned' : ''}"
data-id="${note.id}"
style="background-color:${escHtml(note.color)};color:${textColor};">
style="background-color:${esc(note.color)};color:${textColor};">
<button class="note-card__pin" data-action="pin" data-id="${note.id}"
aria-label="${note.pinned ? t('notes.unpinAction') : t('notes.pinAction')}">
<i data-lucide="${note.pinned ? 'pin-off' : 'pin'}" style="width:12px;height:12px;" aria-hidden="true"></i>
</button>
${note.title ? `<div class="note-card__title">${escHtml(note.title)}</div>` : ''}
${note.title ? `<div class="note-card__title">${esc(note.title)}</div>` : ''}
<div class="note-card__content">${renderMarkdownLight(note.content)}</div>
<div class="note-card__footer">
<div class="note-card__creator">
<span class="note-card__avatar"
style="background-color:${escHtml(note.creator_color || '#8E8E93')}">${initials}</span>
<span>${escHtml(note.creator_name || '')}</span>
style="background-color:${esc(note.creator_color || '#8E8E93')}">${initials}</span>
<span>${esc(note.creator_name || '')}</span>
</div>
<button class="note-card__delete" data-action="delete" data-id="${note.id}" aria-label="${t('notes.deleteLabel')}">
<i data-lucide="trash-2" style="width:12px;height:12px;" aria-hidden="true"></i>
@@ -318,7 +319,7 @@ function openNoteModal({ mode, note = null }) {
<div class="form-group">
<label class="form-label" for="note-title">${t('notes.titleLabel')}</label>
<input type="text" class="form-input" id="note-title"
placeholder="${t('notes.titlePlaceholder')}" value="${escHtml(isEdit && note.title ? note.title : '')}">
placeholder="${t('notes.titlePlaceholder')}" value="${esc(isEdit && note.title ? note.title : '')}">
</div>
<div class="form-group">
<label class="form-label" for="note-content">${t('notes.contentLabel')} <span style="font-weight:400;color:var(--text-tertiary);font-size:.85em;">${t('notes.contentMarkdownHint')}</span></label>
@@ -364,7 +365,7 @@ function openNoteModal({ mode, note = null }) {
</div>
<textarea class="form-input" id="note-content" rows="6"
placeholder="${t('notes.contentPlaceholder')}"
style="resize:vertical;">${escHtml(isEdit ? note.content : '')}</textarea>
style="resize:vertical;">${esc(isEdit ? note.content : '')}</textarea>
</div>
<div class="form-group">
<label class="form-label">${t('notes.colorLabel')}</label>
@@ -498,9 +499,3 @@ function isLightColor(hex) {
return (r * 299 + g * 587 + b * 114) / 1000 > 150;
}
function escHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}