Skip to content

Commit 2110ec2

Browse files
Release 2.5.16 — sanitized sync (selector iframe and nginx proxy AppSec)
Port the ElementTest-WP 2.5.16 security fixes and the unpublished 2.5.15 tracking fixes, while keeping this repository as the update source. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a2cdc59 commit 2110ec2

11 files changed

Lines changed: 265 additions & 40 deletions

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
`readme.txt` remains the canonical WordPress.org release history for this plugin. This file mirrors the shipped release notes in a GitHub-friendly format.
44

5+
## 2.5.16
6+
7+
> AppSec hardening for the admin element selector iframe and the Nginx reverse-proxy IP trust model, plus the tracking fixes from ElementTest-WP 2.5.15 that were not in the published public 2.5.15. JS `VERSION` synced to 2.5.16.
8+
9+
- Security: **Element selector iframe no longer combines `allow-scripts` with `allow-same-origin`.** Proxied page JS cannot read `elementtestAdmin` / the admin nonce from `window.parent` or drive privileged `admin-ajax.php` actions with the admin session. Selector communication stays on `postMessage`; the inject script targets `*` because the sandboxed frame has an opaque origin.
10+
- Security: **`proxy_page()` strips scripts / `javascript:` URLs / inline event handlers** from proxied HTML and emits a CSP that only allows the plugin's selector inject script.
11+
- Security: **Nginx reverse-proxy preset ships no default trusted CIDRs.** Loopback/RFC1918 defaults let clients spoof `X-Forwarded-For` / `X-Real-IP` on common php-fpm setups where `REMOTE_ADDR` is always `127.0.0.1`. Declare proxy egress via `elementtest_trusted_proxy_cidrs`; without it, forwarded headers are ignored.
12+
- Fix: **Hash-fragment pageview goals** (`/thank-you#paid`, query+hash, and hash wildcards) now accept the browser fragment after the fragmentless path/query matches the signed `page_context` URL.
13+
- Fix: **Cached public tracking** can authenticate with an unexpired signed `page_context` token when the localized public nonce is stale. Endpoint handlers still verify test, page/goal scope, assignment proof, and rate limits.
14+
- Fix: **Percent-encoded request URIs** are preserved when minting frontend page-context URLs. Control characters are still stripped.
15+
- Fix: **Page-context token TTL** now follows the Cookie Duration setting (`cookie_days`, default 30 days, capped at 365). `elementtest_page_context_token_ttl` can still shorten or extend the default.
16+
517
## 2.5.15
618

719
> Public tracking page-context hardening and selector-proxy redirect lock-down, plus admin save-test variant prune reorder so goal validation failures cannot delete variants. Ported from ElementTest-WP 2.5.14 (PRs #7 and #11). JS `VERSION` synced to 2.5.15.

‎README.md‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ If only Apache is listening on 80/443, select **None**. If Nginx is on 80/443 wi
191191
Forwarded headers (`X-Forwarded-For`, `X-Real-IP`, `CF-Connecting-IP`, or your custom header) are spoofable by any client that connects to WordPress directly, so ElementTest only honors them when the **direct connection** (`REMOTE_ADDR`) comes from a trusted proxy:
192192

193193
- **Cloudflare** — trusts requests arriving from Cloudflare's published edge IP ranges.
194-
- **Nginx / Managed Hosting** — trusts requests arriving from loopback and private (RFC1918) ranges, which covers same-host and internal-network reverse proxies.
194+
- **Nginx / Managed Hosting** — enables reading `X-Real-IP` / `X-Forwarded-For`, but ships **no default trusted CIDRs** (loopback/RFC1918 defaults were unsafe on common php-fpm setups where every request appears as `127.0.0.1`). Add your proxy's egress range with `elementtest_trusted_proxy_cidrs` or forwarded headers stay ignored.
195195
- **Custom** — ships no trusted ranges by default; you must declare the IP(s) your proxy connects from (see below).
196196

197197
When the direct connection is **not** a trusted proxy, forwarded headers are ignored and the direct connection IP is used. A request that bypasses the proxy and hits `admin-ajax.php` directly therefore cannot forge its IP to evade rate limiting or deduplication.
@@ -208,7 +208,7 @@ add_filter( 'elementtest_trusted_proxy_headers', function () {
208208
} );
209209
```
210210

211-
`elementtest_trusted_proxy_cidrs` — the IP ranges your proxy connects **from**. Add this if your proxy (or load balancer) reaches WordPress from a public IP not covered by the presets above; otherwise its forwarded headers will be ignored:
211+
`elementtest_trusted_proxy_cidrs` — the IP ranges your proxy connects **from**. Required for the Nginx and Custom presets (and for Cloudflare if you terminate TLS on your own edge). Example for a load balancer that reaches WordPress from a known egress range:
212212

213213
```php
214214
add_filter( 'elementtest_trusted_proxy_cidrs', function ( $cidrs ) {
@@ -217,6 +217,16 @@ add_filter( 'elementtest_trusted_proxy_cidrs', function ( $cidrs ) {
217217
} );
218218
```
219219

220+
If PHP-FPM only accepts connections from Nginx on the same host **and** the web server is locked down so clients cannot reach PHP directly, you may include loopback explicitly (this is no longer the plugin default):
221+
222+
```php
223+
add_filter( 'elementtest_trusted_proxy_cidrs', function ( $cidrs ) {
224+
$cidrs[] = '127.0.0.1/32';
225+
$cidrs[] = '::1/128';
226+
return $cidrs;
227+
} );
228+
```
229+
220230
Both IPv4 and IPv6 CIDRs are supported. A bare IP (no `/`) is treated as a single host.
221231

222232
## Report Export (WP-CLI)

‎assets/js/element-selector.js‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@
6060
'<div class="ets-loading-spinner"></div>' +
6161
'<span>Loading page&hellip;</span>' +
6262
'</div>' +
63-
'<iframe id="ets-iframe" class="ets-iframe" sandbox="allow-same-origin allow-scripts"></iframe>' +
63+
// Scripts-only sandbox: omit allow-same-origin so proxied
64+
// page JS cannot read parent.elementtestAdmin (admin nonce)
65+
// or call privileged admin-ajax with the admin session.
66+
'<iframe id="ets-iframe" class="ets-iframe" sandbox="allow-scripts"></iframe>' +
6467
'</div>' +
6568

6669
/* Bottom panel — selected element info */
@@ -172,6 +175,11 @@
172175
function handleMessage( event ) {
173176
var data;
174177

178+
// Ignore messages when the selector is closed.
179+
if ( ! isOpen ) {
180+
return;
181+
}
182+
175183
try {
176184
data = ( typeof event.data === 'string' ) ? JSON.parse( event.data ) : event.data;
177185
} catch ( e ) {

‎assets/js/frontend.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ try {
3131
// Constants & State
3232
// =========================================================================
3333

34-
var VERSION = '2.5.15';
34+
var VERSION = '2.5.16';
3535
var OBSERVER_TIMEOUT = 8000; // Max time (ms) to wait for elements via MutationObserver.
3636
var ANTIFLICKER_TIMEOUT = 3000; // Max time (ms) before forcing anti-flicker removal.
3737

‎assets/js/selector-inject.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,11 @@
323323
}
324324

325325
try {
326-
window.parent.postMessage( JSON.stringify( msg ), window.location.origin );
326+
// Target '*' so postMessage still works when the iframe is sandboxed
327+
// without allow-same-origin (opaque unique origin). The parent
328+
// validates msg.source === 'elementtest-selector' and only accepts
329+
// messages while the selector overlay is open.
330+
window.parent.postMessage( JSON.stringify( msg ), '*' );
327331
} catch ( e ) {
328332
// Silently ignore.
329333
}

‎elementtest-pro.php‎

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Plugin Name: ElementTest Pro
44
* Plugin URI: https://github.com/DougState/elementtest-pro
55
* Description: A/B test various elements (CSS, copy, JS, images) of your pages and track conversion data to measure performance.
6-
* Version: 2.5.15
6+
* Version: 2.5.16
77
* Requires at least: 5.6
88
* Tested up to: 7.0
99
* Requires PHP: 7.4
@@ -23,7 +23,7 @@
2323
}
2424

2525
// Define plugin constants
26-
define( 'ELEMENTTEST_VERSION', '2.5.15' );
26+
define( 'ELEMENTTEST_VERSION', '2.5.16' );
2727
define( 'ELEMENTTEST_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
2828
define( 'ELEMENTTEST_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
2929
define( 'ELEMENTTEST_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
@@ -120,7 +120,7 @@ public function resolve_proxy_headers( $headers ) {
120120
* Forwarded IP headers are only honored when the direct connection
121121
* (REMOTE_ADDR) falls inside one of these ranges, so a request that
122122
* bypasses the proxy cannot spoof its IP. Sites whose proxy connects
123-
* from a public IP not covered by the defaults below should add their
123+
* from a public IP (or any nginx/managed-host setup) should add their
124124
* proxy's CIDR via the `elementtest_trusted_proxy_cidrs` filter.
125125
*
126126
* @since 2.5.4
@@ -160,16 +160,13 @@ public function resolve_proxy_cidrs( $cidrs ) {
160160
) );
161161
break;
162162
case 'nginx':
163-
// Loopback + RFC1918 private ranges: the same-host or
164-
// internal-network reverse proxy used by most managed hosts.
165-
$cidrs = array_merge( $cidrs, array(
166-
'127.0.0.0/8',
167-
'10.0.0.0/8',
168-
'172.16.0.0/12',
169-
'192.168.0.0/16',
170-
'::1/128',
171-
'fc00::/7',
172-
) );
163+
// Intentionally ships no default CIDRs. Trusting loopback /
164+
// RFC1918 by default is unsafe on common php-fpm setups where
165+
// REMOTE_ADDR is 127.0.0.1 for every request — any client that
166+
// can reach PHP (or whose connection appears as loopback) could
167+
// spoof X-Forwarded-For / X-Real-IP. Declare your proxy's real
168+
// egress CIDR via the elementtest_trusted_proxy_cidrs filter;
169+
// without it, forwarded headers are ignored (secure default).
173170
break;
174171
// 'custom' ships no default CIDRs — the admin supplies the
175172
// proxy's address range via the elementtest_trusted_proxy_cidrs

0 commit comments

Comments
 (0)