{"schema":"threatline.record/2","record":{"id":"GHSA-g7rc-4jvq-27m9","aliases":["GHSA-g7rc-4jvq-27m9"],"summary":"e107 Search Author Reflected XSS Report","details":"## Summary\r\n\r\nAnyone, without an account, can put markup and script of their choosing into the\r\nsite's search page. They send a link, or a page that submits a form to the site, and\r\nthe script runs in the browser of whoever opens it. It runs in the site's own origin,\r\nwith that person's session.\r\n\r\nThe search page draws an Author field for search areas that offer one. It passed the\r\n`author` value from the address to the form helper as the field's **options**, not as\r\nits value. The helper reads a string of options the way a query string is read, and\r\ntakes a bracketed parameter as a ready-made list, so the sender chooses every option\r\nof the field, names as well as values. Several of those options reach the page\r\nwithout escaping.\r\n\r\nWhen the person who opens the link is a logged-in administrator, the script acts as\r\nthat administrator. An administrator can install a plugin, so this amounts to running\r\ncode on the server.\r\n\r\n## Is my site affected, and what can I do now?\r\n\r\n**The check.** On the site's search page, switch to the advanced options for each\r\nsearch area. If any area offers an **Author** field, the site is exposed while it runs\r\nan affected version. The comments search offers one, and so do the Download, Forum,\r\nChatbox and FAQ plugins' search areas.\r\n\r\nOn releases up to 2.3.12 the advanced options are only served for a search area that\r\nthe search configuration lists. Opening **Admin > Search** once is enough to list\r\nevery area, so a site where an administrator has ever opened that page should be\r\ntreated as exposed.\r\n\r\n**Until the site is updated to 2.3.13**, either of these closes it:\r\n\r\n1. **Apply the fix by hand.** In `search.php`, the Author field is drawn by a call\r\n   ending `userpicker(...,$adv_key,$_GET[$adv_key]);`. Remove the last argument,\r\n   leaving `userpicker(...,$adv_key);`. The field renders exactly as before for\r\n   every genuine request.\r\n2. **Strip every query parameter whose name starts with `author`** from requests to\r\n   `search.php`, at the web server or a proxy in front of it. Match the name after\r\n   URL-decoding, and include bracketed names such as `author[...]`: those arrive as\r\n   a list of options and need no `=` in the value. A rule that only looks at a plain\r\n   `author=` parameter leaves the site exposed. The search form never relies on\r\n   these parameters (its field, `author_name`, is ignored by every search area), so\r\n   nothing a visitor does from the page is lost.\r\n\r\n## Details\r\n\r\nThe advanced block draws an Author field with the request value as the third\r\nargument. On 2.3.12:\r\n\r\nhttps://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/search.php#L474\r\n\r\nThe development branch has the same call, reworked but with the same arguments:\r\n\r\nhttps://github.com/e107inc/e107/blob/fa4bd2a97da1d0793388384272063516bfd5df1d/search.php#L483\r\n\r\n`e_form::userpicker()` is declared `($name, $value, $options)`. A string in\r\n`$options` goes through `parse_str()`, which decodes the value once more and turns it\r\ninto option **keys** as well as values; a bracketed `author[...]` parameter arrives\r\nas a list and is used as it is:\r\n\r\nhttps://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/e107_handlers/form_handler.php#L2009-L2014\r\n\r\nThe options are merged over the picker's own and handed to `e_form::text()`:\r\n\r\nhttps://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/e107_handlers/form_handler.php#L2094-L2096\r\n\r\nFrom there, four of them reach the page unsafely.\r\n\r\n- **`tabindex`** is in the default option set and is placed inside single quotes as\r\n  it stands, so a quote in the value ends the attribute:\r\n\r\n  https://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/e107_handlers/form_handler.php#L4609\r\n  https://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/e107_handlers/form_handler.php#L4430-L4434\r\n\r\n- **`other`** is in the default option set too and is appended verbatim, which is\r\n  enough to add attributes, close the tag and write elements after it:\r\n\r\n  https://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/e107_handlers/form_handler.php#L4613\r\n  https://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/e107_handlers/form_handler.php#L4446-L4450\r\n\r\n- **Any key beginning `data-`** is kept, and the attribute writer escapes its value\r\n  but writes its name as given, so the name can carry further attributes:\r\n\r\n  https://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/e107_handlers/form_handler.php#L4571\r\n  https://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/e107_handlers/e_parse_class.php#L2157\r\n\r\n- **`selectize`** replaces the picker's whole configuration, which `text()` hands to\r\n  the page's scripts. The bundled picker library treats some of those settings as\r\n  markup, so script runs as the page loads without the visitor touching the field:\r\n\r\n  https://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/e107_handlers/form_handler.php#L874-L882\r\n\r\nThe tag itself is assembled here:\r\n\r\nhttps://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/e107_handlers/form_handler.php#L906\r\n\r\nSo escaping individual options would not have been enough. The fault is that the\r\naddress chose the options at all.\r\n\r\n**Two ways in, both unauthenticated.** The first is an ordinary `GET` of the search\r\npage. The second needs no token, no cookie and no `X-Requested-With` header: the\r\nsearch page's constructor echoes the advanced block on its own for any request the\r\nsite treats as AJAX, and a request declares itself AJAX with an ordinary parameter.\r\nA cross-site form `POST` is a top-level navigation, so the fragment is parsed as a\r\npage in the site's origin.\r\n\r\nhttps://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/search.php#L86-L94\r\nhttps://github.com/e107inc/e107/blob/ffb1c81938deaaa9bf1f897a409b181e0294e450/e107_handlers/e107_class.php#L5123\r\n\r\ne107's built-in request filtering did not stop it in testing, and the site sends no\r\nContent-Security-Policy header.\r\n\r\n**Why 2.2.0.** The call was written in 2014 for an older `userpicker()` whose third\r\nparameter was a default name. The method took its present signature in 2016, but\r\nuntil 2019 it read only three options of its own and dropped the rest. From the\r\nchange that passed the remaining options on to the input (`a6476e680e`, first\r\nreleased in 2.2.0), the address has decided what that input carries. The same\r\nmismatch is why searching by author has never worked in those releases: the form\r\nsends `author_name` while every search area reads `author`.\r\n\r\n**Reproduced** on 2.3.12, configured as above, and on both development branches as\r\nfreshly installed: the injected markup was in the served page, and the script ran in\r\nheadless Chrome. On the development branch, the script also ran in a logged-in\r\nadministrator's browser and read an administration page and its form token, and the\r\npicker-settings route ran script as the page loaded.\r\n\r\n## Impact\r\n\r\nCross-site scripting, reflected, against anyone who follows a link or lands on a page\r\nthat submits a form for them. No account is needed to send it.\r\n\r\nThe script runs in the site's origin with the victim's session. Against an\r\nadministrator it can read and submit any administration form, including the ones\r\nthat install plugins, which means running code on the server. Against a member or a\r\nguest it can act in their name and change what the page shows them.\r\n\r\n## Severity\r\n\r\n`CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H`, 9.6 Critical.\r\n\r\nThe impact is scored as code execution because the script can act as an\r\nadministrator, and an administrator can install code. `UI:R` because the victim has\r\nto open the link. `AC:L` despite the precondition on released versions: a search\r\npage with a configured area is the ordinary state of a running site.\r\n\r\n## Patches\r\n\r\nThe search page now passes the author field its name and value only, so the address\r\ncan no longer choose its options. `e_form::get_attributes()` also writes `tabindex`\r\nthrough the shared, escaping attribute writer as an integer. An integer tabindex\r\nrenders exactly as before.\r\n\r\n`other`, `data-` attribute names and the picker's `selectize` settings are left as\r\nthey are on purpose. `other` is documented as a raw attribute string, and core's own\r\nadministration lists pass inline styles through it. `data-` names and picker settings\r\nare chosen by the code that calls the helper, and core's callers pass fixed ones.\r\nChanging how any of them is written would change what existing callers render. The\r\nfault was a caller handing them request data, and that is what changed.\r\n\r\nRegression tests render the search page with hostile `author` values and check both\r\nthe advanced block and the settings handed to its scripts, and check that a\r\n`tabindex` cannot leave its attribute.\r\n\r\nFix commits: release/v2.3.x [ee474c7415](https://github.com/e107inc/e107/commit/ee474c7415755d51450ab12955714f5ae0d8af87); master [e9416ba0c0](https://github.com/e107inc/e107/commit/e9416ba0c0c55814ea107056b6c889d085af5dcf).\r\n","categories":["Unclassified"],"product":"e107inc/e107","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/e107inc/e107/security/advisories/GHSA-g7rc-4jvq-27m9","source_modified_at":"2026-10-21T11:55:41Z","fetched_at":"2026-10-24T18:03:20.872036Z","package":{"ecosystem":"composer","name":"e107inc/e107"},"range":">= 2.2.0, <= 2.3.12"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/e107inc/e107/security/advisories/GHSA-g7rc-4jvq-27m9","source_modified_at":"2026-10-21T11:55:41Z","fetched_at":"2026-10-24T18:03:20.872036Z","package":{"ecosystem":"composer","name":"e107inc/e107"},"version":"2.3.13"}],"cvss":5.1,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-21T08:01:43Z","modified_at":"2026-10-21T11:55:41Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/e107inc/e107/security/advisories/GHSA-g7rc-4jvq-27m9","fetched_at":"2026-10-24T18:03:20.872036Z","source_modified_at":"2026-10-21T11:55:41Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-21T11:55:41Z","content_hash":"3cad9999d218d30d3f340748ae16feaaead5b8ffeeda3ecba580ec5bedd28899"}}
