Packagist · yeswiki/yeswiki
YesWiki has stored XSS in Bazar form-field templates via unescaped field.label / field.hint (|raw('html'))
|raw('html') to field.label / field.hint in attribute and label-body contexts — stored XSS in form renders (sibling class of commit e6b66aa)CWE: CWE-79 (Improper Neutralization of Input During Web Page Generation, "Cross-site Scripting") via CWE-116 (Improper Encoding or Escaping of Output) — same class as the partial fix at commit e6b66aa
CVSS v3.1: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:N → 4.7 (Medium)
(Privileges Required = High because writing the field definitions requires saisie_formulaire, which tools/bazar/services/Guard.php:58-61 grants only to admins by default; Scope = Changed because the XSS payload set by a form-editor admin executes in the origin context of arbitrary viewers, including unauthenticated visitors.)
Commit e6b66aa ("fix(bazar): leave the twig escape placeholder as is", 2026-05-19) recognised that emitting field.label through Twig's raw('html') filter into an HTML attribute is unsafe — Twig's raw marker suppresses the attribute auto-escape, striptags removes <…> tags but not ", so a label containing " can break out of the attribute and inject event-handler attributes. The commit fixed tools/bazar/templates/inputs/text.twig:19 and tools/bazar/templates/inputs/textarea.twig:3.
At least seven additional templates have the same pattern and were not touched by the fix:
tools/bazar/templates/inputs/range.twig:19 — placeholder="{{ field.label|raw('html')|striptags }}"tools/bazar/templates/inputs/email.twig:13 — placeholder="{{ field.label|raw('html')|striptags }}"tools/bazar/templates/layouts/input.twig:7 — title="{{ field.hint|raw('html') }}" alt="{{ field.hint|raw('html') }}"tools/bazar/templates/inputs/textarea.twig:14 — same title=/alt= pattern (the commit only fixed line 3, line 14 remains)tools/bazar/templates/inputs/user.twig:41, 55 — sametools/bazar/templates/inputs/bookmarklet.twig:4 — sametools/bazar/templates/layouts/input.twig:9, tools/bazar/templates/layouts/field.twig:5, tools/bazar/templates/inputs/subscribe.twig:16, tools/bazar/templates/inputs/linked-entry.twig:4, tools/bazar/templates/inputs/textarea.twig:16, tools/bazar/templates/inputs/bookmarklet.twig:6 — {{ field.label|raw }} outside an attribute (label-body), with no striptags at all, so direct tag injection (<img src=x onerror=…>) executesThe layouts/input.twig and layouts/field.twig files are base layouts inherited by every Bazar field type, so a single malicious field.hint reaches into every form that uses that field.
doryphore at HEAD 6c653dd (the audit checkout)|raw('html') / |raw filter in attribute or label-body contextfield.label and field.hint are populated from form definitionstools/bazar/fields/BazarField.php:46-53:
$this->label = empty($values[self::FIELD_LABEL]) ? '' : html_entity_decode($values[self::FIELD_LABEL]);
$this->size = $values[self::FIELD_SIZE];
$this->maxChars = $values[self::FIELD_MAX_CHARS];
$this->default = $values[self::FIELD_DEFAULT];
$this->required = $values[self::FIELD_REQUIRED] == 1;
$this->searchable = $values[self::FIELD_SEARCHABLE];
$this->hint = $values[self::FIELD_HINT]; // [A] no decoding/escaping
field.label is html_entity_decode($values[FIELD_LABEL]) — the decode actively turns HTML-entity-encoded payloads (", ") back into raw ", defeating any entity-encoded mitigation a form author might apply. field.hint is the raw string from the form definition. Both flow into the field's __toString-like context unchanged. Form definitions are written by users with the saisie_formulaire ACL (tools/bazar/services/Guard.php:45-62 — admins by default; the same ACL the audit team chose to gate imported-form POST handling under in commit fe7244b).
|raw('html')|striptags (placeholder breakout)tools/bazar/templates/inputs/range.twig:19:
placeholder="{{ field.label|raw('html')|striptags }}"
tools/bazar/templates/inputs/email.twig:13:
placeholder="{{ field.label|raw('html')|striptags }}"
raw('html') marks the value as a Markup object, which causes Twig's HTML auto-escaper to skip it (Twig\Markup::__toString). striptags removes <…> sequences but does not touch ", ', or =. A field.label of:
hi" onmouseover="alert(document.cookie)" x="
passes striptags unchanged, is marked safe by raw('html'), and lands inside the attribute as:
placeholder="hi" onmouseover="alert(document.cookie)" x=""
The injected onmouseover fires when a viewer hovers the input. Same vector as the pre-fix text.twig:19.
|raw('html') without striptags (worse)tools/bazar/templates/layouts/input.twig:7:
{% if field.hint %}
<img loading="lazy" class="tooltip_aide" title="{{ field.hint|raw('html') }}" alt="{{ field.hint|raw('html') }}" src="tools/bazar/presentation/images/aide.png" width="16" height="16" />
{% endif %}
Identical patterns in tools/bazar/templates/inputs/textarea.twig:14, tools/bazar/templates/inputs/user.twig:41, tools/bazar/templates/inputs/user.twig:55, tools/bazar/templates/inputs/bookmarklet.twig:4.
There is no striptags here at all, so the attacker has the full attribute-injection alphabet plus full HTML if the parser desynchronises. Setting field.hint = '"><script>alert(1)</script>' gives:
<img … title=""><script>alert(1)</script>" alt="…" …
The <script> runs at page parse time. Because layouts/input.twig is extended by every field-type template, a single malicious field.hint on any field in any form propagates into every form render.
|raw (direct DOM injection)tools/bazar/templates/layouts/input.twig:9:
{{ field.label|raw }}
tools/bazar/templates/layouts/field.twig:5:
{%- block label -%}{{ field.label|raw }}{%- endblock -%}
Plus subscribe.twig:16, linked-entry.twig:4, textarea.twig:16, bookmarklet.twig:6.
These are outside any attribute, in the body of a <label> element. raw suppresses escaping, there is no striptags. field.label = '<img src=x onerror=alert(1)>' injects an <img> tag straight into the label DOM; the onerror fires the moment the page renders, with no user interaction.
e6b66aa is incompleteThe fix correctly replaced field.label | raw('html') | striptags with field.label | striptags | trim (no raw) in text.twig's placeholder and textarea.twig's textarea placeholder. The fix is the right pattern — drop the raw so Twig's attribute-context autoescaper does its job — but it was applied at two specific call sites instead of being treated as a class-wide replacement. The siblings above use the same |raw('html')|striptags or |raw('html') idiom and are all currently exploitable.
saisie_formulaire ACL).range, email, or any other field type (every field type renders through layouts/input.twig, so the title= / alt= / label-body vectors apply universally).range.twig placeholder attribute breakout (Sink class [B])Set the field's label to:
Enter value" onmouseover="alert('XSS via field.label in range.twig')" x="
Save the form. Have any visitor (including unauthenticated guests if the form is published) open a page that renders the form. Hovering the range input fires the injected handler.
Rendered HTML:
<input type="range" … placeholder="Enter value" onmouseover="al
Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.
Check my repoSources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.