From 686276e4f6fa347970b2b1ac817252ab12f61a9f Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Fri, 19 Jun 2026 23:49:54 +0600 Subject: [PATCH 1/7] enhancement: hide Subscription Token value on settings page - Change token input from type=text to type=password - Remove token value from HTML, use masked placeholder instead - Preserve existing token in DB when field submitted empty - Update description text to reflect new behavior Fixes #4305 --- includes/classes/Screen/Settings.php | 6 ++++++ includes/partials/settings-page.php | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/includes/classes/Screen/Settings.php b/includes/classes/Screen/Settings.php index 0e1dacf6ac..0ce6d7414e 100644 --- a/includes/classes/Screen/Settings.php +++ b/includes/classes/Screen/Settings.php @@ -103,6 +103,12 @@ public function action_admin_init() { 'token' => '', ]; + // Preserve the existing token if the field was left empty (it is always empty on load). + if ( empty( $credentials['token'] ) ) { + $prev_credentials = Utils\get_epio_credentials(); + $credentials['token'] = $prev_credentials['token']; + } + Utils\update_option( 'ep_credentials', $credentials ); } diff --git a/includes/partials/settings-page.php b/includes/partials/settings-page.php index 41e44b9dfa..6eadfc461f 100644 --- a/includes/partials/settings-page.php +++ b/includes/partials/settings-page.php @@ -134,12 +134,12 @@ */ if ( apply_filters( 'ep_admin_show_credentials', true ) ) : ?> - disabled type="text" value="" name="ep_credentials[token]" id="ep_token"> + disabled type="password" value="" autocomplete="new-password" placeholder="" name="ep_credentials[token]" id="ep_token">

-

+

From 4e0092acba61175cd3cb30b71ac8d3ee478bec7e Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Mon, 6 Jul 2026 23:03:23 +0600 Subject: [PATCH 2/7] enhancement(settings): harden Subscription Token handling - Guard credentials update when EP_CREDENTIALS defined (prevent crafted POST leaking wp-config token to wp_options) - Reuse prev_ep_credentials instead of redundant get_epio_credentials read - Remove dead ternary branch - Add tests for token preserve-on-empty and new-token save - Add CHANGELOG Security entry Refs #4324 --- CHANGELOG.md | 4 +- includes/classes/Screen/Settings.php | 10 +--- tests/php/screen/TestSettings.php | 89 ++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a71e4f95e..540f565275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,15 @@ All notable changes to this project will be documented in this file, per [the Ke ## [Unreleased] +### Security +* Hide the Subscription Token value in the Settings page. Props [@faisalahammad](https://github.com/faisalahammad) via [#4324](https://github.com/10up/ElasticPress/pull/4324). + diff --git a/includes/classes/Screen/Settings.php b/includes/classes/Screen/Settings.php index 0ce6d7414e..dfb47c3fec 100644 --- a/includes/classes/Screen/Settings.php +++ b/includes/classes/Screen/Settings.php @@ -97,16 +97,12 @@ public function action_admin_init() { Utils\update_option( 'ep_host', $host ); } - if ( isset( $post['ep_credentials'] ) ) { - $credentials = ( isset( $post['ep_credentials'] ) ) ? Utils\sanitize_credentials( $post['ep_credentials'] ) : [ - 'username' => '', - 'token' => '', - ]; + if ( isset( $post['ep_credentials'] ) && ( ! defined( 'EP_CREDENTIALS' ) || ! EP_CREDENTIALS ) ) { + $credentials = Utils\sanitize_credentials( $post['ep_credentials'] ); // Preserve the existing token if the field was left empty (it is always empty on load). if ( empty( $credentials['token'] ) ) { - $prev_credentials = Utils\get_epio_credentials(); - $credentials['token'] = $prev_credentials['token']; + $credentials['token'] = $this->prev_ep_credentials['token']; } Utils\update_option( 'ep_credentials', $credentials ); diff --git a/tests/php/screen/TestSettings.php b/tests/php/screen/TestSettings.php index 11ed3032f1..30affd4913 100644 --- a/tests/php/screen/TestSettings.php +++ b/tests/php/screen/TestSettings.php @@ -108,6 +108,95 @@ public function test_action_admin_init_wrong_host() { $this->assertNotContains( 'ep_host', $_POST ); } + /** + * Test that an empty token in POST preserves the stored token. + * + * The token field is always empty on page load, so a save without a new + * value must not wipe the existing token. + * + * @group screen + * @group settings-screen + */ + public function test_action_admin_init_empty_token_preserves_stored_token() { + global $_POST; + + if ( defined( 'EP_CREDENTIALS' ) && EP_CREDENTIALS ) { + $this->markTestSkipped( 'EP_CREDENTIALS constant overrides the option.' ); + } + + // Make is_epio() true so get_epio_credentials() reads the option. + putenv( 'IS_EPIO_ENVIRONMENT=1' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv + + $settings = new Settings(); + + Utils\update_option( + 'ep_credentials', + [ + 'username' => 'u', + 'token' => 'secret', + ] + ); + + $_POST = [ + 'ep_settings_nonce' => wp_create_nonce( 'elasticpress_settings' ), + 'ep_language' => 'site-default', + 'ep_host' => Utils\get_host(), + 'ep_credentials' => [ + 'username' => 'u', + 'token' => '', + ], + ]; + + $settings->action_admin_init(); + + $this->assertSame( 'secret', Utils\get_option( 'ep_credentials' )['token'] ); + + putenv( 'IS_EPIO_ENVIRONMENT' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv + } + + /** + * Test that a new token value is saved. + * + * @group screen + * @group settings-screen + */ + public function test_action_admin_init_new_token_is_saved() { + global $_POST; + + if ( defined( 'EP_CREDENTIALS' ) && EP_CREDENTIALS ) { + $this->markTestSkipped( 'EP_CREDENTIALS constant overrides the option.' ); + } + + // Make is_epio() true so get_epio_credentials() reads the option. + putenv( 'IS_EPIO_ENVIRONMENT=1' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv + + $settings = new Settings(); + + Utils\update_option( + 'ep_credentials', + [ + 'username' => 'u', + 'token' => 'old', + ] + ); + + $_POST = [ + 'ep_settings_nonce' => wp_create_nonce( 'elasticpress_settings' ), + 'ep_language' => 'site-default', + 'ep_host' => Utils\get_host(), + 'ep_credentials' => [ + 'username' => 'u', + 'token' => 'new-token', + ], + ]; + + $settings->action_admin_init(); + + $this->assertSame( 'new-token', Utils\get_option( 'ep_credentials' )['token'] ); + + putenv( 'IS_EPIO_ENVIRONMENT' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv + } + /** * Test the `add_validation_notice` method * From b3f19c559ec4cf1e7d116a5935288a933868501d Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Fri, 7 Aug 2026 04:04:47 +0600 Subject: [PATCH 3/7] fix(settings): autocomplete off on token field Switch the Subscription Token input autocomplete attribute to off and add a Playwright spec covering the hidden token behavior: the value is never rendered, an empty save preserves the stored token, and a new value is saved. Addresses PR feedback. Refs #4324 --- includes/partials/settings-page.php | 2 +- tests/e2e/src/specs/settings-token.spec.ts | 84 ++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/src/specs/settings-token.spec.ts diff --git a/includes/partials/settings-page.php b/includes/partials/settings-page.php index 6eadfc461f..685ddb4e54 100644 --- a/includes/partials/settings-page.php +++ b/includes/partials/settings-page.php @@ -134,7 +134,7 @@ */ if ( apply_filters( 'ep_admin_show_credentials', true ) ) : ?> - disabled type="password" value="" autocomplete="new-password" placeholder="" name="ep_credentials[token]" id="ep_token"> + disabled type="password" value="" autocomplete="off" placeholder="" name="ep_credentials[token]" id="ep_token">

diff --git a/tests/e2e/src/specs/settings-token.spec.ts b/tests/e2e/src/specs/settings-token.spec.ts new file mode 100644 index 0000000000..abdf3d316c --- /dev/null +++ b/tests/e2e/src/specs/settings-token.spec.ts @@ -0,0 +1,84 @@ +import { test, expect } from '../fixtures.js'; +import { goToAdminPage, isEpIo, wpCli } from '../utils.js'; + +const FAKE_TOKEN = 'super-secret-token'; +const FAKE_USERNAME = 'ep-test-user'; +const EPIO_HOST = 'https://elasticpress.io'; + +/** + * Tests for the Subscription Token field on the ElasticPress Settings page. + * + * The token value is never rendered in the DOM and an empty POST value + * preserves the previously stored token. + */ +test.describe('Settings page Subscription Token field', { tag: '@group1' }, () => { + test.beforeAll(async () => { + if (isEpIo()) { + return; + } + + // Seed a known token and force is_epio() to true via the host pattern + // so the credentials row renders on the ElasticPress.io tab. + await wpCli(`option update ep_host '${EPIO_HOST}' --format=json`); + await wpCli( + `option update ep_credentials '{"username":"${FAKE_USERNAME}","token":"${FAKE_TOKEN}"}' --format=json`, + ); + }); + + test.afterAll(async () => { + if (isEpIo()) { + return; + } + + // Clean up the seeded options so other specs are not affected. + await wpCli('option delete ep_host'); + await wpCli('option delete ep_credentials'); + }); + + test('Token value is not exposed in the page', async ({ loggedInPage }) => { + test.skip(isEpIo(), 'Uses locally seeded credentials.'); + + await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress-settings'); + + const tokenInput = loggedInPage.locator('#ep_token'); + await expect(tokenInput).toBeVisible(); + await expect(tokenInput).toHaveAttribute('type', 'password'); + await expect(tokenInput).toHaveAttribute('autocomplete', 'off'); + await expect(tokenInput).toHaveValue(''); + + // The actual token must not appear anywhere in the rendered HTML. + const pageHtml = await loggedInPage.content(); + expect(pageHtml).not.toContain(FAKE_TOKEN); + }); + + test('Saving without changing the token preserves the stored value', async ({ + loggedInPage, + }) => { + test.skip(isEpIo(), 'Uses locally seeded credentials.'); + + await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress-settings'); + + // Submit the form with the token field left untouched. + await loggedInPage.click('#submit'); + await loggedInPage.waitForLoadState('networkidle'); + + const stored = await wpCli('option get ep_credentials --format=json'); + expect(stored?.toString()).toContain(FAKE_TOKEN); + }); + + test('Saving a new token value updates the stored value', async ({ loggedInPage }) => { + test.skip(isEpIo(), 'Uses locally seeded credentials.'); + + const newToken = 'rotated-token'; + + await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress-settings'); + + await loggedInPage.fill('#ep_token', newToken); + await loggedInPage.click('#submit'); + await loggedInPage.waitForLoadState('networkidle'); + + const stored = await wpCli('option get ep_credentials --format=json'); + expect(stored?.toString()).toContain(newToken); + expect(stored?.toString()).not.toContain(FAKE_TOKEN); + }); +}); From ad2a25be5f8255766032c80b16837bcb8fc012ae Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Fri, 7 Aug 2026 23:05:49 +0600 Subject: [PATCH 4/7] enhancement(settings): add remove token checkbox Add a checkbox on the ElasticPress settings page to clear the stored subscription token. When unchecked, an empty token POST still preserves the stored value (existing behavior). When checked, the token is cleared from wp_options while the username is preserved. Props @faisalahammad Fixes #4324 --- CHANGELOG.md | 4 +- includes/classes/Screen/Settings.php | 20 +++++++--- includes/partials/settings-page.php | 6 +++ tests/e2e/src/specs/settings-token.spec.ts | 14 +++++++ tests/php/screen/TestSettings.php | 45 ++++++++++++++++++++++ 5 files changed, 83 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 540f565275..c0df50088d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,13 @@ All notable changes to this project will be documented in this file, per [the Ke ## [Unreleased] +### Added +* "Remove the saved subscription token" checkbox on the Settings page so subscribers can clear the stored token without editing `wp_options`. Props [@faisalahammad](https://github.com/faisalahammad) via [#4324](https://github.com/10up/ElasticPress/pull/4324). + ### Security * Hide the Subscription Token value in the Settings page. Props [@faisalahammad](https://github.com/faisalahammad) via [#4324](https://github.com/10up/ElasticPress/pull/4324).