I installed 1.3.0 on a throwaway test forum and re-ran the actual exploits, and the six I reported are genuinely closed:
- The username-font and title-font XSS: both crafted values are rejected against the font allowlist now (and stripped again at the render sink as defense-in-depth) - re-ran both, no execution.
- The gradient-direction / style-token CSS injection: allowlisted, the breakout value drops to the default - confirmed no rule injection.
- The colour fields: regex-validated (anchored), good.
- The tier + block checks in the core save method: I could no longer equip a premium frame via import (themes go through the identical unconditional re-validation right beside it), and a blocked user can no longer restyle via import (got a permission error) - both fixed.
Nicely done. A few follow-ups from a deeper pass - one residual security item, then some performance/quality things you can take or leave. All re-tested on 1.3.0, all in Persona itself.
1. The tier fix is complete for frames and post themes, but name badges can still be bypassed via import (medium).
Frames and post themes get re-validated unconditionally in the save method now. But the badge re-validation only runs when the multi-badge field is non-empty, and the older single-badge id column is never tier-checked - not in the save method, and not at render (the render helper filters on active only, and falls back to the single-badge id when the multi field is empty). Because an empty string counts as "empty" in PHP, a free user can send an import with an empty multi-badge field plus a single premium badge id, skip the re-validation, and the premium badge persists and renders.
- Verified: as a plain Registered member I imported a payload equipping the Pro-tier "Elite" badge; it saved and rendered next to my username on posts/profile - a paid badge on a free account.
- Same pattern, minor/latent: the save-time re-validation checks tier but not seasonal availability - the picker enforces the date window, the import path doesn't - so if you ever date-gate an item, it'd be equippable out of its window via import. Your default seasonal items don't set a window, so it's not currently exploitable; just worth closing alongside the badge fix.
- Fix: tier-check (and seasonal-check) the single-badge id in the save method regardless of the multi field - or normalise an empty/"0" multi field to an empty array before the re-validation runs - and/or re-check at render in the badge helper.
2. Latent DOM XSS in the profile music/video embeds - not currently firing, but worth fixing when you fix the embeds (low now, high later).
Your URL sanitiser blocks javascript:/data: and requires http(s), but it keeps angle brackets and quotes (the URL filter it uses doesn't strip them, and it then decodes HTML entities), and the client-side embed builder inserts the stored URL into the iframe/anchor markup by string concatenation (innerHTML). So a stored profile music URL that begins with the allowed domain but continues with markup closing the src attribute survives the sanitiser and would break out when the embed is built.
- Tested caveat: on a stock XenForo 2.3.10 install the embed block never actually renders - the injector looks for a member-header link carrying a user-id attribute, and stock XF's member header doesn't have one, so the embeds silently don't display at all. So this is NOT currently exploitable on stock XF (I confirmed the payload stored but never executed) - but it's a functional bug (embeds don't show), and it becomes a real DOM XSS the moment you fix the render.
- Fix: when you get the embeds rendering, strip/encode angle brackets and quotes in the sanitiser, and set the iframe src as a DOM property rather than building the markup by string concatenation.
3. Performance - the biggest practical one.
On every public page load (guests included), the listener runs unbounded full-table scans of all user styles and all badges (plus fonts and ranks) into PHP memory, with no caching, and emits per-user CSS links site-wide. On a small forum it's fine, but it'll scale poorly as the number of styled users grows - every page pays the cost. Worth caching the assembled output (registry/simple cache, rebuilt on save) and/or only loading the configs actually needed for the usernames on the current page.
4. Minor / packaging.
- The 1.3.0 archive ships several .bak files inside the add-on (older copies of the listener and repository, including pre-fix versions), so old source ends up in the deployed tree - worth excluding *.bak from the build.
- The admin custom-font upload throws a fatal (it calls a move method that doesn't exist on the upload object), so that feature doesn't work / nothing is written - and when you wire it up it'll need validity + extension/size checks.
A couple of smaller XF-convention things (using XF's JS framework for the injected scripts instead of raw script blocks, moving user-facing strings to phrases, atomic counter updates) I'll leave out of here - happy to walk through them privately if useful, no worries either way.
Everything above is from the shipped 1.3.0 source on a local test install; glad to share exact repro for the badge one. Really appreciate how fast you turned the fixes around - the core issues are solidly closed.