Caution
v2.3.13 is a security release for sites on v2.3.12 or earlier. Upgrade from any 2.x at or below v2.3.12. If your site tracks the master branch, update from master instead: installing v2.3.13 would be a downgrade. v2.4.x is planned to be the next forward step.
Important
Upgrade PHP first, now. PHP 8.5.12, 8.4.27, 8.3.36, and 8.2.35 fix two critical vulnerabilities in PHP itself, CVE-2026-109618 and CVE-2026-109619, that can let an unauthenticated request run code on the server. Every PHP that serves web requests is at risk, whether it runs as PHP-FPM, mod_php, or php-cgi. Upgrade PHP to one of these releases right away, even before you upgrade e107.
PHP 8.1 and older no longer get updates from PHP itself. If your PHP comes from a provider of extended support, such as Freexian or Ondřej Surý, follow that provider's guidance for your PHP version. If you use Remi Collet's repository, PHP's announcement says it will not have updates today, and recommends distribution packages or building from source instead. Until you can update, the announcement lists settings that narrow the exposure. e107 v2.3.13 runs on PHP 5.6 and later.
Then upgrade e107 promptly. This release closes 114 security advisories: 29 Critical, 37 High, 39 Medium, and 9 Low. Security below lists them by area.
Most advisories name a way to narrow the exposure until you can upgrade, such as a setting, a permission to withdraw, or a web-server rule. None of those is a fix.
Before upgrading:
e107_config.php sets $mySQLport, check that your database server listens on it. e107 now connects on that port, where it used to ignore the line.index.php follows the request. The settings are under Site address and database below./~user/ address serves the site from a folder whose name holds anything but ASCII letters, digits, and - . _ ~, rename it to keep e107's cookies to the site. e107 now sets them on the folder above, the whole host when that folder is at the root, where other applications receive them. Visitors signed in before the update sign in once more.After upgrading:
e107_plugins/faqs/controllers/ if it is still on disk, then run the FAQs update in Admin » Plugin Manager. Run its other updates too, such as the PM plugin's 3.2.e_HTTP_STATIC, clear your CDN's cache.e107_config.php has no site_path and files saved since v2.3.4 are missing, merge the folder marked Known bad, as For Administrators » Fixed » Site address and database describes.javascript:;{ in their name, description, or author website or email, where a shortcode still runs.Warning
Some of these fixes change behavior you may be relying on. All but the last group close a way to attack a site. For Administrators » Changed has the details under each area's name; plugin and theme items are under For Developers » Changed.
For site owners:
data- attributes when next saved, so a page relying on data-toggle stops working. If you trust the author, let them post script in Admin » Preferences.{ as entities, so a key=value reader such as {URL=search|q=O'Brien} gets its value cut at the quote. Shortcodes in templates are unaffected.javascript:, vbscript:, or data: address, or one starting with a shortcode other than an {e_…} folder, unless you may post script. A heading that goes nowhere takes #.comment.php now sends its answers as application/json, so a theme or plugin script that parses them itself stops posting or moderating comments until that theme or plugin is updated.order setting must name columns, such as n.news_datestamp DESC; any other value sorts newest first.e107_images and e107_files. Remove those by hand if you meant to.%, +, or / to the front page after sign-in.[hide] block now opens to a signed-in member who can view the forum and has posted in its thread; none ever opened before.[img] of a site file outside the Media Manager is resized to the Default Image Sizes, 300 by 300 on a new install, where it used to arrive full size. Change them in Admin » Media Manager » Preferences.For plugin and theme authors, any row of the Behavior changes table under For Developers » Changed may matter; these most:
GuzzleHttp\Psr7\* functions and bbcode_shortcodes::renderEmotes() are gone, and on PHP 8 front-end code that relied on e_pref::save() defining admin phrases hits undefined constants.site_path are back on their own media and system folders. They saved uploads, backups, and logs under a stray folder; the dashboard links to the merge. (#6498)& stays an &. Since 2.2.0, text saved by anyone outside "Class which can post HTML" showed & for a typed & (older text until re-saved), and a bbcode link or image to ?a=1&b=2 lost its second parameter. (#6791, 94ef3d1ae4, 73227b2392)LinkedInOpenID, OktaOIDC, and X. (#6372)/faqs could open the admin area instead. (#6502)An "HTML author" below may post HTML but not script: on a new install, any administrator.
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-j4jr-2rjm-rmpq | 9.9 Critical | A member's [video] could run script for every reader with no click, administrators included. |
| GHSA-948v-m8j8-77vm | 9.9 Critical | A member's [glyph] could run script for every reader with no click, administrators included. |
| GHSA-x3h8-x4r6-mc6x | 9.0 Critical | A member's comment or forum post could run script for forum topic visitors, or for an administrator opening the admin comment list. |
| GHSA-pggp-3p94-5mpc | 9.0 Critical | A member's bbcode inside a [table], [hide], or [html] block, such as in a signature, could run script for every reader. |
| GHSA-m88q-h57q-w28x | 9.0 Critical | A forum member's image caption could run script for a moderator or administrator who opened the post to edit it. |
| GHSA-9jr4-3wxc-m2cm | 9.0 Critical | A member's [file] could carry a script link that ran for whoever clicked it. |
| GHSA-x2x3-xr6p-4xc2 | 9.0 Critical | A member's [url] or [link] could get a script address past the link check through an e107 path constant. |
| GHSA-frc7-3r8j-5pw3 | 9.0 Critical | A member's [img] thumbnail could carry a script link that ran when clicked in comments paged in with Next. |
| GHSA-pr63-vf44-2whp | 9.0 Critical | A forum member could run script for every reader of a thread with a quote in its title, through {e_PAGETITLE}. |
| GHSA-hgqv-65ww-wq7p | 8.5 High | Under a Turkish or Azerbaijani locale, an HTML author could keep a script address in a link or frame. |
| GHSA-cmcq-6m83-2w77 | 8.4 High | An HTML author could turn a quote inside an attribute into an event handler that ran for every reader. |
| GHSA-28qc-h8v2-55rf | 8.4 High | An HTML author could put a bbcode inside an HTML attribute that added script for every reader. |
| GHSA-x3v9-7pgp-6c4f | 8.4 High | An HTML author could make a tag inside an [scode] block, or a malformed [code] block, run script for every reader. |
| GHSA-8j93-fv53-8498 | 8.4 High | An HTML author could hide markup in an HTML comment that escaped the cleaning and ran for every reader. |
| GHSA-fhp6-g58h-v9w3 | 8.4 High | On libxml2 before 2.15, an HTML author could hide markup in a processing instruction, running for every reader. |
| GHSA-vj2h-r54v-2533 | 8.4 High | On libxml2 2.14 or later, an HTML author could hide tags in an iframe's text, running for every reader. |
| GHSA-xj8c-p924-m7pq | 8.4 High | An HTML author could end a bbcode tag inside an HTML tag or comment, turning the text after it into script. |
| GHSA-53pg-w64x-rch7 | 8.4 High | An HTML author could hide markup in an editor-inserted bbcode element, running for every reader. |
| GHSA-rqf5-2crw-v646 | 8.0 High | With clickable links opening in the same window (not the default), a member's plain-text script address became a link that ran when clicked. |
| GHSA-46mx-667v-9hwp | 8.0 High | On PHP before 8.2 under a Turkish or Azerbaijani locale, a member's [url] or [link] could get a script address past the check. |
| GHSA-2x4v-8xq6-mp82 | 8.0 High | An HTML author, or a member whose news submission was approved, could run script through a shortcode inside an HTML attribute. |
| GHSA-wpp9-c4vh-p9fq | 6.8 Medium | A member's bbcode image or link could send each reader's security token to another site, enabling token-protected admin actions. |
| GHSA-j5mj-c54p-ccw8 | 5.0 Medium | A member's [img] could make the server list any folder it could read, outside the website included, or an FTP server's. |
| GHSA-33g6-fqff-2w9p | 4.4 Medium | A member not allowed to post HTML could get working HTML into RSS feeds and forum subscription emails. |
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-7w79-c68w-m3hq | 9.1 Critical | An HTML author could run script in readers' browsers through data- attributes, some needing no click. |
| GHSA-x99q-r9j9-wr88 | 8.4 High | An HTML author could make a show-more link run script for whoever clicked it. |
| GHSA-4chr-7w3m-mqq3 | 8.4 High | An HTML author could make a pop-up link run script for whoever clicked it. |
| GHSA-8cw5-vrrm-639v | 8.4 High | An HTML author could make a comment edit button run script for whoever clicked it. |
| GHSA-8698-q2q9-x4xm | 8.4 High | An HTML author could make a rating run script when a signed-in reader hovered over it. |
| GHSA-5hp5-4669-q626 | 8.4 High | An HTML author could write a link that fetched script from another site and ran it on one click. |
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-39qm-9c2q-qvwp | 9.6 Critical | A forum post's attachment list could be read back naming files its poster never uploaded, so a guest could make the forum serve another guest's attachment. |
| GHSA-m95h-mp65-826r | 9.6 Critical | A visitor posting without an account could give a name that ran script for everyone shown it. |
| GHSA-6m74-6r8m-92wx | 9.0 Critical | A forum poster could name an attachment so that it ran script for the topic's readers. |
| GHSA-c32r-5vgh-pxmg | 9.0 Critical | A thread's starter could edit its title to run script for everyone shown it. |
| GHSA-x3x6-97h2-7xp4 | 9.0 Critical | A member could name a thread so that it ran script when an administrator opened its report. |
| GHSA-hj5v-m79p-4cjq | 9.0 Critical | A member could edit their own post to run script for everyone who opened the topic. |
| GHSA-4h6f-79jg-c8fw | 9.0 Critical | A member's post could run script in the editor of an administrator who quoted or edited it. |
| GHSA-cr4j-g226-94fx | 9.0 Critical | A code in a thread's name could run script for everyone who opened its forum. |
| GHSA-gmxq-j944-84rf | 6.5 Medium | A moderator of one forum could act as one in other forums. |
| GHSA-h763-2g75-85x2 | 6.5 Medium | A member who may post in a forum but not view it could read its posts. |
| GHSA-3qc7-83pq-gf73 | 5.4 Medium | A member who could start a thread could overwrite any poll on the site. |
| GHSA-6qr8-c9rm-jc72 | 5.3 Medium | A visitor allowed to view no forum was shown the whole forum index. |
| GHSA-cj4m-q95v-98c6 | 5.3 Medium | Anyone could learn the titles of threads in forums closed to them. |
| GHSA-3w6f-q2fp-3qcm | 5.3 Medium | A forum's page named the sub-forums in it that were closed to the visitor. |
| GHSA-f6wf-9rgv-v6m5 | 5.3 Medium | The forum search named every forum to any visitor, admin-only ones included. |
| GHSA-835r-q7wf-499j | 5.3 Medium | A forum moderated by a class in a class tree gave moderator powers to the wrong users. |
| GHSA-9qxp-m7jg-9xwf | 4.3 Medium | A member could quote a post into any open thread, even one in a forum hidden from them. |
| GHSA-xj38-4v6m-2c83 | 4.3 Medium | The author of any reply could rename its thread and unpin it. |
| GHSA-pg3x-w47r-26c2 | 4.3 Medium | Members could start topics in forums where only staff may start them. |
| GHSA-hg7j-grmq-hwwv | 4.3 Medium | A moderator could split posts into a forum they do not moderate. |
| GHSA-fr3x-g52f-pg45 | 3.1 Low | A member could save a post to a forum or thread their permission was not checked against. |
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-vfc3-x99c-2j47 | 9.6 Critical | Anyone could craft a comment.php link that ran script for whoever opened it while signed in. |
| GHSA-g7rc-4jvq-27m9 | 9.6 Critical | Anyone could send a link that ran script, through the advanced search's Author field, for whoever opened it, administrators included. |
| GHSA-7232-gr39-h92m | 9.0 Critical | A member's thread name, comment subject, or chatbox message could run script on List New Items and its menus. |
| GHSA-96vv-8fmj-6xfq | 5.3 Medium | comment.php showed visitors the titles and comments of items their own pages withheld. |
| GHSA-c3hr-jgwg-r7xv | 5.3 Medium | The latest comments menu showed comments left on password-protected pages. |
| GHSA-58j5-4h2g-fh4c | 5.3 Medium | The comments search showed anyone comments, with the item's title, on news items and downloads they could not read. |
| GHSA-xpgr-mpvw-m63w | 5.3 Medium | The comments search showed comments an administrator had blocked or not yet approved. |
| GHSA-2rfr-36m8-hjc2 | 4.3 Medium | A member could page through the comments of items they may not read. |
| GHSA-xf8f-w5mv-hcg3 | 4.3 Medium | A member could post comments onto items they may not read. |
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-ff9g-2q9w-43gh | 9.6 Critical | Anyone could send a link to an admin list or a search whose page links ran script for whoever opened it. |
| GHSA-prf8-95f7-7q86 | 9.0 Critical | A member's news submission approved unedited, or any page, news item, or FAQ question, could run script for readers through shortcode parameters. |
| GHSA-xpfc-85q6-4659 | 8.0 High | A page or FAQ question naming {IMAGESELECTOR} could run script when a reader picked an image. |
| GHSA-hxpw-337x-cj7g | 8.0 High | A page or FAQ question naming {IMAGESELECTOR} could make its preview link run script on a click. |
| GHSA-6r9c-6jvj-5wrp | 8.0 High | Another website could run script in an administrator's browser through any page whose text names {IMAGEPREVIEW}. |
| GHSA-mc79-4frh-x2p7 | 8.0 High | A page or FAQ question naming {UPLOADFILE} could run script when a reader clicked Add Another File. |
| GHSA-994c-jv82-j79r | 5.9 Medium | A member's news submission, once published, could send each reader's security token to another site. |
| GHSA-55xx-wjmc-63jc | 5.4 Medium | A page or FAQ question naming {BREADCRUMB=...} could print markup from a crafted link, running its script. |
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-r42m-hch3-vg4g | 9.6 Critical | A crafted link could run script in a signed-in administrator's browser through the PHP Errors debug panel, even with debugging off. |
| GHSA-fmrc-cg23-8fmv | 9.6 Critical | A crafted link to the news page could run script in a signed-in administrator's browser through its debug table, even with debugging off. |
| GHSA-q899-38jg-q5hm | 9.6 Critical | A crafted link could run script in a signed-in administrator's browser through the Shortcodes / BBCode debug panel, even with debugging off. |
| GHSA-pwhr-95w9-f629 | 9.0 Critical | A member's comment, signature, or submitted news could run script for an administrator who opened it for editing. |
| GHSA-v773-ph53-fmx6 | 8.4 High | An administrator limited to pages could run script for the main administrator through a page title in an admin list. |
| GHSA-p622-mg4c-fpc6 | 6.8 Medium | An administrator who may manage users could read every account's password hash from the user list's search suggestions. |
| GHSA-g676-96h8-qgv5 | 6.0 Medium | A delegated administrator could make an admin form write a row it never loaded, such as the main administrator's account. |
| GHSA-r7xc-wvxg-23j5 | 5.5 Medium | Any administrator, whatever permissions they held, could change any site preference through an admin page's options. |
| GHSA-h657-rx62-qm6j | 4.9 Medium | An administrator holding any news permission could rewrite news items inline and reorder every news category. |
| GHSA-ccrh-5wjq-72mq | 4.4 Medium | An administrator holding only the news category permission could rewrite any news item by front-end inline editing. |
| GHSA-78j8-h8ww-p8wf | 3.3 Low | On PHP before 8.2 under a Turkish locale, a delegated administrator could reach an admin route its permission refused. |
| GHSA-3cjg-6rq3-m3qr | 2.7 Low | An administrator allowed only to manage bans could change the ban messages and durations reserved to the main administrator. |
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-2vhc-52q2-2j5f | 8.4 High | An administrator limited to one area, such as Navigation, could save a javascript: link that ran for whoever clicked it. |
| GHSA-fm7r-8pvc-x36q | 8.4 High | An administrator limited to Navigation could add a popup link whose address ran script for whoever clicked it. |
| GHSA-8wmw-xrxv-hvrm | 8.4 High | An administrator limited to pages could put script in a chapter's custom field settings, run when the main administrator edits one of its pages. |
| GHSA-fhcg-cf34-cg8h | 8.4 High | An administrator limited to the hero plugin could put script in a slide's bullets or buttons, run for every home page visitor. |
| GHSA-c8wm-82cq-3qj2 | 8.4 High | An HTML author could plant a menu link that, clicked in the Menu Manager, ran another site's script as the administrator. |
| GHSA-chxg-8grm-hpmq | 8.0 High | With screentips on, a link description could run script for every visitor of a theme using the older {SITELINKS} menu. |
| GHSA-86rc-hq8m-mwwr | 7.6 High | An administrator limited to pages could put script in a custom page field, run for anyone viewing a layout that prints it. |
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-3gqj-7q5v-m8wg | 9.6 Critical | On sites with public uploads switched on, an uploader could run script in the browser of an administrator reviewing Admin > Uploads. |
| GHSA-7xc8-m3cv-mq65 | 7.2 High | An administrator could store a Media Manager upload under a file type the site does not allow to be uploaded. |
| GHSA-v6mf-xx46-236j | 3.8 Low | An administrator with Media Manager access could move a file from elsewhere on the server into the public media folder. |
| GHSA-3c5x-q658-vx68 | 3.8 Low | An administrator with Media Manager access could delete or overwrite files outside the media folder. |
| GHSA-69r3-7r4c-6gmw | 3.8 Low | An administrator with Media Manager access could delete files outside the avatars folder. |
| GHSA-w39w-6mq9-p36q | 2.7 Low | An administrator with Media Manager access could import a file into folders outside the media folder. |
| GHSA-768c-62jm-23j8 | 2.7 Low | An administrator with Media Manager access could change a database query with a quote in a file's name. |
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-cx6q-399j-w6fw | 9.0 Critical | A download's name, set by a downloads administrator or an accepted public upload, could run script for an administrator opening its edit page. |
| GHSA-hhh4-9hgp-84w8 | 8.4 High | An administrator who manages downloads could give a mirror an address that ran script for any administrator opening that download's edit page. |
| GHSA-7hgq-9836-h28c | 8.0 High | Where members may ask FAQ questions, an answered question could run script, through its share buttons, for everyone opening the FAQ page. |
| GHSA-x33v-p7mw-q88f | 5.7 Medium | Anyone who could get a {MENU} shortcode into displayed text, such as an answered FAQ question, could read the database through a news menu. |
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-45hh-fwg8-24j2 | 8.4 High | An administrator holding only the social plugin's permission could run script on every page through a social page address. |
| GHSA-2vgc-f528-4vvf | 8.4 High | An administrator holding only the social plugin's permission could run script in other administrators' sessions, the main administrator's included, through a stored provider or file name. |
| GHSA-gg38-5hqf-7h6r | 7.6 High | An administrator holding only the social plugin's permission could run script through the Facebook app id on pages that load Facebook's SDK. |
| GHSA-q9p8-h9h3-r5v5 | 6.1 Medium | When social login has an OpenID provider enabled, anyone could write a sign-in link that put their script on the page a visitor lands on. |
| GHSA-p4fv-hmmr-38p8 | 4.9 Medium | An administrator holding only the social plugin's permission could make social login read files on the server, e107's configuration file included. |
| GHSA-mg3f-xg6g-g3pc | 3.8 Low | An administrator holding only the social plugin's permission could choose the classes, files, and servers social login uses, and what it unserializes. |
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-6mxv-9x4j-8rhr | 8.3 High | On some nginx and PHP-FPM setups, a crafted link could rewrite the link and stylesheet addresses on a page and run script for whoever opened it. |
| GHSA-g2gx-997c-mc67 | 7.1 High | Any page an administrator visited could make the plugin manager install, repair, or upgrade a plugin by hiding a made-up token from e107's check. |
| GHSA-wjmf-r4mf-m9vg | 6.5 Medium | On a site at the root of its host, a crafted link could make the navbar sign-in form send a visitor's username and password to another host. |
| GHSA-cvhc-885w-mm2m | 6.5 Medium | Where the signup form's Password field was set to Hide, anyone could replace the password of an account still awaiting activation. |
| GHSA-93ff-wc89-mhjf | 6.3 Medium | On an HTTPS site, any member could set a profile picture that made an administrator's browser carry out admin actions, such as installing a plugin. |
| GHSA-6wf7-qwj9-hr3r | 5.9 Medium | A page on a neighboring subdomain could make an administrator's browser carry out admin actions, such as installing a plugin. |
| GHSA-hq36-v99x-77g4 | 5.9 Medium | A neighboring subdomain could choose the page an administrator landed on after signing in, and make that page carry out an admin action. |
| GHSA-9mrp-hhpm-ph93 | 5.3 Medium | The activation resend form showed anyone the email address of an account awaiting activation, given only its user name. |
| GHSA-p8q2-x72j-8mvj | 4.3 Medium | With friendly URLs on, a link from another site could make a signed-in member carry out a front-end action, such as blocking a message sender. |
| GHSA-9f6g-4wp7-whmj | 4.3 Medium | A link from another site could make a member who then signed in on that page carry out its action, such as blocking a message sender. |
index.php?logout&e-token={E_TOKEN}: e107 fills in each visitor's token, in navigation links only. (#6303){.{ as entities. Pages, news, and other stored text are covered; templates are not. A shortcode reading key=value pairs, such as {URL=search|q=O'Brien}, now gets its value cut at the quote. (GHSA-prf8-95f7-7q86)data- attributes. A page or news item relying on data-toggle stops working when next saved; stored content is not changed. Since 2.3.0 a new install lets no one post script, main administrators included; choose who may in Admin » Preferences. (GHSA-7w79-c68w-m3hq)[code], [scode], [table], [html], or [php] block shows as typed, and another bbcode shows its HTML as text.[hide] block that opens inside an HTML tag and closes outside it shows, as typed, to every reader.] falls inside an HTML tag or comment shows as text, and such a [hide hides nothing.[img] of a site file outside the Media Manager asked for its thumbnail with a broken size, so it got the full-size file. It now follows the Default Image Sizes (300 by 300 on a new install). (73227b2392)After updating, run the forum plugin's update when the admin area offers it. It changes no post, but counts edited posts holding markup their author or last editor may not post; some may be legitimate HTML. Check those whose post_user or post_edit_user may not post HTML, without viewing them on the forum as an administrator. If the author may not post HTML, saving the post from Edit strips it; fix the rest in the database. (GHSA-hj5v-m79p-4cjq)
This read-only query lists every edited post holding <; the posts to check are among them (adjust the e107_ prefix):
SELECT post_id, post_thread, post_user, post_edit_user FROM e107_forum_post WHERE post_edit_datestamp > 0 AND post_entry LIKE '%<%';
Forum moderators are the users who hold the moderator class, including members of the classes that inherit it. On the tree "Set a default user hierarchy" builds, a forum moderated by Admins and Mods is now moderated by its members and the main administrators, no longer by every administrator; add an administrator to Admins and Mods to keep them moderating it. A forum moderated by Main Admin is now moderated by main administrators alone, who moderate every forum. (GHSA-835r-q7wf-499j)
A new topic needs Thread Creation Class and Post Permission both. (GHSA-pg3x-w47r-26c2)
A forum a member may post in but not view takes no posts from them. (GHSA-h763-2g75-85x2)
Only an opening post's edit renames a thread, and a non-moderator's edit no longer unpins it. (GHSA-xj38-4v6m-2c83)
Split and move list only forums the moderator moderates and can view. (GHSA-hg7j-grmq-hwwv)
"Enable polls for" refuses a poll, not just its form. It ships set to No One. (GHSA-3qc7-83pq-gf73)
A sub-forum is closed to whoever its forum is closed to, whatever its own Visibility says. Clear the Sub-forum setting of one meant for a wider audience. (bf9538c730)
A [hide] block opens for a signed-in member who can view the forum and has posted in its thread. None ever opened before, so every stored block now opens to them. Hidden text also stays out of the RSS feed and the latest forum posts menu. (#6782, f28d0802aa)
Thread names render only inline bbcode, such as bold, color, and size; other bbcode keeps only its text, and HTML stored in a name shows as typed. (GHSA-c32r-5vgh-pxmg)
Where uploads allow SVG, an SVG attached to a forum post is offered as a download, not shown as a picture. (#6748)
application/json. Sites defining e_HTTP_STATIC should clear their CDN's cache. (GHSA-vfc3-x99c-2j47)5 covers pages and chapters, J menus; deletes need J1, page options the main administrator.Link fields refuse a javascript:, vbscript:, or data: address, or one starting with a shortcode other than an {e_…} folder, unless you may post script: Navigation, social links, Custom Button URL, custom page URL fields, Linkwords, download mirrors, feature box images, hero buttons, and public uploads. Stored addresses are kept, so review yours; a heading that goes nowhere takes #. (GHSA-2vhc-52q2-2j5f)
Custom page fields and hero slides no longer run script their author may not post. Field values and hero bullets saved before 2.3.13 keep their formatting but lose script and event handlers, with no re-save, except icon, tag, checkbox, and progress-bar values, which show HTML only when saved by someone who may post script. Where you narrowed who may post HTML, kept formatting can include some their author may not post. Old field labels and hero buttons show HTML as text until changed. (GHSA-86rc-hq8m-mwwr, GHSA-fhcg-cf34-cg8h)
With caching on, a fragment holding a visitor's security token is not cached, so the comment form is built fresh. Clear Admin Area » Settings » Cache once after upgrading. (#6307)
_ . , : = & -. The address remembered for after sign-in now comes only from the session, so a members-only site sends a visitor whose remembered query held %, +, or / to the front page. (GHSA-9f6g-4wp7-whmj, GHSA-hq36-v99x-77g4)Social login now ignores stored provider settings that only a site's own code should set. The social plugin saves provider settings only under names Hybridauth supports. Telegram's form no longer offers a callback field. Nothing stored is rewritten; a social sign-in in progress during the upgrade may have to start over. (GHSA-mg3f-xg6g-g3pc, GHSA-p4fv-hmmr-38p8)
Social login checks the provider's TLS certificate, except for OpenID 2.0 providers such as Steam, and offers LinkedInOpenID, OktaOIDC, and X. The bundled Hybridauth is now 3.13.0. A host without a usable CA bundle, which already fails plugin downloads, now fails social login too. (#6372)
Signed-in pages no longer load the social login library just to ask whether social login is on. Answering used to parse every bundled provider file on every signed-in page, even with social login off. (#6514)
order setting must name columns. The latest-news and news-grid menus accept only a list of columns, each optionally followed by ASC or DESC. Any other value now sorts newest first. (GHSA-x33v-p7mw-q88f)fastcgi_split_path_info with PATH_INFO, add try_files $uri =404;, or set cgi.fix_pathinfo=0; in a named location that hands unknown addresses to index.php, add fastcgi_param SCRIPT_NAME /index.php;, with the site's folder in front if it has one./~user/ address, a web folder whose name holds anything but ASCII letters, digits, and - . _ ~ is percent-encoded in the site's address, and the site's cookies are set on the folder above it. Every application under that folder receives them. Where the name holds a comma, a sign-in from before the update survives signing out until its cookie expires, within a day by default.e107_config.php. A $mySQLport line was ignored. If yours sets one, check that your database server listens on it. (#6663)& in a [link], [url], [file], [email], [img], [flash], or [stream] address was encoded too often, so ?a=1&b=2 lost its second parameter. An apostrophe in an [img] alt text or caption no longer shows as an entity. (94ef3d1ae4, 73227b2392)& is stored once. Since 2.2.0, text from anyone outside "Class which can post HTML" showed & for a typed &. Older text loses the extra level when next saved, except inside [code], where you remove it by hand. (#6791)[color=#ff0000] stays as typed, not a bare [color]. (#6444)[size2]hello[b]world[/size] no longer vanishes. (#6573)[scode] keep the quotes around their attribute values.+ in a field after an editor field, such as a page's meta title, is no longer saved as %2B.[sanitised] block no longer fails; it shows ##Sanitised##.a < b beside markup keeps its < on hosts with libxml2 2.9.10 or older, which dropped it and what followed.[quote] block; one with bold text or a link was quoted as plain text. (#6782){UPLOADFILE} and upload.php stay closed while public uploads are switched off, as on a new install. (a7f86886a9)+. (#6330, #6313, #6357, #6439)# after a space finds matches on the default sort method. (#6478)/faqs could open the admin area for administrators and the admin login for everyone else. After upgrading, delete e107_plugins/faqs/controllers/ if it is still on disk, then run the FAQs update in Admin » Plugin Manager, which rebuilds the URL settings. (#6502)& in an author's name no longer breaks the feed. (#6473)login.php already closes with User Registration/Login disabled and no social login. (#6501)42, is no longer refused a link while that account has a reset outstanding.wget, curl -o, lwp-download, or onmouse no longer ends on a blank page. The request filter answered any posted value or query string holding them with an empty 400 error. (#6759)user_ban_failed_login, only when it adds one. (#6771)/ again. The Site Information tab silently refused to save it. Until it includes the site's host, the admin home warns main administrators that links in mail sent by scheduled tasks will be relative. (#6117)plugin.xml names comment types in <commentID> no longer deletes every comment on the site, nor fails with a single type on PHP 8. Its own comments are now removed only when its tables are, and kept when they are kept. No bundled plugin declares any. (#6780)e107_system is on another file system, such as a container volume of its own; it used to stop at "Couldn't Move". If the move into place fails during an update, the installed copy is put back. (#6713)stmp.jpg, whose second to fourth characters are tmp. (#6777)site_path in e107_config.php is back on its own media and system folders. From v2.3.4 every such site saved uploads, backups, and logs under the folder 5058f1af83. If files saved since are missing, follow the dashboard's link to Database Utilities » Multi-Site and merge the folder marked Known bad. (#6498)og:url links are full addresses again where the Site URL holds only a folder, as a subfolder install's does. (#6601)e_parse::toBBCodeUrlAttribute() and e_parse::bbcodeTextAsTyped() encode a bbcode's values once. Give toBBCodeUrlAttribute() a bbcode's stored address; it returns '' for a refused scheme. bbcodeTextAsTyped() gives back any other parameter as typed, for the bbcode to encode once. A bbcode encoding $parm as stored shows & for &. (GHSA-9jr4-3wxc-m2cm, GHSA-x2x3-xr6p-4xc2)
| Name | What it does |
|---|---|
e_parse::shortcodeModifiersForAuthor() | Adds scparse_off unless the author is an administrator. |
e_parse::toRssForAuthor(), notify::sendForAuthor() | The same for feeds and notifications; toRss() and send() are unchanged. |
e_parse::languageTerm($name) | Static. A defined language term's value, else null. |
e_parse::attributeValue() | Now public. Encodes a value for an attribute. |
e_bbcode::getRegistered(), isRegistered() | Every registered bbcode and who provides it; whether a tag names one. |
eShims::strtolower(), eShims::strtoupper() | Static. ASCII-only case folding. |
{E_TOKEN} | In a stored navigation link, filled with each visitor's security token. |
e_admin_controller_ui::$listScope | The rows of a shared table an admin list owns, applied to the list, edit load, inline editor, and sort; its array form also pins those columns. |
'linkAddress' => true | Field option. Refuses a script address on every save, inline included, from a saver outside post_script unless it is unchanged; the form shows the stored bytes, and a refused inline edit answers 400 with the reason. |
e_parse::maySaveLinkAddress() | Whether the current user may save a link address. |
e_admin_controller_ui::maySaveLinkAddresses() | Protected. Asks maySaveLinkAddress() for each of a save's link fields, and refuses the save with an error on each field that fails. |
e_parse::isUnchangedPost() | Tells an untouched post from a changed one. |
'search' => false | Field option. Keeps search, filters, and sorting off a field, as users.php does for the password; the ajax filter suggests only searched columns, matching the term as text. |
comment::canReadComments() | Whether the visitor may read an item's comments. |
FORLAN_HELP_IMAGE_DISPLAY, FORLAN_HELP_ICON_DISPLAY | New forum admin phrases. |
e_file::webOnlyCurlOptions() | Curl options limiting a request and its redirects to HTTP and HTTPS; throws RuntimeException where curl cannot be limited. |
e_file::currentUserMayUsePublicUploads() | Whether public uploads are on and the current user is in the upload class. |
e107.callbacks.isWebAddress(url) | JavaScript. True for an http, https, or page-relative address. |
e_search::authorId($author) | The user id an advanced author filter sent, or 0. |
e107\Database\IdentifierFilter | identifier(), direction(), orderBy(), and filterOrderBy() check SQL identifiers and sort fragments, answering false or a default outside the grammar. |
POLLAN_POST_POLL, BNRLAN_HELP_*, BNRLAN_PLACEHOLDER_*, BNRLAN_TITLE_REMOTE_IMAGE, BNRLAN_MENU_MANAGER_CONFIG | New phrases: the forum poll heading, replacing the undefined LAN_4, and the Banner admin texts, with English fallbacks. |
e107\Flood\SourceGate | Rations an action per source: isClosedTo(), record(), forget(). |
e107::invalidateOpcache($file) | Drops OPcache's copy of a PHP file e107 replaces or deletes. |
e107::cookiePath($path) | Static. A web path cut above its first folder holding a %; scope a plugin's own cookie with e107::cookiePath(e_HTTP). |
e_parse::toDBWithoutScript() | Converts as toDB() stores for an author who may post HTML but not script. |
e_parse::toJsUrl(), sitelinks::popupScript() | Build a javascript: address and a popup call. |
e_bbcode::hideBlocks(), hideBlockTree() | A text's [hide] blocks, and every one, nested ones included. |
sitelinks::fillToken() | Fills a stored navigation link's {E_TOKEN}. |
e107forum::threadNameHtml(), withoutHiddenBlocks(), canAttachPoll(), mayReadHidden() | The forum's thread-name renderer, [hide] stripper, and poll and [hide] checks. |
forum_attachments::label(), asServed(), isSvg(), fileName() | Static. |
e107\SocialLogin\SessionStorage | Social login's Hybridauth session storage, which never unserializes a value. |
media_admin_ui::mediaFolderPath() | An item's expanded address, '' when its folder is missing, or false when it lies outside the media folder. |
e107\Admin\AddonUpdateStatus, admin_shortcodes::renderAddonUpdateNotice() | The dashboard's add-on update answer, kept site-wide, and its notice. |
e_search::getCommentHandlerPath(), isCommentHandlerAvailable() | A comment search handler's path, and whether its plugin can serve it. |
e_plugin::isInstallRequired(array $meta) | Static. Whether parsed plugin.xml asks to be installed. |
e107\Database\TreeOrder::positions() | Depth-first sort keys and depths for an adjacency list; selectTree() uses it. |
private_message::QUEUE_TYPE, QUEUE_TYPE_RUNNING | gen_type of a queued bulk PM send, and of a claimed one. |
e_online::defineUnsampledState() | Defines the online constants as empty strings where goOnline() did not run. |
banlistManager::importBanlistCsv() | Imports a ban list CSV; returns counts and per-line errors. |
redirection::isSafeLanding() | Whether sign-in may land on an address, judged by its spelling, not its host. |
e107\Storage\SiteFolder, SiteFolderScan, SiteFolderMerge | Site folder pairs, the scan, and the merge. |
Vendored guzzlehttp/psr7 is 2.13.1, so the global GuzzleHttp\Psr7\* functions are gone and a plugin calling them fatals. Use the static methods on Message, Utils, Query, Header, and MimeType. psr7 and what it requires are downgraded to run on PHP 5.6; Hybridauth 3.13.0 needs no downgrade. (#6372)
e_HTTP is cut at the running script and percent-encodes all but letters, digits, and - . _ ~ / %, unless its folders are the install's own on disk. e_REQUEST_URI and e_REQUEST_HTTP start with one slash, never /\ or /&, and e_SELF and e_REQUEST_URI spell the site's folders as e_HTTP does. (GHSA-6mxv-9x4j-8rhr, GHSA-wjmf-r4mf-m9vg)
e107\User\LogoutConfirmation asks a member who opens a logout link without a token whether to log out. index.php serves it through e107\Routing\RouteTable with e107\Http\Request, Response, and e107\Theme\Page, from the new logout_template.php, which a theme can override. A customized index.php that does not set $_E107['route_table'] keeps the old refusal. (#6753)
Inside an HTML attribute, print a custom page field with mode=raw, as in alt="{CPAGEFIELD: name=x&mode=raw}", and never a rich text or text area field or a hero bullet's text: printed normally, a value can end the attribute and run script, whoever saved it. (GHSA-86rc-hq8m-mwwr, GHSA-fhcg-cf34-cg8h)
SHOW COLUMNS probe, and Who's Online classifies each user agent once.tmp table is pruned on one request in a hundred, and a database session close no longer collects expired sessions each time.strftime() shim reuses its IntlDateFormatter, and e_media::getGlyphs() builds each precompiled icon set once per request.| Name | What changed |
|---|---|
bbcode_shortcodes::renderEmotes() | Removed, so a theme or plugin calling it fails. r_emote() draws the emoticon panel. |
e_parse::toAttribute(), replaceConstants() | toAttribute() expands only web folder path constants, and encodes their values. replaceConstants() by default expands only web and server path constants; $all = true expands the rest. |
e_parse::toHTML() | A strip list that names hide strips [hide] blocks with the rest; they came back as the bbcode's box. |
e_parse::toDB() | A numeric fifth argument, a user id, only forced encoding; now that user's post_html and post_script decide what is kept. |
text_truncate(), toEmail(), toForm() | Keep < and > encoded where they used to decode them, and text_truncate() keeps every entity. |
| Shortcode parameters in content | Quotes, angle brackets, and { arrive as entities. |
| A field whose whole text names a constant | Converts only when the constant is a language term. |
toWYSIWYG() hook | Receives an admin UI bbarea field's text, and the news body's, encoded. |
e107Email {BODY} | Receives the body unparsed: render it first, or set mail_body_author on a bulk mail. |
{LAN=X} | Prints LAN_X, or X only when it is shaped like a language term: a capital first, word characters only, and either LAN inside or a final digit. Other constants print nothing. |
{NEXTPREV} | Re-encodes its address and drops a non-web one. pagetitle entries lose tags and shortcodes. bullet prints as text unless it is one <img> under THEME_ABS with only alt and class, like core's. |
{BREADCRUMB=name} | Reads only an array global with a plain variable name. |
{IMAGESELECTOR}, {IMAGEPREVIEW}, {UPLOADFILE} | Quote their values as JavaScript strings and refuse a non-web preview address. {IMAGESELECTOR} ignores undocumented parameters. |
{PLUGIN=}, {MENU=}, {PAGE_NAVIGATION: name=} | {PLUGIN=} and {MENU=} take only word characters and hyphens; name= takes only page_metatitle, or menu_title for the menu's class. |
{SEARCH_ADV_ID} | Binds the advanced search template's label; an overriding theme still on for="t" keeps the old binding. |
| Required-field marker | e107.css draws the asterisk. A theme that draws its own may show two; .required:empty::after { content: none } turns core's off. |
| Right-aligned dropdowns and floats | Core uses dropdown-menu dropdown-menu-right dropdown-menu-end, and right floats pull-right float-right float-end. Menus lost pull-right, so a theme bundling Bootstrap older than 3.1.0 loses their alignment. |
| bootstrap5 Font Awesome | Loads as CSS, not script, so CSS aimed at svg.svg-inline--fa stops matching there. |
e-expandit, e-comment-edit | Look their target up as a selector only. |
e-modal | Opens only an http, https, or relative address, and prints data-modal-caption as text. |
e-ajax | Fetches only from this site, and inserts an answer only if it ends there as text/html or application/json. data-loading-icon is applied as class names. |
.e-rate | Its status line and vote count must be its siblings, as core writes them. A rating without data-hint is skipped. |
comment::enter_comment() | Refuses items the poster may not read, and comment types core does not model, such as a plugin's own. |
| comment.php AJAX answers | application/json; read them with dataType: 'json', not $.parseJSON(). |
e107forum::forumGetForumList() | Returns false to a caller who may view no forum, and to an object built with new e107forum(true); forumGetForumList(true) returns every row. |
poll::submit_poll() | Mode 1 needs poll admin rights. |
e_list.php heading and info | Printed as built; a section that wants them rendered renders them itself. |
e107MailManager::sendEmails(), private_message::pm_send_notify() | Return false when the send fails. |
| Admin UI routes | Names fold case in ASCII; an unknown action falls back only to a granted default; an ajax page runs only on ajax; actions reach only action methods. |
| Admin UI saves | Follow the route: create inserts; edit and inline update the loaded row or refuse. Inline, sort, and a plugin's batch_<plugin>_<command> need the edit route. |
e_admin_ui::PrefsSaveTrigger() | Saves only keys $prefs declares; declare every option you post. |
e_front_model::getIfPosted() | Writes a posted string's & as &, so a form redrawn after a failed save shows what was posted, entities included. |
| Front-end inline editing | Refuses a map without perms; inline.php used to read perm. Maps are kept under a new session key, so inline editing works once a page is loaded after the upgrade. |
| Menu Manager options | A .e-menumanager-option link loads only from menus.php, and mode=conf redirects only to a config.php or *_menu_config.php. |
e107plugin::manage_comments('remove', ...) | Takes one type or a list; returns false for an empty list instead of deleting every comment. |
e_file::curlOptions() | Throws RuntimeException where curl cannot be limited to HTTP and HTTPS. |
e_user_provider | Builds Hybridauth on first use; its protected $hybridauth is null until then. Only stored settings are filtered, not a configuration passed to the constructor. |
e_user_provider::login() | A refused sign-up (code 5) throws with an empty message; its reasons go to the message stack. |
| Comment search handlers | Skipped unless one sets $comments_class[key] or $comments_where[key], saying who may read its items, as the poll plugin's does. Blocked comments are excluded. |
An addon's author search filter | Receives the picked member's user id. e_search::authorId() returns it, or 0 for typed text; the _blank plugin's e_search.php shows the pattern. |
e107_handlers/search/ | Six dead legacy handlers are deleted. An upgrade leaves the old copies on disk. |
e_pref::save() | No longer loads the admin language pack, which a front-end, cron, or CLI request saving a preference, such as a new most-online record, gained. Front-end code reading admin phrases must load them or use defset(). |
e_db_pdo | Instances share one connection per set of parameters, so session state set through one (temporary tables, SET SESSION, user variables, locks, transactions) is seen by the others. For a session of your own, call connect() with $newLink = true. |
| Preference cache files | JSON wherever JSON reads back the identical array. e107::unserialize() reads both forms. |
dbError() | Answers alike on both backends: null, or "$from :: $text". |
xmlClass::getLastErrorMessage() | Always a string, naming the parse errors or a failed fetch's HTTP status. |
selectTree() | Same _treesort and _depth; needs only SELECT; refuses bad identifiers. |
e_news_tree::loadJoin() | db_order must be columns with optional ASC/DESC, else newest first; db_limit is cast to integers. |
ecache::setMD5() | Hashes text with the site's web path. Code that builds an entry's file name from md5($text) should use retrieve() and clear() with its tag; setMD5($hash, false) is unchanged. |
e_array::unserialize() | Reads a row as written before trying it unslashed, so a value containing 0 => ' no longer empties its row. |
class_list() | Deprecated, because only its user-id form includes inherited classes. Use e_user_model::getClassList(). |
| CHAP | e107_web/js/chap_script.js is deleted, UserHandler::CheckCHAP() and e_core_session::challenge() are deprecated and unused by core, and userlogin::login() ignores $response. |
caption, html, or help no longer raises an undefined-key warning. (#6214){LOGO} no longer raises a diagnostic on a legacy theme whose logo file is missing or unreadable. (#6148)e107_db_debug::log() and dump() name a top-level caller {main} instead of warning, and stop printing ()(). (#6378)upload.php no longer reads form fields a first visit has not posted. (#6361)http_build_query() gets '', not null. (#6445)usubstr() and html_truncate() keep the text they should. usubstr() without a length returned '' below PHP 8; html_truncate() with $exact = false lost text that had no space. (#6464, #6475)parent::__construct(), or never calls it, no longer fatals on PHP 8, and keeps the value. (#6658)cleanHtml() no longer fatals on the call after a <pre> inside a stripped tag, nor e_form::uc_checkbox() on an array value. (#6801)optArray leaves the batch and filter menus with a debug line instead of breaking the list page. (#6403)defset(), so a pack missing one no longer stops the page on PHP 8. (#6526)selectTree() creates no stored functions, and siblings sharing an order no longer interleave. (#6297)isTable() sees tables the same instance creates, drops, or renames. (#6374)tables(), isTable(), and hasLanguage() no longer list another prefix's tables. (#6672)e_db_mysql::lastInsertId() answers for the instance's own last statement. (#6715)multi_row plugin or theme preference keeps its own cache file and object. (#6338)e107MailManager reads a queued email back as saveEmail() stored it; a raw apostrophe in its sender, copies, or attachment emptied all three. (34591e2243)e_file::copy() passes its options into subfolders and returns false at the first entry it cannot copy; the theme converter's clone stops there. (#6713)e_file::get_files()'s standard filter skips only names that start with .tmp. (#6777)goOnline() is skipped, as on AJAX, the online constants are empty strings, so online.php no longer fatals.MEMBERS_ONLINE is 0.{ONLINE_TABLE_MEMBERS_ONLINE} and {ONLINE_TABLE_GUESTS_ONLINE} were swapped.Thanks to
/ and the admin area an old language pack took down (#6101); towget and the pull request that removed it (#6759, #6762); and toFull changelog: v2.3.12...v2.3.13
Caution
v2.3.12 is a security release for sites on v2.3.11 or earlier. Upgrade from any 2.x at or below v2.3.11. If your site tracks the master branch, you are already past v2.3.12, so installing it would be a downgrade. v2.4.x is planned to be the next forward step.
Important
Upgrade immediately. This release closes twenty-one security advisories outright and narrows a twenty-second, which needs one setting from you as well as the update.
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-f7x7-v438-qmf4 | 9.9 Critical | A member's post could attach a script to an ordinary image and run it for every reader, with nothing to click. |
| GHSA-48cx-ccvq-52mc | 9.9 Critical | Anything a member could write in BBCode could run script in another visitor's browser. |
| GHSA-9gr7-g6pw-5244 | 9.8 Critical | Anyone could sign in as a social-linked account, an administrator's included, with no password. |
| GHSA-m8v8-wc99-3h82 | 8.8 High | A member could give themselves a second way in that a password change does not revoke. |
| GHSA-pq2m-9gxf-64x9 | 8.8 High | A member who knew only their own password could write any column on their own account, including the two that make it an administrator. |
| GHSA-7v5h-vrhj-wjf5 | 8.1 High | One read of the database forged a working login for every account on the site. |
| GHSA-vr9h-v6xq-4m35 | 8.1 High | The automatic ban ships switched on and never counted a wrong password. |
| GHSA-c33m-2hph-47p4 | 7.4 High | Comparisons in the login paths accepted values that were not the ones expected. |
| GHSA-pf37-7c5m-mpg3 | 7.2 High | An administrator holding only the Languages permission could put PHP of their choosing into a file every visitor loads. |
| GHSA-22m8-h4gh-vfh3 | 6.8 Medium | Signing in did not change the session identifier, so one known beforehand was known afterward. |
| GHSA-pw4f-4544-2mv2 | 6.5 Medium | A handler meant only for the command line answered anonymous web requests as the main administrator. |
| GHSA-9qgj-v67f-r22q | 6.5 Medium | Anyone holding a live session could set a new password without knowing the old one. |
| GHSA-396r-g8m8-w9xx | 6.1 Medium | A crafted link to the File Inspector ran script in the browser of the administrator who opened it. |
| GHSA-3357-4wp5-w5fp | 6.1 Medium | A contact form open to guests handed posted values back to the page as markup. |
| GHSA-cg88-cq8v-w6g6 | 5.4 Medium | A page an administrator merely looked at could run database operations on their site. |
| GHSA-8xcj-7wmw-r939 | 5.4 Medium | A page anyone could link to deleted a member's private messages and rewrote their block list. |
| GHSA-72mc-5q6j-2fqq | 5.3 Medium | One anonymous request returned every user name on the site, banned and unvalidated accounts included. |
| GHSA-qj68-pgg7-hf3p | 5.3 Medium | Anyone could publish FAQ entries without an account. |
| GHSA-f2q2-hchc-h3wx | 4.3 Medium | One member could vote on a star rating over and over and set the average to anything. |
| GHSA-vhf5-8cfh-jr22 | 4.3 Medium | Comments switched off on an item did not stop a member commenting on it. |
| GHSA-hwmh-m57x-w5h2 | 3.8 Low | The user manager did not apply the rules that protect administrator accounts to its batch route or to its single-row Unban and Delete controls. |
| GHSA-w24r-4r8j-vqgc | 3.7 Low | A forged Host header was reflected into the addresses the site prints. Also needs a setting from you, see below. |
The two worst need only a member account. GHSA-f7x7-v438-qmf4 and GHSA-48cx-ccvq-52mc between them reach every field that accepts BBCode: forum posts, comments, profile signatures. An administrator who views that content can install plugins, so the ceiling is a full site takeover.
Several need no account at all, so any internet-facing site could have been reached through them. The three worst are the passwordless sign-in as a social-linked account (GHSA-9gr7-g6pw-5244), the unlimited password guessing (GHSA-vr9h-v6xq-4m35), and the anonymous request that boots e107 as the main administrator (GHSA-pw4f-4544-2mv2). Look through Admin » Users for accounts you do not recognize, and for accounts holding administrator rights that should not.
Two can be narrowed short of upgrading. GHSA-3357-4wp5-w5fp reaches a browser allowed to see the affected contact form. Setting Contact page visibility to Members removes the exposure for signed-out visitors, but a signed-in member or administrator can still be targeted. To narrow it further, set the page to Nobody, and set the bundled contact menu's own visibility to Nobody in Menu Manager or remove it too, since that menu does not read the contact preference. Members-only has shipped since v2.3.2, so an older site is open to guests until someone changes it. GHSA-pf37-7c5m-mpg3 needs an admin account holding the Languages permission, so withdrawing it narrows the exposure. Neither issue can be hidden from the main administrator, who passes every class and permission check.
One is not closed by upgrading. GHSA-w24r-4r8j-vqgc needs a hostname from you, not a code change: a default install has never told e107 which hostnames it answers to, so it builds its addresses from whatever Host header arrives. Set siteurl to your site's full address, or list your hostnames under Trusted Hosts in Admin » Preferences. Either one arms the boot-time host check, so name every hostname you serve on: once e107 knows of one, a request arriving on a hostname you did not name is refused outright. Until you do, and while your site still takes new member registrations, a stranger can have e107 email somebody an account activation link that points at a host they picked.
Warning
Some of these fixes change behavior you may be relying on. Each is deliberate, and each can stop something that worked in v2.3.11.
Do these before you upgrade.
siteurl cannot lock you out, but a wrong entry in either can, and it takes the admin area with it. The way back in is the database: correct trusted_hosts or siteurl in the SitePrefs row, then delete S_Config_core.cache.php from your e107_system cache directory so the correction is read.10.77.66.* never matched anybody and starts matching after the ban files are next regenerated on v2.3.12. A wildcard whitelist row stays inert, as it was.user_tracking setting while it is still there. On a site installed before v2.3.0 where it was never changed, it reads cookie mode, and visitors can keep the old forgeable authentication cookie for up to thirty days, since only an explicit logout expires it. The old cookie can no longer authenticate anyone after upgrading. If you also want every browser to begin a fresh session under a new name, change Cookie Name in Admin » Preferences afterward.These may behave differently afterward.
index.php?logout by hand and needs updating.Fatal error: Declaration of ... line. Rename that plugin's folder to get the site back, then send its author the one-line fix at For Developers » Changed.LAN_CRON_* constants are gone, and a theme or plugin that prints one bare is a fatal error on PHP 8. If a page dies after upgrading, search your theme and plugin folders for LAN_CRON_; the five are named at For Developers » Changed.[table] keeps only the presentational attributes the tag was meant for and drops any others.[img] height, caption, or loading value set in the editor survives the save, where each used to be discarded every time.e107_handlers are refused where the server honors e107's .htaccess files. Core serves nothing from that directory directly, but a third-party public asset or endpoint placed there stops being served and must move.signin_template.php written against the old LAN_LOGINMENU_* names loses its labels.After upgrading, run Admin » Database » Update. This release changes the database: the failed-login ban gains a duration, the generic table gains two indexes, and session rows are re-keyed. Until you run it, those are pending.
On PHP 5.6 or 7.x, also check "Proof that a request came from this site" in Admin » Preferences after upgrading. Saving that screen while the recommended value was unset could silently select Off and stop e107 publishing security tokens. v2.3.12 fixes the form comparison but deliberately does not change a setting already stored as Off.
For Administrators below has the detail on each.
.. entry could delete e107_system, taking the cache, the logs, your saved backups, and the ban-list files with it. (#6119)schtasks line, and a shell script. The Manage tab reports refused calls and links to Setup. (#5919)user_tracking preference is gone from Admin » Preferences and the authentication token lives only in the server-side session, so a browser still holding the old cookie is anonymous until its owner signs in again. The checkbox goes even from a site that had the setting switched off, since five of the eight places that drew it never consulted it. (GHSA-7v5h-vrhj-wjf5)ban_durations had no install default, so a failed-login ban was written with no expiry and never lifted. The counter now reads a rolling hour and failed-login bans get a one-hour duration, applied to existing sites once on upgrade and never overruling one you set afterward. The ban lands on the eleventh failure from one address inside the hour, successful sign-ins notwithstanding, and none is raised while that duration is missing or set to Indefinite. (GHSA-vr9h-v6xq-4m35)1e3 confirmed 1000, and it stored the first box, locking an administrator out of the account they thought they had set. On a site that allows email login it also stored a hash of the empty string as the email password. Its hand-written form now carries the token every other e107 form does.generic table gains two indexes on upgrade. The ban counter reads that table on every failed login, and had to read every row ever recorded to do so. The check now drops history older than thirty days as it runs. (GHSA-vr9h-v6xq-4m35)siteurl. An operator who listed their hostnames under Trusted Hosts and left siteurl relative was never checked against the list they had just written. (GHSA-w24r-4r8j-vqgc)1e3 opened a page whose password was 1000. On PHP 8, where a trailing space no longer stops PHP reading a string as a number, 123 opened it too. Both stop working, and the same tightening applies to a plaintext password held by an alt_auth source. (GHSA-c33m-2hph-47p4)e107-cli. Over the web the run is an ordinary guest request, so your trusted-host list and the ban check apply; use the site address the Setup tab shows rather than localhost or an IP. (#5919)98b4920)[table] is the one to check after upgrading. It rebuilds the tag from the attributes you wrote, keeping the presentational ones it was meant for and dropping anything else, so a table styled through an unusual attribute falls back to the default.http, https, ftp, ftps, or mailto now renders as its own words. The old guard looked for javascript: alone and a tab inside the scheme walked past it, so [link] and [url] ask the URL encoder instead; a tel: or sms: link keeps its text and stops being a link.[img], [textarea], and [stream] drop only the offending attribute; [alert] and [block] go through the class-attribute guard the other tags already used, with [alert] falling back to its default styling if its parameter holds whitespace; and [link], [email], [quote], and [flash] encode their values instead of listing them.Deprecated: Constant E_STRICT is deprecated line appeared twice at the head of every response, carrying your full server path. Scheduled tasks and command-line runs no longer bury their own output in deprecation notices. (#5951, #5952)?logout on any page has been the e107 idiom since v1, so a theme, menu, or template that spells it out by hand leaves the visitor signed in with an error on the page. Both bundled themes and every core logout link were updated.rate.php ballot and index.php?provider=<name> both acted on a bare address. The address a provider returns to is unchanged, so nothing is re-registered with Facebook or Google.e107_admin/e107_update.php reached the update routines from a bare address, so an image tag on a page a main administrator visited flushed the system cache and re-ran every installed plugin's setup file. The page's own form still submits.10.77.66.* was stored as typed while the check compares an encoded form of the address, so it sat in the Banlist looking live and stopped nobody. Trailing whole-octet IPv4 wildcards are encoded now, on ban rows alone: a wildcard whitelist row stays inert. It takes effect the next time that screen rewrites the ban files, so open Admin » Users » Banlist and save one entry after upgrading. (#6114)LAN_SIGNIN_* strings replace them with identical text; the twelfth, the password field's label, now comes from core's LAN_PASSWORD and reads "Password" rather than "Password: ". (#5719)[textarea] and [stream] emitted each parameter as an attribute name, where no encoding protects it, so a member could write an event handler that fires for every reader with nothing to click. [img], [alert], and [block] let a parameter escape its quotes to the same end. All five now filter or encode each parameter where they render it, which is the only boundary that reaches content stored before the upgrade. (GHSA-f7x7-v438-qmf4, GHSA-48cx-ccvq-52mc)
[img] pasted id, class, style, title, loading, and the width and height overrides into the tag as given, for every caller and not only the BBCode.e_parse::isBBcode() answered false for any text carrying something shaped like <word...>, so a single tag anywhere in a submission skipped every save-time BBCode filter. It answers on the BBCode alone now. Stored rows are untouched, and the next save of such a post is filtered like any other. (#6265)[img] keeps a caption and a loading value through the save, as it now does a height, and the caption reaches the reader as it was typed rather than with its apostrophes spelled out.[link], [email], [quote], [table], and [flash] each wrote the text after the tag name, and for [email] and [table] the text between the tags as well, straight into an HTML attribute or an inline script, so a member could attach an event handler, or with [flash] inject an element outright. Ordinary content renders as before; [table] is the exception, above. (GHSA-48cx-ccvq-52mc)autologin field reached the login handler, which read provider out of it as an internal mode. That mode matches on the account's social-provider identifier, a name rather than a secret, and returns success without checking a password: that identifier posted with an empty password signed you in as that account, even with social login switched off. The identifier was member-editable, a second credential no password change revokes. (GHSA-9gr7-g6pw-5244, GHSA-m8v8-wc99-3h82)e107_session table after upgrading, because an identifier captured before it works until its row expires. (GHSA-7v5h-vrhj-wjf5)e1097e6, 641d761)508dc47, f12d76c)e107_system and overwrote the cached timestamp Admin » Mail reads, once per request, with nothing to stop it. The caller could not supply the message text, so it could not mark anyone's mail as bounced or make the site send mail. (GHSA-pw4f-4544-2mv2)Host header the visitor sent. e107 puts them in its canonical link, its OpenGraph tags and its resource URLs, and the check that should have refused an unrecognized hostname almost never ran. It now runs whenever you have told e107 a hostname of its own; if you never have, the note at the top of these release notes says what to set, because upgrading alone does not close this for you. (GHSA-w24r-4r8j-vqgc)403 Bad URL. (#5893).. entry no longer deletes e107_system. Uploading a zip whose first entry sits above the root made the unpacker resolve its destination to the parent of e107's temporary directory and remove it recursively, taking the cache, the logs, the saved backups, the import folder, and the ban-list files. Four upload failures that all reported "Couldn't detect the root folder in the zip" now name their reason. (#6119){SITELINKS} or {SITELINKS_ALT} went blank. Neither bundled theme does. (#6065){FEATUREBOX}. Its preview printed a name only the Feature Box plugin defines, which is fatal on PHP 8 when that plugin is inactive. (#5956)_blank sample plugin's navbar link points at the page it ships rather than a file it does not contain. (#5790, #6224)e_PAGE is defined after the 404 page has been rendered, so a plugin's shortcode file read a constant that did not exist yet and every unknown URL ended in a fatal on PHP 8. (#6024)?page= value, which was an uncaught error on PHP 8, so a scanner or a mistyped link took the page down. Only sites whose News pagination preference was changed are reached. (#6110)[email] and [link=mailto:] survive the "Make URLs clickable" setting. The clickable pass ran before the BBCodes and handed them an anchor where they expected an address, so [link=mailto:] lost the address and [email] produced a link inside a link. It runs after them now. (#5954)e107-cli or nobody. (#5944)/news.php. (#5614)Reply-To header, which many mail clients never show and which is lost when the message is forwarded. The From address stays the site's own, so the site's mail still passes SPF. (#5980)forum_icons_template.php left about two dozen IMAGE_* names undefined, which is fatal on PHP 8. Such a theme now renders the plugin's default icon where it drew nothing. (#6209)[youtube] save path for a pasted embed replaced the post with a placeholder, printed its parse into the save response, and dropped the privacy-domain setting; a pasted player now round-trips. (#6265)url_config made every address carrying a path fail on PHP 8 while the front page kept answering. Admin » Database » Update puts the default back. (#5928)open_basedir set. It probes absolute paths for a control panel and a PHP binary, most of them outside every allowed prefix on such a host. (#5991)@ was collected and printed anyway, which the atomic cache write made visible: every ordinary cache miss began reporting a failed stat. (#5992)thumb.php address to the delete and to the size check, so a checked image was cleared from the account and left on disk while every avatar counted as missing, and the height report read the width preference. (#6023)body, p, a, and a:hover, into the shared inline stylesheet the admin header emits. (#5981)e_parse::toJsString() encodes a value for a JavaScript string literal. It returns the quoted literal, its own quotes included, so interpolate the result bare. Quotes, apostrophes, angle brackets, and ampersands become hex escapes, so it is safe inside an HTML attribute carrying script too. Encoding failure returns an empty string literal. (5b27db3)e107::writeFileAtomic($file, $data, $mode = null) writes a file so a concurrent reader gets the old contents or the new, never a partial one. It writes through tempnam() in the target's own directory and rename()s into place, falling back to file_put_contents() where either step is impossible, so a true return means the file holds the data, not that the write was atomic. Overwriting resets mode and owner, and the read side is uncovered. (#5935)e_form::copyable() renders a block of text with a Copy button. It takes the text and an optional label, and emits its own script and styles once per page. New LAN_EFORM_COPY and LAN_EFORM_COPIED constants back it. (#5944)e107\Reflection\ReflectionProperty and e107\Reflection\ReflectionMethod make private members readable across the whole supported PHP range. PHP 8.1 stopped requiring setAccessible() and 8.5 deprecates the now-empty call, while 5.6 through 8.0 still require it, so these two subclasses make the call themselves only where it is needed. (#5951)eRequest $request = null is the only nullable spelling PHP 5.6 accepts and the one PHP 8.4 deprecates, so it had to go. The five that fatal are eFront::dispatch(), eDispatcher::dispatch(), eController::run(), eUrlConfig::parse(), and e_session::fetchMetadataReachesUs(). The fix is one line: delete the eRequest, eRouter, eResponse, or array hint from the override. Watch eUrlConfig::parse(), which a plugin shipping its own url/url.php overrides. (#5951, #5930)(integer) casts through setAccessible(), implicitly nullable parameters, E_STRICT, and strptime(). The suite does not reach everything the tree holds. Two are replacements rather than respellings: Latin-1 conversion goes through iconv() alone, and e_parse::cleanHtml() now substitutes ? for invalid UTF-8 where it produced U+FFFD. (#5951)e107plugin::execute_plugin_method() no longer assigns version_from onto a plugin's setup class, which should call e107::getPlugin(), and e107::__get() memoizes into a static array, so $e107->tp after e107::destruct() returns the object rather than null. Six classes now declare the properties their constructors assign, so a subclass declaring one with narrower visibility fails with an access-level error. (#5951)LOGIN_TABLE_REMEMBERME, LOGIN_TABLE_AUTOLOGIN, and LOGIN_TABLE_AUTOLOGIN_LAN go from the core login form; {LM_REMEMBERME} and {SIGNIN_REMEMBERME} go from the Login menu and Sign-in plugins. An unresolved shortcode renders empty; the language constants stay defined, since a removed constant printed bare is fatal on PHP 8. The user_tracking row is no longer seeded on a fresh install, so third-party code reading it without varset() warns there; on an upgraded site it survives reading cookie, so code branching on it takes the branch for a feature that is gone. session_set() keeps its value in the session and ignores its $expire, $path, $domain, and $secure arguments, so a value parked across visits is lost when the session ends. e_session_db::_sanitize() is now static, which is fatal for a subclass that overrides it as an instance method. Session rows are re-keyed to a digest of the identifier, which raises a pending core update on essentially every site. (GHSA-7v5h-vrhj-wjf5)userlogin::login() no longer honors 'provider' in its $autologin argument, and user_xup is out of the member-editable field list. The provider mode travels on an instance flag only userlogin::loginProvider() sets, and class2.php casts the posted value to an integer, so no mode can be selected from a request. The documented 'signup' value still works, and social signup and login write user_xup server-side without consulting the field list. (GHSA-9gr7-g6pw-5244, GHSA-m8v8-wc99-3h82)updated_data, updated_key, updated_extended, extended_key, and the private getValidationKey() in usersettings.php are gone; the change is held in the session and the form carries an opaque handle. A confirmation the server is no longer holding anything for now says so instead of reporting success, and the confirmed values are no longer passed through filter(..., 'str'), which had been double-encoding plain fields and flattening cleanHtml() output on rich-text ones. UserHandler::hasReadonlyField() has also been repaired. It answered false for any field set it could not iterate, which is the one answer its own documentation promises never to give; it now reads an array or a Traversable, refuses anything else rather than reporting no restricted field, and recognizes a list of field names as well as a keyed set. user_class remains writable, because user settings already filters it through the classes the member is allowed to edit. (GHSA-pq2m-9gxf-64x9)e_admin_controller_ui::_handleListBatch() asked only that the posted column be a declared field; it now also requires the 'batch' flag the dropdown is built from, and refuses a field declared 'data' => false. A plugin admin UI that posts a batch trigger for a field it never marked 'batch' => true now silently does nothing. (#6074)$_E107['cli'] is refused when the request arrived over HTTP. The shared guard tests the shape of the request, as cron.php already did, rather than a User-Agent header and the debug flag. The debug clause is deleted rather than corrected: nothing has read e107_config.php that early. (GHSA-pw4f-4544-2mv2)e107_handlers. Core fetches nothing from that directory over the web, so a default installation loses nothing. A third-party public asset or endpoint placed there stops being served on a host that honors e107's .htaccess files and must move elsewhere. The PHP bootstrap guard above remains the protection on servers that ignore the file. (064476a)cronScheduler's docblock states the contract: a task runs as the first administrator from the command line and as a guest over HTTP, so it must not read ADMIN, USERID, USERNAME, or USERCLASS_LIST, and must not assume e107::redirect() is a no-op. cronScheduler::run() now takes a $via argument, so a subclass overriding run() with no parameters fails to load; nothing in the tree does. (#5944)LAN_CRON_* constants and one help file are gone, and about forty constants are new. LAN_CRON_13, LAN_CRON_14, LAN_CRON_15, LAN_CRON_16, and LAN_CRON_60 go, with e107_languages/English/admin/help/cron.php. A language pack that translates the old names needs updating, and a theme or plugin that prints any of the five bare is a fatal error on PHP 8. (#5944)804197f)
^[a-z0-9_]+$, and one that does not is left in the page as written. Members reach this parser, because toEmail() turns shortcode parsing on by default.var_export().email.php narrows its plugin source parameter, as print.php already did, differing only in that it also permits a hyphen.e_jsmanager encodes every asset URL it prints and packs its registry through one method. A path or media value registered by a caller reached a <link> or <script> attribute unencoded, and one carrying the #|# separator the registry joins on could shift the fields of its own record. Reaching either needs a permission that already grants code execution, so this is hardening rather than an advisory. (cceda40, ac2f645)select(), count(), delete(), fields(), insert(), update(), replace(), db_UpdateArray(), db_FieldList()) return false, without throwing, and record error number -1 for a table name outside [A-Za-z0-9_]+, on both drivers, so a plugin that passed a dotted db.table or a backticked name now gets false. And users_admin_ui::beforeUpdate() filters user_class through checkAllowed(), so every route into the model applies the userclass_editclass rule, vetting a withdrawal like an assignment.e_db_pdo::close() now releases the connection, and the last result set with it. A PDOStatement holds a reference to its connection, so nulling the handle left the server connection alive whenever a query had run. Nothing fetched before close() is readable after it. The mysqli driver already behaved this way. (#5935)e_pref::save() honors its own "no messages" argument. Both error branches printed the raw mySQL error #NNNN on screen whatever the caller asked for. Around two dozen core callers pass false, so a failed preference write in the update routines, the plugin handler, Menu Manager, Search, or the URL configuration now reports to Admin > Logs. (#6018)null used to be stored, which is invisible to isset(), so a theme calling count() or in_array() on getThemePref('x') took the page down on PHP 8. A field the form posts as name[] now stores an empty array, everything else an empty string. If your theme relied on getThemePref('x', 'fallback') returning the fallback for an unticked box, it now gets ''. A theme's values also save into its own preference row. (#5995, #6036, #6133)e107-tests up and e107-tests ci-unit, so a CI failure reproduces locally with those two commands. The unit matrix no longer measures coverage nothing consumed, which was most of its wall clock: the PHP 5.6 cell falls from about sixteen minutes to two. (#5884, #6180, #6221, #5940, #6270)AdminConfirmTokenTest is gone: it matched source against a registry pinned by file and line, so unrelated edits failed it, and 0038_AdminConfirmTokenCest covers the same ground against the real forms. The entry-script sweep now runs each script in its own process and fails on a warning or a notice. themeHandlerTest stops leaving theme preferences behind, which had been failing a later case in a shuffled run, and .gitignore now covers the e107_config.php.bak the harness writes while a test site is up, which a broad git add could stage with the database password in it. (a6e54a1, #5917, #6228)?debug= set. (#5886, #5917, #6109)sitelinks::get() called count() on a missing submenu bucket in link display mode 3; e_parse::toAvatar() multiplied an empty string when the documented hd option was used with no explicit height; four forum templates read IMAGE_post2 and IMAGE_e as barewords at include time; and language::$_select_array was read before it was declared. None is reachable from core or a shipped theme. (#6073, #6060, #6086, #6085)e_db::getLastErrorNumber() returns a MySQL error number on the PDO driver. It returned PDOException::getCode(), the SQLSTATE: '23000' where the mysqli driver returns 1062. PDO is the shipped default, so every caller comparing that value against a MySQL error code failed silently on nearly every install. The Feature Box admin screen is the demonstrated casualty: its duplicate-key branch never ran. Four more failure paths now record a number as well as the text. (#5993, #6040)e_file::isValidURL() connects to the address its own policy check passed. It asked the outbound policy about the URL, then handed the URL as typed to fopen(), so the stream wrapper resolved the name again and connected wherever that answer pointed. Every other outbound path in the class already pins. The one-second budget now travels in the request context, and the status line is parsed as a code rather than searched as a substring. (#6027)ini_set() calls on the session settings cannot take effect. The harness now generates Codeception's actor classes in its own process, and the suite bootstrap turns off display_errors before e107 boots. Bringing an existing environment up with different flags either takes effect or stops and tells you to recreate it, and ci-unit checks whether the running PHP has xdebug loaded rather than trusting a label. (#5884, #5950, #5937)Thanks to
Host header reflected into the site's addresses; and, in a second batch on September 5, the File Inspector link that ran script in an administrator's session, the private messages a page could delete, the member typeahead that answered anyone who asked, the FAQ entries that needed no account, and the comments accepted where they had been switched off; to{FEATUREBOX} (#5956), the Feature Box menu that drew nothing (#5957), the PDO driver reporting a SQLSTATE (#5993), the theme manager reading another theme's language file (#5996), the member list reporting no members (#6002), the Login menu's missing plugin statistics (#6044) and its statistics rendering outside the menu (#6048), the theme options that reported "saved" either way (#6018), and the Users Online page empty since 2019 (#6108) with its hand-built profile links (#6107). Her chatbox fork also identified the emote panel's dead first click (#5613) and the form actions that break search-engine-friendly URLs (#5614, #5616); toopen_basedir warnings filling Schedule Tasks > Setup (#5991), the silenced cache diagnostics reaching the PHP Errors panel (#5992), the contact form drawing a second CAPTCHA over a plugin's own (#6014), the ban durations and failed logins that could not be saved (#6051), the missing username placeholder (#6096), and the news page killed by a junk ?page= value (#6110); toFull changelog: v2.3.11...v2.3.12
Caution
v2.3.11 is a security release for sites on v2.3.10 or earlier.
Upgrade from any 2.x at or below v2.3.10. If your site tracks the master branch, you are already past v2.3.11, so installing it would be a downgrade. v2.4.x is planned to be the next forward step.
Important
Upgrade immediately. v2.3.11 is the largest security release e107 has published to date. It closes a chain that let a visitor with no account run code on the server, a path from a signed-in member to arbitrary PHP on disk on any site that lets members choose their own theme, several endpoints an anonymous request could abuse, restricted forum and download content being served to anonymous feed readers, an open redirect, and a set of places where untrusted content was written into a page without being encoded for it.
Forty-four advisories accompany this release. Forty-one are new. Three are already-published advisories being amended in place, because each was fixed incompletely the first time: GHSA-3j33-c9v4-4p42, GHSA-5w63-63rh-99q6 and GHSA-92fr-7h4f-22pp. If you upgraded for one of those, you were not covered and are not covered until you upgrade again.
Updated September 2026. These advisories were first published as seventeen, several of which covered more than one vulnerability. A CVE identifies a single vulnerability, so they have been split until each advisory covers one. Nothing about the fixes changed. GHSA-87hm-vh32-7c3r, which was briefly amended for this release, is back to the download handler it reported, which v2.3.10 fixed; the two thumbnail endpoints it had been extended to are advisories of their own below.
| Advisory | Severity | What it was |
|---|---|---|
| GHSA-376g-2pcx-p4x8 | 9.8 Critical | A visitor with no account could run code on your server. Admin pages acted on a request before checking who sent it. |
| GHSA-3j33-c9v4-4p42 CVE-2026-48997 | 9.8 Critical | Amended. The image resizer pasted a site preference straight into a shell command. The first fix escaped two values on that line and left the third. |
| GHSA-p2p8-9jwc-8985 | 9.3 Critical | An anonymous visitor could store script in a page only administrators see. |
| GHSA-p4vr-pjjc-cvrc | 9.1 Critical | On a site that allows anonymous forum posting, a visitor with no account owned every anonymous post, and deleting one could delete any file on the server. |
| GHSA-x6mx-9j79-rg8x | 8.1 High | Deleting your own forum post could delete any file the web server can write to. |
| GHSA-2qvf-jf25-5w68 | 8.0 High | The admin dashboard's news and add-ons panels wrote what they fetched from e107.org into the page without encoding it. |
| GHSA-vgr2-p8wp-crr2 | 7.5 High | On a site that lets members choose their own theme, any signed-in member could reach the theme manager and install a theme or a plugin. |
| GHSA-wpfx-95hf-26jc | 7.5 High | The forum RSS feed served posts from restricted forums to anyone. |
| GHSA-w5f3-6v5g-4mm4 | 7.5 High | e107 did not check TLS certificates on its own outbound requests, so an attacker on the network could swap the plugin, theme, or update being installed. |
| GHSA-9747-hf3v-cvcv | 7.2 High | Any administrator could set the ImageMagick path, which e107 ran as a shell command. |
| GHSA-8h49-xpqr-224j | 7.2 High | An administrator with only a delegated user-management permission could grant themselves full administrator permissions. |
| GHSA-w85q-mcvf-7q5v | 7.2 High | The same administrator could set the password and email address of a more powerful administrator's account. |
| GHSA-4crv-cpjw-pm8h | 7.2 High | The user manager's batch actions wrote any field the request named, including permissions and passwords. |
| GHSA-2pqf-36x9-m46r | 7.1 High | A moderator of one forum could delete, lock, and move threads in every forum. |
| GHSA-7484-7876-mw5v | 6.5 Medium | The contact form skipped its CAPTCHA when the field was left out, so it could be scripted. |
| GHSA-q3vc-vvpg-4397 | 6.5 Medium | Any member could subscribe to any forum thread and receive every new reply by email, even in forums they cannot open. |
| GHSA-f4v9-vcf8-24p3 | 6.5 Medium | Any member could send a private message as somebody else. |
| GHSA-46vx-phhg-m6h9 | 6.5 Medium | Any member could read every other member's private message attachments by counting through the message IDs. |
| GHSA-5w63-63rh-99q6 CVE-2026-43934 | 6.5 Medium | Amended. Any signed-in member could still edit anyone's comment. The first fix closed the AJAX route and left the legacy one. |
| GHSA-jxxm-5qpx-h42q | 6.1 Medium | A redirect that took its destination from the request could be pointed off your site, which is a ready-made phishing page on your domain. |
| GHSA-8f4p-744g-q3fh | 5.4 Medium | A member could store script in the online-users menu by visiting a crafted address. |
| GHSA-c5h2-vq47-f8mg | 5.3 Medium | Forum listings showed thread titles from restricted forums. |
| GHSA-cgqh-7xgr-rxcr | 5.3 Medium | The download RSS feed listed downloads from restricted categories. |
| GHSA-h656-p8m2-3497 | 5.3 Medium | The comment RSS feed served comments on content the reader cannot open. |
| GHSA-f85h-mrq3-4r39 | 5.3 Medium | An anonymous request could remove a user class from any account. |
| GHSA-74pv-g3g9-86c2 | 5.3 Medium | The sitemap endpoint let anyone call any public method of any plugin's sitemap add-on. |
| GHSA-q4fp-j8j4-hmjj | 5.3 Medium | A web request with a wrong cron token made your site email its server environment and the cron password to the administrator. |
| GHSA-7vwm-467p-863j | 5.3 Medium | One request could set a poll to any result. |
| GHSA-gfxc-8v45-jpfj | 5.3 Medium | Download mirrors kept serving downloads the administrator had switched off. |
| GHSA-55jg-43v9-qwr8 | 5.3 Medium | The contact form would email any registered account named in the request. |
| GHSA-mx62-pm97-36vh | 5.3 Medium | The contact form accepted submissions from visitors outside the class allowed to use it. |
| GHSA-w5qr-xwc7-hq3r | 5.3 Medium | A bundled TinyMCE endpoint booted e107 and parsed input for anyone, with no permission check. |
| GHSA-rfq3-mc44-648g | 5.3 Medium | thumb.php would read any image the web server can read, from anywhere on the server. |
| GHSA-wq9c-9fw4-3fg2 | 5.3 Medium | The legacy e107_images/thumb.php returned any image-named file untouched, and could be made to fetch a URL. |
| GHSA-jv3q-35qg-rr53 | 4.7 Medium | The newsfeed plugin displayed and stored feed content without encoding it. |
| GHSA-pm75-xgh7-h3m8 | 4.7 Medium | email.php wrote the address you arrived from into a hidden form field without encoding it, so a crafted link could add attributes to that field. |
| GHSA-gfh5-w9r2-3546 | 4.3 Medium | Members could attach files to forum posts without the upload permission. |
| GHSA-v55m-8jrf-77g3 | 4.3 Medium | A forum quick reply could be posted into any thread, including closed ones and ones in forums the poster cannot open. |
| GHSA-92fr-7h4f-22pp CVE-2026-43936 | 4.3 Medium | Amended. An outbound request was checked once and then allowed to follow a redirect anywhere at all. |
| GHSA-h4jq-w26j-r3h2 | 3.8 Low | A permission an admin page declared for one of its routes could be bypassed by changing the case of the route's name. |
| GHSA-wmqr-5mw9-hx7x | 3.7 Low | The thumbnailer ignored the user class the media library restricts a file to. |
| GHSA-mpqj-pp9q-7jjf | 3.7 Low | The TinyMCE configuration reserved for the main administrator could be requested by anyone. |
| GHSA-m2vw-pp79-82rx | 3.7 Low | Forum attachments could be fetched by their file path, bypassing the forum's permissions. |
| GHSA-vjq7-79mw-2vm4 | 2.7 Low | Any user-management administrator could reset the password of every pending registration. |
You must upgrade to mitigate these risks.
Warning
Some of these fixes change behavior you may be relying on. Read For Administrators » Changed before you upgrade, in particular the notes on outbound requests over TLS, redirects that leave your site, and the legacy e107_images/thumb.php endpoint. Each one is a deliberate secure-by-default choice, and each one can stop something working that worked in older versions of e107.
eval(), which is indistinguishable from a web shell to a scanner. Hosts were emptying the file rather than quarantining it, which took the whole site down. (#5863, #5873)email.php now actually redirects. For years it sent a malformed Location: header, so a visitor who was not permitted to email an item was served an empty page rather than being sent to the front page. It sends the visitor to the front page now, which is what it always meant to do, and which is observable if you were relying on the blank page. (GHSA-jxxm-5qpx-h42q)e107_images/thumb.php endpoint is now a translating shim. Nothing in e107 has called it since the 0.8 series, but upgrades never delete files, so it ships a replacement rather than being left as it was. It now translates the request it understands into one the modern thumbnailer serves, and refuses the shapes that were the vulnerability: an absolute path, a path containing .., and a source carrying a scheme://. The size selector that used to hand a small source back untouched is accepted and ignored, so a source narrower than the size you asked for comes back through the modern thumbnailer rather than as the original bytes. A thumbnail it does serve may also differ byte for byte from before, because the modern endpoint encodes to the Thumbnail quality preference, 75 by default, rather than the legacy endpoint's 99. (GHSA-wq9c-9fw4-3fg2)Sec-Fetch-Site, which the browser sets and no attacker's page can forge, is now an accepted alternative, so a page that never got a token is no longer an outage. Admin Area » Site Preferences » Site Information, directly below Trusted Hosts, now offers e107's recommendation (the default, and what every site that has never touched this setting gets), token or same-site, same-site only, same-origin only, token required, token optional and logged, and off. The three settings the previous release shipped keep exactly the meaning they had, so nothing already stored changes behavior. (#5858)parse_scbatch() interface would load a batch from anywhere PHP can read, including remote and wrapper addresses, and the result is executed. It now only accepts a file inside the plugin, theme, or core directories, and every call raises a deprecation notice. Callers pass their own filename, which is what the interface was for, so this should be invisible; if a plugin on your site loads a batch from elsewhere, it will stop. (#5868){TOTAL_FORUMPOSTS} now reports posts, and the number it shows will get bigger. It counted rows in the topics table while naming itself for posts, and cached that count under a key two other shortcodes fill with a genuine post count. Whichever of them rendered first decided the number for the rest of the page, so a member with 21 of 42 posts could render 50% or 300% on the same page with nothing about the site having changed. All three now count posts. If you display this figure anywhere, expect it to rise on any forum where anyone has ever replied, because replies were never being counted. (5ca7930)default_install.xml seeded the four social URLs with #, and neither guard in the social shortcode skipped that value, so every new e107 site put four footer links in its theme that open a blank tab and announce a destination they do not have. The placeholders are gone from the core and from the voux theme, and the shortcode refuses a # destination even if one is already stored. (a10f513)/bin/sh. It is escaped now. This is the one change in the chain that also protects a site whose preference was poisoned before the patch, because it makes an already-stored value inert. A path containing spaces starts working as a side effect. (GHSA-3j33-c9v4-4p42)parse_scbatch() handed its filename to a function that accepts every stream wrapper PHP offers, and the lines it returns are executed later by the template parser, so nothing dangerous appeared to happen at the point of the call. The core's own copy of this endpoint was removed some time ago, but at least one third-party plugin copied the pattern and repaired the broken bootstrap that had been keeping the core's copy inert. (#5868)e107_config.php as the obvious target, which leaves a site believing it is not installed. The same loop was also broken for genuine attachments, which were never actually deleted and were orphaned instead. (GHSA-x6mx-9j79-rg8x).htaccess, nginx in particular, raw paths keep working, and there the random component in the name is all that stands in the way. New attachments gain that component; renaming the files already on disk is a migration and is not in this release. Separately, the upload itself did not check the permission to attach files, so any member who could post could attach files even where you had turned attachments off. (GHSA-m2vw-pp79-82rx, GHSA-gfh5-w9r2-3546).htaccess, nginx in particular, is not protected by it, and there the random component in the filename is all that is left. That component was widened from four digits to sixteen hexadecimal characters, which is the last place in the tree a security-bearing value came from a weak random source.apache, which is Apache 1's; every modern configuration reports something else and walked straight through. It now asks whether this is a web request rather than which binary is running, because tightening the list would have broken cron on cPanel and DirectAdmin, where the command-line PHP is frequently a CGI build.e107_images/thumb.php served the file untouched when the request selected its noscale mode, which is an unauthenticated arbitrary file read with no resizing involved at all. See Changed above for what replaces it. (GHSA-wq9c-9fw4-3fg2)email.php. (GHSA-8f4p-744g-q3fh, GHSA-jv3q-35qg-rr53, GHSA-2qvf-jf25-5w68, GHSA-pm75-xgh7-h3m8)<div data-x="<script>"> switched injection off for every form after it while the page still rendered perfectly; and AJAX list fragments never reached the point where a page is given its token, so filtering a list and then using a batch action was refused.e107_handlers/menumanager_class.php carried the classic backdoor fingerprint, a base64 decode of a request parameter fed into the superglobals, and eval() on the contents of a theme file, and e107_admin/menus.php carried the decode half as well. Scanners match on exactly that, and one that cleans a file rather than quarantining it leaves nothing behind, which takes the site down with an error naming a file that has nothing to do with the fault. The core is not doing the truncating; the core was holding the pattern that invites it. All three patterns are gone: the links are ordinary query strings, the legacy theme layouts are read by a parser instead of being executed, and the failure now names the class that is actually missing. Every theme that takes this path was checked to report the same templates and menu areas as before. (#5863, #5873)default_install.xml declared seven preferences twice, and the importer resolves a duplicate by letting the last one win. For the contact recipient setting the losing value was the one that works: on PHP 8 the surviving value did not match any option in the dropdown, so the dropdown preselected its first entry, which is "nobody", and the preferences page saves every field whether or not you touched it. This is a plausible source of "my contact form disappeared", and it is install-time only. The upgrade repairs the stored preference where it still holds a non-numeric leftover, but it cannot tell a deliberate "nobody" from one this bug produced, so if your contact form went quiet, set the recipient again after upgrading. (a10f513){USER_FORUMPER} showed 0% for everyone but the first member on the page. The member's own post tally was worked out inside the block that computes the site-wide total once and caches it, but read outside that block. The first member on a page filled the cache and got the right answer; every member after them took the cache-hit path, where the tally had never been set, and divided nothing by the total. (cdca499){LANGUAGELINKS} on a page decided the shape of all the rest. Its two options were stored in constants, and a constant cannot be changed once set, so a theme carrying the shortcode in a language menu and again in the footer got one of the two wrong, depending on which was drawn first. Both are read per call now. Code that sets the constants beforehand to pick the defaults still works, which is what the original comment promised; an option on the call itself now wins. (1260398)require_once, which does nothing at all the second time it is asked for a file, so the variables the template defines were not there and the mailout reported the template as missing. Each file is still read once; what it defines is now kept and read from there. (ef1de0a)e_theme_layout_parser reads the layouts out of a pre-v2.2.2 theme.php by tokenizing the source and folding the assignments, replacing a chain of regular expressions feeding eval(). It covers what real themes use: string literals, heredocs and nowdocs, array literals, element assignment, and concatenation with constants. Assignments inside a function or class body are left alone, matching what eval() did, and an expression that will not fold keeps its raw source as the value, so a theme doing something clever still lists its layouts. (#5863)redirect_class::goOffsite() is how a caller states that a destination deliberately leaves the site. go() takes the permit as a fifth argument, and writing that out at the call site states nothing: an author who copies it and drops an argument turns off cache prevention rather than turning on the permit. (GHSA-jxxm-5qpx-h42q)e107forum::permList(), visibleForumIds(), and threadVisibleSql() express the forum's visibility rule once. The feeds and listings now ask the forum rather than each rebuilding the same two-leg user class test; three copies of one permission rule is how that defect arrived. (GHSA-wpfx-95hf-26jc, GHSA-c5h2-vq47-f8mg)e_user_model::checkAdminPwchangeToken() replaces the hand-written ADMINPWCHANGE comparisons. Its documentation is explicit about how little the check is worth: the value is an MD5 of a timestamp, it is the MD5 of '0' for an administrator who has never changed their password, and the MD5 of the empty string for anyone who is not an administrator at all. It confirms a submission came from a form this account was served. It is not a forgery check, and it is not a permission check. (GHSA-vgr2-p8wp-crr2)e107_plugins/forum/forum_attachments.php holds the forum's attachment rules in one place: which forums a caller may read, which post a given attachment hangs off, and where the attachment directories are. forum_class::sendFile() asks it before serving, so an attachment in a restricted forum is refused, and one that no post accounts for is refused rather than served as an orphan. It also writes the deny rules that stop the web server answering for the raw path. (GHSA-m2vw-pp79-82rx)$tp->toHTML() is not an output encoder, and its documentation now says so. 'scripts' is true in its default option set and it decodes entities inside script blocks, so passing remote or visitor-supplied content through it re-emits <script> verbatim and undoes any encoding applied earlier. It is also not context aware: the result is a fragment of HTML, not an attribute value, not a URL, and not JavaScript. Several sinks in the tree were relying on it as an encoder. For visitor content, pass a USER_* context, which strips scripts; for an attribute use toAttribute(); for a URL encode for the URL. (GHSA-jv3q-35qg-rr53)e_admin_dispatcher::__construct() authenticates before it does anything else, ahead of runObservers() and the controller's init(), and ahead of requiring boot.php. It does not key on e_ADMIN_AREA, because for plugins that constant is a filename heuristic and a third-party page named settings.php would keep the whole window. The opt-out is a protected $requireAuth property on the dispatcher, which cannot be set from a theme or an e_module.php. AJAX requests are answered by e_jshelper::sendAjaxError() with a 403 rather than being redirected. hasRouteAccess() now canonicalizes the route and mode before it looks them up, because a route can be spelled several ways and a declared permission map could previously be bypassed by spelling it differently. (GHSA-376g-2pcx-p4x8, GHSA-h4jq-w26j-r3h2)redirect_class::go() refuses an off-site destination by default. The predicate compares the host rather than prefixing the string, honors the trusted_hosts preference, treats any scheme other than HTTP and HTTPS as leaving, and normalizes its input the way a URL parser does before testing it, so the string that is tested is the string that is emitted. verifyDestinationUrl() shares that normalization. A refusal is written to the PHP error log, because go() runs too early in the boot to reach the logging subsystem. (GHSA-jxxm-5qpx-h42q)csrf_enforce now names which proof a request must bring. The modes are a menu rather than a ladder: TOKEN_CHECK_OFF (0), TOKEN_CHECK_LOG (1), TOKEN_CHECK_ENFORCE (2), CSRF_CHECK_TOKEN_OR_SAME_SITE (3), CSRF_CHECK_SAME_SITE (4), and CSRF_CHECK_SAME_ORIGIN (5). An unset, empty, or out-of-range value resolves through CSRF_CHECK_RECOMMENDED, which currently points at 3, deliberately indirect so that the recommendation can be raised in a later release without every operator having to act. It points at 3 rather than 4 because an upgrade cannot ask the operator's browser what it supports. 'same-site' is honored only where Origin names a host this site serves, so a user-content subdomain or one that has been taken over does not vouch for a request. In modes 0, 4 and 5 e_token_injector stops injecting, because a token nothing checks is not merely wasted work: it is a live session token stamped into every same-origin form on the page, including one an author or an attacker put in a news item. Three ad-hoc guards that demanded or faked a token had to go with it, in comment.php, comment_class.php, and forum_class.php. The token guards on the state-changing GET routes in plugin.php, theme.php, and language.php stay. (#5858)PRFLAN_294 through PRFLAN_298 shipped in the previous release describing a setting with three choices, and they are restored to exactly what shipped rather than being reworded in place, because rewording a key that has already gone out leaves every translation silently wrong with nothing signaling that it needs revisiting. The new wording takes PRFLAN_305 onwards; the gap at 299 to 304 belongs to a master preference, so a key means the same thing on both branches. (#5858)e_jsmanager::url() appends the cache identifier only when its flag is exactly true, and the script branch passed the string 'js', so stylesheets always got a parameter and scripts never did. The identifier is now paired with a hash of e_VERSION, so every core asset address changes on upgrade rather than only when an administrator empties the cache. It is hashed because an asset address is public and the exact release is not worth publishing on every page. (#5858)parse_scbatch() raises E_USER_DEPRECATED on every call, and file mode is confined to e_PLUGIN, e_THEME, and e_CORE. The path is canonicalized through e_file::resolveSendPath(), which fails for every stream wrapper, and the roots deliberately omit media, uploads, and system so that an upload can never become an executable batch. A refused path returns an empty array, which is already what an unreadable path returned. Its documentation now records the trust contract, the deferred eval(), and the class-based batch API that superseded it in 2.0. Callers that pass __FILE__, which is what the interface was for, are unaffected. (#5868)resize_image() loses its 'stdout' surface. The legacy thumbnailer was the only caller; the two other apparent callers were already dead, one requiring the handler without calling it and one inside a comment block. mimeFromFilename() is deprecated rather than removed, because third-party plugins require this handler directly and that is the only place it can still be called from. resize_method is now constrained to the three backends that exist, defaulting to gd2, where an unrecognized value used to print Invalid resize function and return false instead of producing an image. (GHSA-wq9c-9fw4-3fg2, GHSA-9747-hf3v-cvcv)?enc=<base64> decoded back through parse_str($string, $_GET) is gone, replaced with http_build_query(), which also encodes the class and custom-pages values properly where the old raw concatenation did not. The gate that decides whether a request belongs to the menu manager now tests the two parameters those links carry, which is the same set of requests. No decoder is kept for old links, deliberately: keeping the decode next to the eval() is what scanners match on. (#5863)e_file::protectDirectory() is called when a private message attachment is written, as well as from install and upgrade, so a directory created by a restore, or on a site whose plugin predates this, is covered the first time anything is written to it rather than only by a hook that has already run. It deliberately does not live inside e_file::getUserDir(), because the forum reaches that same helper and applies its own protection from its own setup. (GHSA-46vx-phhg-m6h9)ecache::clear($tag, true) does not mean "and everything related". The second argument selects the system namespace, so the forum admin's cache clear was emptying entries nothing ever writes while the entries the reader writes stayed on disk. The cache handler's documentation now says which argument is which.--worktree names another tree explicitly and sits beside --env as a global flag; E107_TESTS_NO_HANDOVER, which only existed to paper over the old default, is removed. (#5868)resolveSendPath() no longer fatals when called outside a class2 bootstrap, which is how the legacy thumbnail entry point reaches it. (GHSA-wq9c-9fw4-3fg2)havePluginInstalled(), dontSeeTableInDatabase() and havePluginTables() were added, along with a guard on the admin feed constant so that a test can point it at a local fixture rather than reaching the network. Without a real install, a test that posts to a front-end plugin page gets a redirect either way, which is indistinguishable from a refusal, so those tests would have passed for free. havePluginTables() creates every table a plugin ships with the engine e107 would actually pick; the pattern it replaced required a statement to end immediately after the engine name, so any table carrying options after it was missed, and because the body group is lazy several tables could collapse into one match.checkAccess() rewrites the action to e403 while the controller is still constructed and its init() still runs, so the page says Access Denied while the write lands. A test that reads the response body passes against vulnerable code.csrf_enforce mode is pinned, against requests bringing no proof, a wrong token, a valid token and the Sec-Fetch-Site values that distinguish the modes, with the cookieless POST that has no ambient authority to borrow covered in all six. One case is the guard on the default itself: if the recommendation ever moves to a mode that reads no token, that assertion fails, which is the intended way to find out that the change turns away every visitor whose browser predates Fetch Metadata.Thanks to
menumanager_class.php arriving at 0 bytes, and for going back to his host to identify the scanner and the signature that did it (#5863); and toFull changelog: v2.3.10...v2.3.11
Caution
v2.3.10 is a security release for sites on v2.3.9 or earlier.
Upgrade from any 2.x at or below v2.3.9. If your site tracks the master branch, you are already past v2.3.10, so installing it would be a downgrade. v2.4.x is planned to be the next forward step.
Important
Upgrade promptly. v2.3.10 closes an unauthenticated arbitrary file read that hands out e107_config.php, and with it your database credentials, on a default install. It also closes a cross-site request forgery hole that made every form on the site forgeable, and replaces the guessable random numbers behind password reset codes, session tokens, and activation keys.
e107_config.php. Sites on PHP 8.0 or later are affected; the containment check that was meant to stop this held on PHP 7.4 and earlier. (1fa51ecb56)You must upgrade to mitigate these risks.
Warning
The new cross-site request forgery protection can break form submissions, and we want to hear about it when it does.
Refusing a POST that arrives without a security token changes how every form on the site is accepted, on the front end as well as in the admin area. Getting that right across two decades of core, themes, and third-party plugins was the hardest part of this release, and some setups will hit a case we did not anticipate.
If a form stops working after upgrading, go to Admin Area » Site Preferences » Security & Protection and set Requests without a security token to Allow and log. Affected requests go through instead of being refused, and each one is written to the admin log with the names of the fields it posted, so you can see what is failing without leaving your site broken.
Then please open an issue with what the log shows. If you believe e107's cross-site request forgery protection should support your use case, that is a gap we want to close in the core. The preference is there to keep you running while we do, not as the answer.
POST form gets a hidden e-token input as its first child, and every HTML page gets a <meta name="e-token"> before </head>. This is the part of the release most likely to surprise a theme: a stylesheet or script that targets form > :first-child will now select the hidden input instead of whatever it selected before. It is worth loading a page or two on any theme that you are using. There is no setting to stop e107 adding them: with e_form::token() deprecated and no longer called anywhere in the core, a site that switched injection off would only end up refusing its own forms. If the markup disturbs your theme, the fix is the theme's selector, and the setting above is what keeps you running in the meantime.e107_files/e_ajax.php, e107_files/e_js.php, e107_web/js/e_ajax.php, or e107_web/js/e_js.php. None of them have done anything since 2012 and the newest e107 no longer ships them, but an upgrade never deletes files, so the File Inspector will now tell you to remove any copies you still have. (#5853)e107_files/public/, that no version 2 install has ever created, and on PHP 8.0 and later that turned the whole check into "allow anything". A default install on PHP 8 gave up e107_config.php, database password included, to an unauthenticated request. Containment is now done properly, comparing full resolved paths and refusing outright if it cannot work out where it is allowed to serve from. (1fa51ecb56)LINKDISPLAY set to 4 took down every page. That setting loads a slide-out menu plugin that is no longer shipped with the core, and the core loaded it without checking it was there, so every page carrying the site links shortcode died. Sites that still have the plugin behave exactly as before; sites without it fall through to ordinary site links. (#5854)rss.php fell back to a file that moved in v2.3.1, which was a guaranteed fatal error. The fallback is now guarded rather than repointed, because a site upgraded from v2.3.0 or earlier still carries the old file and still renders from it. (#5854)user_menu plugin was folded into user, so the page came up with empty labels. (#5854)admin.js and the whole scriptaculous folder were deleted from the core in 2013, but three places still asked the browser to load them, producing a pair of 404s on nearly every admin page on any site that has not switched the deprecated Prototype library setting to "none". (#5854)e107_plugins/clock_menu/, which was removed from the core long ago, so every new site started life with a broken menu record. (#5854)social/instafeed_menu.php, a file with no history in this repository at all, so Menu Manager listed a menu that would have failed the moment anyone switched it on. (#5854)log and metaweblog plugins were treated as part of the core. Both moved to repositories of their own, so a site that installs either one from its own repository was being classified as running a core plugin. (#5854)e_random: one fail-closed source for every secret. bytes(), hex(), int(), and pick(), plus isAvailable() for the installer. There is no weak fallback anywhere in it: with no cryptographically secure generator behind it every method throws, because a guessable secret is worse than an unavailable site, and the exception message names exactly which of /dev/urandom, OpenSSL, libsodium, or mcrypt PHP is missing. The public surface is identical to the e107\Security\Random class on master, which reaches this one through a class_alias, so a later fix moves between the branches unchanged. The installer refuses to proceed on a host that cannot supply a generator, which is the last point where an operator can still do something about it. (#5856)e_token_injector: the token is added to the finished page. e_form::open() has never emitted a token, and a large part of the core plus an unbounded set of third-party plugins writes raw <form> markup, which is why token protection was opt-in and mostly opted out of. The injector rewrites the response at the e_http_header::setContent() flush point, so every eligible form is protected without a plugin changing a line. It is unconditional and has no off switch, because with e_form::token() deprecated and no longer called anywhere in the core, switching it off would leave a site refusing its own forms rather than relaxing anything. Eligibility is deliberately narrow, because a token sent to the wrong place is worse than no token: HTML responses only, method="post" only, and an action that can be proven same-origin, so a form posting to a payment gateway is left alone. Markup inside <textarea>, <script>, and HTML comments is never touched, because the language file editor and several plugin admin screens legitimately keep form markup inside a textarea and writing into one corrupts it on save. (#5856)e107.security.csrfToken() in JavaScript. One new member on the e107 object, and a function rather than a value so that where the token is read from stays an implementation detail we can change later. Anything using $.ajax or $.post needs nothing: a $.ajaxPrefilter attaches the token to same-origin POSTs, in the request body where the payload allows it and in an X-e-token header where it does not. Reach for csrfToken() only when driving your own XMLHttpRequest or fetch(), as the Plupload uploader does, and never put the token in a query string, where it would be written into every access log in front of the site. (#5856)e_file::resolveSendPath(), getSendRoots(), isAbsolutePath(), and isSafeRelativePath(). resolveSendPath() returns the canonical path or false, rejecting NUL bytes before realpath() can see them (PHP 8 raises a ValueError there, which would turn a bad request into a fatal), dropping roots that cannot be resolved instead of letting one poison the result, comparing canonical prefixes terminated with a separator rather than searching for a substring anywhere, and failing closed when no root resolves at all. Stream wrappers such as php:// and phar:// are refused structurally, by never resolving. (1fa51ecb56)$opts['roots'] on e_file::send(). It replaces the default root list rather than extending it, which is what lets a caller handling untrusted input pin itself to a single directory. No caller in the tree passed $opts before, so nothing existing changes behavior. The core media handler widens to e_ROOT rather than narrowing, because plugin and theme installs both write public {e_PLUGIN} and {e_THEME} rows through importIcons(). (1fa51ecb56)paragonie/random_compat, vendored as a production dependency. random_bytes() and random_int() are native from PHP 7 onwards, and this branch still supports 5.6. The polyfill is a production dependency rather than a development one because e107 cannot mint a single secret without it. Every changed file in this release passes php -l under php:5.6-cli. (#5856)e_randomTest pins the output shape every consumer depends on and the fail-closed contract, including a child process with the generator taken away entirely. How it is taken away depends on the PHP version, because the two generations lose it in different places: on PHP 7 and later random_bytes() and random_int() are disabled through disable_functions, which is what a hardened php.ini looks like from inside the class, and on PHP 5.6 the probe declares the two functions itself, which reproduces how the random_compat polyfill behaves on a host with no entropy source. e_token_injectorTest covers eligibility and the markup it must not touch. Six acceptance Cests drive the rules over the wire: path traversal, token injection, a tokenless POST to an endpoint that never had a guard, a cookieless POST, allow-and-log recording field names and not values, and password reset code redemption. (#5856, 1fa51ecb56)e_session::check() refuses a state-changing request that carries no token. Every disjunct used to read isset(...) && !checkFormToken(...), so omitting the field matched no branch and fell through to return true. A POST is now refused when it presents a cookie and carries no token. The cookie condition is the point: a request carrying nothing the browser attached on its own has no ambient authority to borrow. Only POST counts as state-changing, since it is the only method a browser sends cross-site without a preflight, so every GET behaves exactly as before. The mode is read through e_session::tokenCheckMode() and can be overridden for a single request with e_session::setTokenCheckMode(), which returns the displaced value and restores preference control when passed null. checkFormToken() now compares with hash_equals(). HTTP_X_E_TOKEN is accepted alongside the posted field. (#5856)e_form::token() is deprecated as of v2.3.10, and nothing in the core calls it any more. Avoid it in new code and drop the call when you are next in that file. Nothing is removed and calling it stays harmless: the page simply ends up with two identical hidden inputs. Form POST protection is automatic whether the markup came from e_form, a theme template, or raw HTML in a plugin. If you are building an AJAX request body by hand, take the token from e107.security.csrfToken() on the JavaScript side instead. (#5856)e-token guard is gone from twelve entry points. Each opened with the same six lines forcing $_POST['e-token'] to an empty string before class2.php, so that a missing token would be read as an invalid one. The rule in check() covers every entry point at once, so the copies said nothing the framework does not already do, and they actively defeated the carve-out for cookieless callers by refusing requests the rule deliberately allows. The plugin scaffold generator no longer writes the guard into new plugins either. (#5856)e_admin_controller::checkRequestToken() and the update routines keep their own check() call. It turns a bare die() into an error an admin can read, and it catches a token that expired between rendering a form and submitting it. Neither forces the tokenless case any more, because class2.php has already applied the site's tokenCheckMode() and second-guessing it there would make allow-and-log enforce. (#5856)e_file::send() runs its containment check before tearing down the output buffers, and streams from the path that passed it. Both are behavioral fixes rather than tidying. The check used to run after ob_end_clean(), so a realpath() warning could land inside the body of a binary download; and containment was tested on realpath($filename) while the actual I/O used $filename, leaving a window in which a symlink swapped between the two would be read despite a passing check. (1fa51ecb56)e107plugin::$core_plugins and e_plugin's copy have been merged into e_plugin::CORE_PLUGINS, so the two lists cannot drift apart again, and log and metaweblog are out of it. (#5854)filter_request() was deliberately left alone. It blocks the literal .https://e107.org/.https://e107.org/ and it is not the boundary here: .https://e107.org/https://e107.org/.https://e107.org/ reaches exactly the same file without ever containing that string, so no amount of pattern matching would have helped. Anyone maintaining a downstream package should fix the containment check rather than the filter. (1fa51ecb56)e107_web/js/e_ajax.php and e107_web/js/e_js.php. Both were moved out of e107_files/ in November 2012 without their relative bootstrap path following, so both have raised a fatal error on every direct request in every release since v2.0alpha. e_ajax.php additionally passed $_POST['ajax_scfile'] to parse_scbatch(), which reads the path with file() and evaluates the resulting SC_BEGIN blocks. That chain was never reachable in any release or tagged prerelease, but repairing the bootstrap path on its own would have made it live, so the endpoint is removed rather than repaired. The chatbox_menu header whose only statement loaded it, the featurebox ajax_url default that pointed at it, and both entries in the redirection exception list have gone with it. (#5853)pcltar.lib.php and pcltrace.lib.php (archive handling reachable only from a branch that sits after an unconditional return, inside a block comment; pclzip.lib.php stays, and pclerror.lib.php is kept for its documented PCLZIP_ERROR_EXTERNAL integration), search/search_event.php (never registered as a search engine in this project's history, and it queried tables belonging to a plugin that is no longer part of the core), e_file_inspector_sqlphar.php and cli_class.php (each declares a class whose name appears nowhere else and has no autoload entry), and metaweblog.php (required a plugin that moved to its own repository in December 2020, so it has been a guaranteed fatal since v2.3.1). (#5853)db_verify_class.php, sc_admin_navigationOld() in admin_shortcodes.php, the mail form and recipient renderers in mailout_admin_class.php, and the theme upload and renderOnline() blocks in theme_handler.php. These blocks are a live hazard: automated review tooling and human readers alike keep matching them with grep and mistaking them for reachable code, which has already produced false vulnerability findings against this project. Removal was mechanical and constrained: only comment runs of twelve lines or more were dropped, each file was rejected unless its live token stream was byte-identical before and after, and every file was then checked with php -l. Docblocks and explanatory prose are preserved throughout. (#5853)e107_admin/filemanager.php, e107_admin/fla.php, e107_handlers/emote.php, and e107_web/utilities/*.php stay exactly as they are. An e107 upgrade extracts over the top and never deletes files, so an empty file in the core is what overwrites a previous version's working code on an upgraded site. Deleting the stub would leave the old code live. (#5853)Thanks to
e_ajax.php shortcode batch endpoint (GHSA-wpmq-47ch-x74p). No release ever shipped it in a state where it could run, so it is not a vulnerability in any version of e107, but the report is what prompted the dead-code clearance in this release and the File Inspector entries for the leftovers; and toFull changelog: v2.3.9...v2.3.10
Caution
v2.3.9 is a maintenance release for sites on v2.3.8 or earlier.
Upgrade from any 2.x at or below v2.3.8. If your site tracks the master branch, you are already past v2.3.9, so installing it would be a downgrade. v2.4.x is planned to be the next forward step.
Note
There are no security fixes in this release. If your site is still on v2.3.7 or earlier, the release you need urgently is v2.3.8, which closed an unauthenticated SQL injection and two further advisories. v2.3.9 carries all of that forward and adds bug fixes on top.
A-Za-z0-9_-. MySQL and MariaDB permit periods, spaces, dollar signs and Unicode in database names, verified on MariaDB 10.11 and MySQL 8.0, so an existing database named, for example, e107_2.3.8 could not be used at all. The restriction protected nothing, because whoever fills in that form already holds the database credentials, and the name only ever reaches SQL inside backticks. It is now escaped there instead. The table prefix keeps its A-Za-z0-9_ rule, because the prefix really is dropped into queries unquoted. (#5819)via.placeholder.com has gone; the domain no longer completes a TLS handshake. thumb.php used to redirect missing-image requests there, the hero plugin's three sample slides embedded its URLs directly, and the media handler had a special case for the domain name. thumb.php now serves a small SVG placeholder it generates itself, with cache headers, in about two microseconds; the hero slides ship as local SVG files; and image detection now ignores the query string, so existing database content still pointing at the dead service is classified correctly instead of by domain name. (#5581)login.php, ignoring a customlogin.php if the site had one, and the login page could only render in its bare iframe form. Worse, with registration disabled and no social login, the stock login page turns a guest away and bounces them back to the page they came from, looping until the browser aborts. e107 now redirects to whichever login page the site is configured to use, and falls back to the members-only notice in exactly that dead-end case, so the loop cannot form. (#5335)http://twitter.com/share link and a Facebook "like" frame into every gallery overlay. The insecure addresses were blocked as mixed content on any HTTPS site, and the Facebook frame never received the right address anyway, so what remained was broken markup on every image. The default is now empty. A theme that defines its own share template still overrides it and is unaffected. (#5651)SECURITY.md file at the top of the repository still pointed reporters at an e-mail address that does not reliably deliver. GitHub serves the policy from .github/SECURITY.md, which asks for private reports through GitHub Security Advisories, so the stale copy has been removed. To report a vulnerability in e107, use Security Advisories. (#5822)e107-tests harness, backported from master. release/v2.3.x can now run its own PHP matrix locally, including the 5.6 and 7.0 legs whose end-of-life Debian bases previously could not build a container at all. State moves out of per-worktree docker/.envs/ files into Docker labels and per-environment named vendor volumes, so list is global, gc reaps orphans, and the repository's composer.lock is never rewritten (composer install, with a loud per-environment composer update fallback when the interpreter cannot satisfy the lock). Adapted for this branch: no Selenium service, DEFAULT_PHP=8.2, acceptance browses http://localhost/ from inside the web container, and Codeception stays at 4.2.2. Validated on 5.6, 7.0, 7.4, 8.1 and 8.2. (#5837, #5836)testCdnUrlContainsVersion asserts each CDN version appears in its resolved URL, and testCdnFileReportsHardcodedVersion exercises every entry's own version_arguments pattern against real bytes vendored under tests/_data/library_headers/. testRefreshCdnFixtures regenerates those fixtures from the live CDNs and is the one Internet-touching path, skipped unless E107_REFRESH_LIBRARY_FIXTURES is set. (#4560)check_name()'s non-strict mode (the database name) no longer imposes a character-set rule; strict mode (the table prefix) keeps A-Za-z0-9_, because the prefix is interpolated unquoted into every query. A new quoteDbName() doubles backticks and wraps the name, and the three CREATE/ALTER/DROP DATABASE sites use it. e_db_pdo::database() and e_db_mysql::database() double backticks in their quoted interpolations; mysqli_select_db() needs nothing. The legacy quote and semicolon guard stays, because a semicolon cannot be represented in the PDO DSN. SqlInjectionFixesTest pins the new contract, including a backtick-doubling breakout guard. New strings LANINS_148, LANINS_149 and LANINS_150 replace the misused LANINS_105. (#5819)login.php forced e_IFRAME unconditionally. It now defers to the login core template, so setting $LOGIN_TEMPLATE['page']['noiframe'] = true in a theme override renders the login page with the full site chrome. The default is unchanged. The members-only redirect target also respects e_LOGIN now rather than hardcoding login.php. (#5335)LAN_418 in lan_upload.php is now LAN_UL_041. LAN_407 and LAN_418 were each defined with different values in two core language packs, so load order decided which text won. The unused LAN_407 blurb has been dropped from lan_signup.php (the signup e-mail composes from LAN_EMAIL_04/05/06), and lan_upload.php's "Absolute maximum file size: " is renamed to LAN_UL_041, with upload.php repointed. lan_search.php keeps sole ownership of LAN_418. Translators and language-pack maintainers: add LAN_UL_041 to your lan_upload.php; the old LAN_418 definition there is no longer read. (#5730)sc_cb_avatar() honours $parm. The chatbox avatar shortcode hardcoded crop => 'C' and rebuilt its own options array, discarding any crop, w or h a template passed in, which pinned chatbox avatars to the legacy thumb.php query-string URL while sibling avatar shortcodes forwarded $parm and reached SEF addresses. $parm is now merged over the defaults. 'C' remains the default crop, so existing rendering is unchanged, but a template can pass crop=false to escape to a SEF-eligible URL where thumb.php is broken on the host. (#5657)e_library_manager takes an injectable file handler. It defaults to e107::getFile(), and detectPath() and getVersion() read through it, so a test can stub every remote read. _before() injects a handler that throws on any fetch, so an accidental network read now fails loudly rather than quietly downloading. testDetectionVersionConsistency compares each CDN pin against its bundled local sibling offline, which catches drift between the shipped copy and the hard-coded version, and testCoreLibraryPresence has dropped its remote URL probe. The cdn.jquery.once pin is corrected: it claimed 2.1.2 while the bundled copy ships 2.2.3, and the legacy jsDelivr path it used does not serve 2.2.3 at all, so it is repointed at the npm path. (#4560)e_parse::toAvatar() fetches through the e107::getFile() singleton. Its base64 data-URI branch fetched a real avatar over the network at test time, so any DNS or network hiccup silently fell through to the plain remote <img> and failed the assertion, which looked order-dependent under the shuffled suite. The fetch is now injectable and the test stubs it. (#5841)build-release.yml is replaced by release.yml. The build step is unchanged in substance, but the workflow now finishes the job instead of stopping at a workflow artifact: it uploads the archives and core_image.phar to the GitHub Release, then uploads the nine archives to SourceForge over SFTP with a pinned host key and sets the per-platform default downloads. A guard asserts the full tag history was fetched before the core integrity image is built, and a build whose tag does not match the version it produced refuses to upload. (#5831)e_navigation::admin() cross-join. The sorted branch built a list of every entry's display text and then, for each text, re-appended every entry matching it, emitting K entries K times each for a same-named group. The text list is now deduplicated before the cross-join. (#5786)pclzip reading zero bytes. fread($h, 0) throws a ValueError on PHP 8.2+, so an archive containing a zero-byte file fatally errored during in-memory compression. The read is guarded, and the file is still written as a zero-byte entry. (#5208)stripos() folds case for ASCII only, so a title differing in case on a non-ASCII letter could be dropped by the PHP post-filter even though the case-insensitive database collation had matched it. The five title filters now use the parser's ustrpos() and ustrtolower(), which use mbstring when it is present and fall back to the byte functions when it is not, so there is no new hard dependency. (#5791)Thanks to
Full changelog: v2.3.8...v2.3.9
Caution
v2.3.8 is a security release for sites on v2.3.7 or earlier.
Upgrade from any 2.x at or below v2.3.7. If your site tracks the master branch, you are already past v2.3.8, so installing it would be a downgrade. v2.4.x is planned to be the next forward step.
Important
Upgrade promptly. v2.3.8 closes an unauthenticated SQL injection reachable on a default site, finishes the core-wide SQL-injection cleanup begun in v2.3.7, and removes an open redirect.
eval(), so a hostile value written into the database out of band (for example through a separate injection) could run as PHP when that account's preferences were loaded. It is now parsed safely and rejects anything that is not plain stored data, which closes the second stage an injection would otherwise chain into. (59cf3aaaa0)You must upgrade to mitigate the numerous vulnerability risks.
eval(), with only a loose check on the front of the string. A hostile value planted in the database could therefore run as code the next time that account's preferences loaded. The value is now read with a strict parser that only accepts ordinary stored data and refuses anything else, so it can no longer be turned into code, even if something else managed to write into the database. (59cf3aaaa0)Deep audit SQL injection risks closed across the core (security). The following vulnerable areas are now closed off to SQL injection:
Nothing changes in how the site behaves and there is nothing to configure. Upgrading is critical to protect your site from potential exploitation. (d11929a0ff, 32940962e3, fb8fe63e9e)
The plugin/core update screen now carries a security token (hardening). The action to update your plugins/database could previously be triggered by a forged request if a logged-in main administrator was lured to a malicious page (a CSRF). It now requires e107's own one-time form token before any update runs, so only a genuine click on the real update button can start it. Sites that have deliberately turned off token protection are unaffected. (23fa132aee)
A saved value that could run as code is now read safely (security). e107 stores some data (such as a user's preferences) as text and reads it back when needed. One of those readers was rebuilding the value by running it through PHP's code evaluator, so a value tampered with directly in the database could execute the next time it was loaded. That reader now uses a strict parser that only understands ordinary saved data and rejects anything else, so a poisoned value fails safely instead of running. There is nothing to configure. Ref GHSA-568x-w5qj-vr7c, CVE-2026-57859. (59cf3aaaa0)
download/request.php would redirect to any web address placed in the link's query string, letting a crafted request.php?https://… link bounce a visitor off to another site under the trust of your domain (a phishing aid). Real downloads are served by their numeric ID and are unaffected; an address that is not a registered download now falls through to the normal file-not-found page. Ref GHSA-wcc5-8jrf-6q26. (b1def30489)TypeError, because the array was handed to defined(). The caption is now checked before that call, so the grid renders. Fixes #5649. (ddf6726c1a)[newpage] marker, so a page break splits the content as intended. Fixes #5792. (1ef5658a52)SqlInjectionFixesTest pins the news item integer cast, the admin batch field allowlist (reject and accept paths), the user-model ID cast, the user-extended partial-update and create validation, and the installer name checks. The probe controller is built via eval so the unit loader's include-time class scan does not pull in admin_ui.php. (181eeba90e)e107TemplateSourceTest. Pins the capture-once-per-physical-file behavior so a template backing two registry paths cannot regress to caching an empty array for the second one. (4b68ed8b40)DownloadOpenRedirectCest + seeNoRedirectTo acceptance helper. Asserts request.php does not emit an off-site Location redirect. (b1def30489)e_array::unserialize() parser coverage. New e_arrayTest cases pin the sink and the live e_user_pref::load() path, a var_export() round-trip, an edge-form round-trip (NUL / INF / NAN / PHP_INT_MIN), and an injection-rejection case; validated against the var_export()/eval() baseline on PHP 5.6 through 8.5. (59cf3aaaa0)news_classTest pins the news-grid array-caption guard; e107TinyMceParserTest and wysiwygTest pin the [newpage] pagebreak separator. (ddf6726c1a, 1ef5658a52)e_db prepared-statement and query builder API, so tainted values are escaped inside their SQL quotes with escape(), numeric / LIMIT / OFFSET positions are (int) cast, and dynamic table/column identifiers are confined to a strict [A-Za-z0-9_] allowlist (escaping cannot protect an identifier). The news item action reuses the integer cast it already computed; admin_ui / model_class validate the column against the controller's declared fields before it reaches the SET/WHERE clause, both at dispatch and unconditionally in batchUpdate(); e_user_model::load() int-casts the ID and the unadmin path moves its permission gate ahead of the query. If you maintain a plugin that builds raw SQL from request input, mirror these practices! (32940962e3)_getFieldValue() safestr now escapes. The safestr field type was serialized into single quotes without escape(), unlike the sibling str branch; its filter() pre-processing encodes quotes but leaves the backslash intact, so a trailing backslash could escape its own closing quote. It now calls escape() to match str, and the stored bytes are unchanged for any value without a backslash. xmlClass::e107Export() validates each requested table against the live table list before it reaches the FROM identifier position. (fb8fe63e9e)e107Update choke point. e107Update::__construct() reads key($_POST['update']) / key($_POST['update_core']) and runs the upgrade routines; both callers (db.php's db_update branch and e107_update.php) are gated only by getperms('0'). A valid session token is now required at the shared constructor choke point before any update runs, validated through e107::getSession()->check(false) (fail-closed, honors e_SECURITY_LEVEL); the real form emitted by renderForm() now includes the e-token hidden field. (23fa132aee)e_array::unserialize() no longer uses eval(). It rebuilt the var_export() array-storage format by prepending $data = and calling eval(), guarded only by a substr(...,0,5) === 'array' check, so a stored value beginning with array that carried a PHP expression executed on load (second-order, reachable through e_user_pref::load() from a poisoned user_prefs column). It is replaced by a token_get_all() parser that accepts only the literal grammar var_export() emits (array structure with string / int / float / bool / null / nested-array leaves) and fails closed otherwise. The multi-token edge forms (NUL via 'a' . "\0" . 'b', INF/-INF/NAN, PHP_INT_MIN) are read faithfully, and the existing $data = strip, stripslashes fixups and PDO => fix are retained, so legacy and addslash'd values still round-trip; stored bytes are unchanged for well-formed data. Ref CVE-2026-57859 / GHSA-568x-w5qj-vr7c. (59cf3aaaa0)e107::_getTemplate() caches each template under an override (/ext) and a non-override registry path; with no theme override both resolve to the same file, but include_once() only populated the template variable on the first inclusion, so the second flavor cached array() and every later lookup on it returned false. The file's definitions are now captured once under the templates/source/ registry namespace and re-read for each registry path the file backs. Template PHP still executes exactly once per request and theme overrides keep their distinct content. This clears the mail.php "notify" FIXME (2f9dae5, 2014) and un-deadens the page.php "default" fallback (46b2518, 2016). (4b68ed8b40)e107_event::trigger() tolerates stale handlers. A notify_prefs entry whose class no longer exists is now skipped with a logged EVENT_01 warning behind a class_exists() guard, and the handler catch is widened to also cover \Throwable, so an \Error raised inside a handler on PHP 7+ is logged rather than left to fatal. The widening is a second catch(\Throwable) after catch(\Exception) so the code stays parseable and correct on PHP 5.6, where \Throwable is undefined and \Exception already catches every throwable. (40e73cefde)Our thanks to Aastha Aggarwal (@Aastha2602) for responsibly disclosing the open redirect (GHSA-wcc5-8jrf-6q26), and to @Jimmi08, whose report prompted the wider SQL-injection audit behind this release (GHSA-fr8h-3vfv-9q98), both through GitHub Security Advisories. We also thank Saidakbarxon Maxsudxonov (@sermikr0) for reporting the preferences code-execution issue (GHSA-568x-w5qj-vr7c, CVE-2026-57859), coordinated through VulnCheck.
Caution
v2.3.7 is a security release for sites on v2.3.6 or earlier.
Upgrade from any 2.x at or below v2.3.6. If your site tracks the master branch, you are already past v2.3.7, so installing it would be a downgrade. v2.4.x is planned to be the next forward step.
Important
Upgrade immediately. v2.3.7 closes two unauthenticated, network-reachable, Critical vulnerabilities. Neither requires authentication, user interaction, or any special privilege, and every public-facing site on v2.3.6 or earlier is exposed.
Remote code execution via install.php (GHSA-c8h6-wpj3-4cr8, CVSS 9.8). Reachable on a default installation: install.php stays live and exploitable after setup.
Mitigation: Delete install.php.
SQL injection throughout the e107 core (GHSA-5f75-c25x-59wx, CVSS 10.0). Request data is interpolated into SQL across ordinary, unauthenticated front-end request handling, including code an anonymous visitor triggers with a single page view (online-presence tracking writes the request URI into the database after a sanitizer that still permits the closing quote). It is not confined to any one plugin or feature, so no configuration change mitigates it. The only protection short of upgrading is to keep the site off the public Internet.
Treat this as an emergency upgrade.
install.php stayed reachable after setup and could be tricked into running an attacker's code, with no login. It now refuses to run once the site is installed, and the information the setup wizard carries between steps can no longer be used to smuggle in code. (6c40429521, 5d6d649e8c)e107_config.php first. Automated ("unattended") installs are only accepted on a site that is not already set up. Ref GHSA-c8h6-wpj3-4cr8. (5d6d649e8c)alt_auth external-database login that the report originally came in on. There is nothing for you to configure and the site behaves exactly as before; installing this release is what protects you. Ref GHSA-5f75-c25x-59wx. (120bd4bdbe)Host header a visitor's browser sends; it now rejects a malformed value and uses the server's own hostname instead. On a correctly configured site the existing host check already turns away mismatched hosts, so this is an extra safety net, and it also makes sure a non-standard port is kept in the links e107 generates. Ref #5734. (a2f386b27f)media_cat_ui::createPage() called count() on an unset writeParms value when every category owner is restricted, throwing a TypeError under PHP 8. It now uses a null-safe check and keeps the same "category creation not available" behavior. Fixes #5741. (d5e0d07814)row-reverse, so the visible order is unchanged and only the Enter behavior is fixed. (2865e98f96)installStateTest builds the real guzzlehttp/psr7 POP chain and asserts the new JSON codec rejects it while round-tripping scalars; SqlInjectionFixesTest pins the core SQL-injection remediations; and acceptance coverage exercises the installer reinstall guard, paste-to-resume, and the unattended-install path end to end. (6c40429521, 892758085c)eShims::setcookie() options-array shim. Backported SetcookieTrait (the PHP 7.3+ options-array form with a pre-7.3 SameSite-into-path fallback) so the installer can set a hardened (HttpOnly, Secure, SameSite=Strict) resume cookie with one call across every supported runtime. Existing setcookie() callers are untouched. (1063102678)install.php previously unserialize()d base64-encoded $_POST['previous_steps'] with no allowed_classes, at file scope on every request and again in the e_install constructor; the bundled guzzlehttp/psr7 PumpStream POP chain reached arbitrary callable execution once the decoded value flowed into site_path and was coerced to a string. Wizard state now travels as base64(JSON).hex(HMAC), verified in constant time on every request against a per-install CSPRNG signing token, and json_decode() only yields arrays and scalars, so object injection (CWE-502) is impossible by construction. e107_config.php is read as one of three positive states (fresh, install-pending, installed) instead of an emptiness check; the unattended ?create_tables path compares credentials with hash_equals() and refuses a database that already holds an e107 schema; and the finished config is written last, so a half-finished install never looks installed. (6c40429521, 5d6d649e8c)e_db prepared-statement API, tainted values are escaped inside their SQL quotes, numeric / LIMIT / OFFSET positions are (int) cast, and dynamic table/column identifiers are confined to a strict [A-Za-z0-9_] allowlist (escaping cannot protect identifiers). Import providers are fixed at the root in base_import_class::database(). Many sinks are reachable without a login: the online-presence tracker, for example, wrote e_REQUEST_URI into online_location after only FILTER_SANITIZE_URL, which keeps the single quote, so a crafted address reached the query directly. If you maintain a plugin that builds raw SQL from request input, mirror these. (120bd4bdbe)e_db_pdo::escape() now escapes. The shared testEscape() expectation that codified the pass-through is updated, which makes the e_db_mysql override redundant. One caller (social_setup.php) used escape() on a value interpolated into an unquoted numeric context and is cast to int instead. (#5749, b467b74f72)Host resolution helper (hardening). The request host is resolved through a shared resolveHttpHost() backported from master: it accepts the client Host only when it matches a host[:port] shape and otherwise falls back to SERVER_NAME, preserving the visited port. On a configured site the isAllowedHost() killswitch (added in v2.3.6) already rejects a mismatched Host before it reaches a URL, so this is defense-in-depth (covering an unset or misconfigured siteurl, and partial-bootstrap entry points that skip the killswitch) plus a regression lock: the accompanying e107HostValidationTest pins port preservation so a future sync from master cannot silently reintroduce the port-strip. (a2f386b27f)CONTRIBUTING.md is now a policy document (backwards compatibility as the primary constraint, AI-assisted contributions welcome on the same quality bar, explicit rejection criteria for low-effort output), with matching PULL_REQUEST_TEMPLATE.md, issue forms, FUNDING.yml, and a new SECURITY.md. (2b49a62ce8)e107Install.log was written into the docroot, where the shipped .htaccess has no rule for .log files, so its install diagnostics (DB host, table prefix, pref dumps on failure) were fetchable over HTTP. It now lives under the deny-by-default e107_system/. (c1ef5bab81)e107_config.php could be served from the prior compile, hiding the freshly written provisioning lock. The installer now opcache_invalidate()s the file around each read and write. (72b333107e)GitPreparer's rollback used git reset --hard && git clean -fdx, which destroyed uncommitted work on a crashed run. It now runs each suite inside a disposable git worktree with flock()-based orphan cleanup, never touching the main tree. (aac93f1791)Both vulnerabilities were responsibly disclosed through GitHub Security Advisories. Our thanks to @baozongwi (GHSA-c8h6-wpj3-4cr8) and @Jimmi08 (GHSA-5f75-c25x-59wx) for the reports.
Caution
v2.3.6 is a bug-fix release for sites on v2.3.5 or earlier.
Upgrade from v2.3.5 or earlier 2.x. If your site tracks the master branch, you are already past v2.3.6, so installing it would be a downgrade. v2.4.x is planned to be the next forward step.
resize_image() (GHSA-3j33-c9v4-4p42). The ImageMagick convert command line did not shell-escape its destination filename; reachable via news submission with the right combination of prefs. (794a179f)www., and strips trailing ports before comparison, and returns a logged 503 instead of a bare die(). Closes #5627 and #5634. (aba3b169)trusted_hosts SitePref for parked, staging, and multi-host setups. Authorise additional incoming hostnames from Admin → Preferences without touching e107_config.php. (c2bb246b)thumb.php (#5664), fpw.php on v1-style themes (#5653), the FAQ cron registration, and the admin nav / user-handler bare-constant reads all stop crashing on PHP 8. (59aef4f1, 02a79729, 0635801a, b1290139)trusted_hosts SitePref. New textarea below the Site URL row in Admin → Preferences. Paste additional hostnames one per line (e.g. staging.example.com, example.org); the server-side normaliser strips schemes/paths and de-duplicates case-insensitively, so https://staging.example.com/ saves as staging.example.com. The pref is purely additive: requests whose Host matches the configured siteurl continue to pass without listing it here. Ref GHSA-7pmw-jwvr-cq2x, #5627. (c2bb246b)Host-header validation (security). www. and the bare apex now match each other, hostnames compare case-insensitively, and trailing ports are stripped before comparison, so https://example.com:8080/ matches https://example.com/. Sites that were stuck on the "Site Configuration Issue Detected" page after upgrading from v2.3.3 should now boot again without manual SQL edits.
The check itself still requires the request Host to match the configured siteurl (or fall under one of its subdomains, or be listed in trusted_hosts). Ref GHSA-7pmw-jwvr-cq2x. (aba3b169)
Host-mismatch response. The bare die('Site Configuration Issue Detected. ...') that returned 200 OK with one line of plain text is replaced by a 503 Service Unavailable with a short HTML body that points the operator at the server error log. The diagnostic detail (configured siteurl, request Host) is written via error_log(), which previously sat after the die() and never fired.
The visible page intentionally does not echo the incoming Host, the configured siteurl, or any admin URL. The diagnostic surface stays in the log, which already requires server access. (aba3b169)
thumb.php boots the framework directly without class2.php, so E107_DEBUG_LEVEL was never defined for that request; the first call that opened a DB handle (typically when the SitePrefs disk cache was cold) hit a PHP 8+ fatal in e_db_pdo::__construct() and broke every thumbnail on the site. Now guarded with defset(). Fixes #5664. (59aef4f1)E107_DEBUG_LEVEL fatal out of the way, thumb.php then failed with SQLSTATE[HY000] [2002] No such file or directory because its manual bootstrap chain never populated the MySQL config the PDO connector reads. The bootstrap now routes through e107::initCore() (the same call class2.php uses), so credentials are set before the first DB call. Follow-up to #5664 / #5665. (02a79729)fpw.php fataled with Undefined constant LAN_112 when the active theme had no theme.xml (the legacy theme path uses e107_core/templates/legacy/fpw_template.php, which still references the old constant). The BC shim that mapped LAN_112 → LAN_FPW22 only fired in the members-only branch; it now fires before every downstream template require, regardless of branch. Fixes #5653. (d9725b91)e107_core/templates/legacy/ that referenced a dropped v1.x LAN_* constant could fatal on PHP 8 with a single missed reference. e107::predefineLegacyLans() now tokenises each legacy template before it loads and auto-defines any missing LAN_* with its own name as a value, emitting an E_USER_WARNING per auto-define so the maintainer trail stays visible. Wired into the six legacy require sites (fpw.php ×2, search.php, signup.php, user.php, usersettings.php). Refs #5653. (b1290139)sitelinks_class::setIconArray() built the admin nav map from ~40 E_32_* constants and user_handler::$core_perms referenced dozens of ADLAN_* / ADMSLAN_* / E_16_* / E_32_* entries, all loaded lazily by the admin language file as bare reads. PHP 8 promotes bare reads of undefined constants to fatal Errors, so any caller that hit these before the admin language file loaded crashed. Both sites are now wrapped in defset() so the undefined case returns the empty default. (0635801a)e107_plugins/faqs/e_cron.php referenced LANA_FAQ_CRON_1, LANA_FAQ_CRON_2, and LAN_AUTOMATION at admin cron-registration time, which runs before English_admin.php is loaded. PHP 8 turned the bareword fallback into a fatal; the registration path now uses defset(). (21f7b584)strftime() deprecation on PHP 8.1+. StrptimeTrait::buildMonthArrays() called PHP's strftime() to localise month names. PHP 8.1 deprecated the function and PHP 9 will remove it, so the @-suppressed call still landed in error_log. Both calls are now routed through the eShims::strftime() polyfill that already exists in the tree. (295ce2a1)e107::predefineLegacyLans($path) token-scan safety net for legacy templates. Tokenises the requested template, finds bare LAN_* token references (skipping function/method/class/static contexts and call sites), and define()s any that are still missing with their own name as value. Token extraction is cached on hash_file('sha256', $path) keyed entries (APCu when available, otherwise a file under e_CACHE); a process-local memo short-circuits repeat resolutions. Warm-cache cost is roughly 6 µs vs the ~4 µs baseline.
Wired into fpw.php, search.php, signup.php, user.php, and usersettings.php. The wrappers call it immediately before the existing require/include so caller-scope template variables ($FPW_TABLE, $SIGNUP_BODY, etc.) remain assignable. Refs #5653. (b1290139)
trusted_hosts SitePref plumbing. A new e107::isAllowedHost() private helper composes the allow-list from parse_url(siteurl)['host'] plus the entries saved in trusted_hosts. The pref reads as a newline-separated list; the saver normalises (scheme/path strip, case-fold dedup) before writing back. (c2bb246b)
Docker-based parallel test environments. e107_tests/bin/e107-tests is a new CLI that spins up an isolated PHP + Apache + MySQL stack per worktree (up | down | reset | clean | install | urls | status | logs | shell | db-shell | exec | run | list). Each worktree + matrix combo gets a deterministic compose project name derived from the worktree path, so parallel sessions never collide on container names, networks, volumes, or host ports. A new config.docker.yml layer slots into the existing config cascade (sample → yml → docker → local).
Existing deployers (local, sftp, cpanel, none) and the CI workflows are untouched. (9b5f40ea)
Test coverage. resize_handlerTest exercises the ImageMagick branch of resize_image() with three command-substitution payloads and asserts no marker file is created and no id-style output leaks into a filename. e107HostValidationTest covers the case-fold, www-strip, and port-strip behaviour of e107::isAllowedHost() plus trusted_hosts composition. e107RequireLegacyTemplateTest covers the tokeniser context filtering, define/warn behaviour, scope preservation, missing-file return value, and the #5653 regression case. (794a179f, c2bb246b, b1290139)
language::bcDefs() no-argument default expanded. The boot-time call from class2.php:590 previously defined only LAN_180 → LAN_SEARCH. Legacy templates that referenced any other dropped v1.x constant whose replacement happens to live in English/English.php still fataled on PHP 8. The default now covers ~20 boot-resolvable aliases: generic actions (LAN_406 / LAN_419 / LAN_435), the v1 download prefix (LAN_dl_7..LAN_dl_35), and a defined-as-empty group for dropped-without-replacement constants.
Conflict policy: where the same legacy constant had divergent mappings across per-entrypoint shims (LAN_7..LAN_10, LAN_112, LAN_122, etc.) the global default deliberately omits them and per-entrypoint shims remain authoritative. Mappings whose replacement lives in a lazy lan_*.php cannot be resolved at boot (defined() returns false and the entry silently no-ops); those are still handled by per-entrypoint language::bcDefs() calls after the relevant e107::coreLan(). Refs #5653. (d2ef6b10)
Host comparison helper. A new e107::normaliseHost() lowercases, strips a trailing :port, and strips a leading www. before comparison. Plugins that previously did their own Host-header guards should switch to e107::isAllowedHost() so the trusted_hosts allow-list composes correctly. (aba3b169)
resize_image() shell escaping. The convert command line now passes the destination through escapeshellarg() and casts the integer geometry/quality args. If you maintain a fork or call resize_image() directly from a plugin, no migration is required, but any caller that previously relied on quoting the destination itself can drop that quoting. Ref GHSA-3j33-c9v4-4p42. (794a179f)
thumb.php bootstrap. The manual bootstrap chain (prepare_request / setDirs / set_constants / ...) is replaced by e107::initCore() fed the same mySQL-prefixed $sql_info compact() array class2.php passes.
Note the v2.3.x e107::_init() does not translate keys the way master's e107::setMySQLConfig() does, so the array must go in prefixed. The master-style str_replace('mySQL', '', $k) translation breaks v2.3.x because e_db_pdo still reads $config['mySQLserver'] etc. Follow-up to #5664 / #5665. (02a79729)
submitnews.php title slug. The slug used to build the destination filename for resize is now confined to [A-Za-z0-9_] before reaching resize_image(), so shell metacharacters never escape the filter even if a future caller relaxes the filename quoting. Ref GHSA-3j33-c9v4-4p42. (794a179f)
resize_handlerTest teardown sentinel. tearDown() ran even when setUp() had short-circuited via markTestSkipped() (CI images without ImageMagick); in that case $savedPref === null took the unset($GLOBALS['pref']) branch and wiped the global pref array for every following test in the shuffled unit-suite run. A prefMutated sentinel now flips true only after _before() mutates $pref, and _after() bails out early otherwise.
Locally invisible because ImageMagick was usually installed and the early-skip branch wasn't taken. (8cf19729)
CI on the legacy PHP cells. actions/checkout@v3 was reclassified to a Node 20 action, which requires glibc 2.27+; the official php:5.6 / php:7.0 images are on Debian 9 (glibc 2.24) and the action errors before its main logic. The two legacy cells now use an inline git fetch + git checkout against the run SHA.
The php:5.6 × mysql:8.0 and php:7.0 × mysql:8.0 combinations have also been dropped via matrix.exclude. Mysql:8 starts with character-set-server=utf8mb4 by default and the PDO driver in those PHP versions doesn't recognise the charset, failing the connection handshake before any test runs. (d024d205, 756dc92c)
Composer audit no longer blocks resolution. Composer 2.9.x refuses to resolve package versions affected by a Packagist security advisory at install time. Every twig/twig between v1.28 and v3.20 carries at least one advisory, leaving v3.26.0 as the only resolution candidate; v3.26.0 requires PHP 8.1+, so the PHP 7.4 cell of the unit-test matrix failed composer update. The test harness is dev-only and never ships with a release, so the audit's resolution-time block is now disabled via config.audit.block-insecure: false. composer audit still runs after install and reports any advisories. (46351d1a)
Matrix CI design restored. The "Collapse 5×5 matrix into one host-orchestrated job" experiment that landed during the v2.3.5 cycle was reverted: single jobs ran ~20 minutes end-to-end vs ~5 minutes per cell under master's parallel matrix, and exhibited intermittent docker / mysql container startup races at the per-cell DB pre-create step. v2.3.x CI is back to the matrix layout that mirrors master, with the legacy PHP cells extended via the inline-checkout and matrix.exclude pair above. (da47684c)
language::bcDefs() graveyard collisions on v2.3.x. Pre-defining LAN_199, LAN_406, and LAN_419 at boot collided with v2.3.x's lazy lan_search.php and lan_upload.php (which still use define(); master converted them to array form in #5465). PHPUnit's convertWarningsToExceptions=true escalated the "Constant already defined" warning and aborted mid-load. Those three entries are dropped on this branch; the lazy LAN files remain authoritative. (f1b4bf27)
Caution
v2.3.5 is a security release for sites on v2.3.4 or earlier.
Upgrade from v2.3.4 or earlier 2.x. If your site tracks the master branch, you are already past v2.3.5, so installing it would be a downgrade. v2.4.x is planned to be the next forward step.
Important
v2.3.5 ships a single fix for a CSRF vulnerability in the AJAX comment moderation endpoints. Sites that allow user comments and have at least one logged-in moderator or admin should upgrade promptly.
delete, approve, and edit branches in comment.php previously executed when only the payload and the ADMIN gate were present, so a cross-origin POST without an e-token would still go through. A logged-in moderator visiting an attacker page could be made to block, approve, or edit comments without their knowledge. The endpoints now reject any mutation request that lacks a valid form token, and the front-end UI threads the token through the delete/approve/edit anchors so legitimate clicks keep working. (a46a77a6)/comment.php?mode=delete, ?mode=approve, and ?mode=edit now return {"msg":"Unauthorized access!","error":true} when the request omits or fails the e-token check. The shipped front-end UI was updated in lockstep, so this is transparent for legitimate use. If you maintain a custom theme that replaces the comment-options or comment-edit shortcodes, see the developer note below. Ref: GHSA-m4hh-m278-jwg5. (a46a77a6)Comment-options shortcodes now emit data-token. comment_shortcodes::sc_comment_delete() and the comment-edit anchor render a data-token attribute populated from e_TOKEN. The corresponding handlers in front.jquery.js read it back and include e-token in the AJAX payload. If you ship a fork of these shortcodes or a custom AJAX client that talks to /comment.php, mirror the change — without it your delete/approve/edit calls will be rejected. Ref: GHSA-m4hh-m278-jwg5. (a46a77a6)
Token enforcement is local to comment.php for now. The central session_handler::check() still only validates the token when one is present. If you add new AJAX mutation branches to comment.php or to similar endpoints, guard them explicitly with e107::getSession()->checkFormToken($_POST['e-token']) until the central gate is tightened.