Skip to content

✨ Improved protection against spam member signups#29319

Merged
9larsons merged 4 commits into
mainfrom
codex/enable-request-integrity-by-default
Jul 14, 2026
Merged

✨ Improved protection against spam member signups#29319
9larsons merged 4 commits into
mainfrom
codex/enable-request-integrity-by-default

Conversation

@9larsons

@9larsons 9larsons commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What changed

Enabled verifyRequestIntegrity in Ghost's default configuration.

Updated the members magic-link acceptance tests to fetch integrity tokens through the public Members API and include them in protected requests, while retaining explicit coverage for missing and invalid tokens.

Why

Request integrity validation has been available behind an opt-in flag since 2024, but leaving it disabled by default means installations do not benefit from the spam protection unless operators discover and configure the undocumented flag themselves.

With this change, member requests require the integrity token that Portal and Signup Form already send. Installations can still explicitly override the setting if needed.

Ref #20767

Testing

  • pnpm nx run ghost:test:e2e -- test/e2e-api/members/send-magic-link.test.js (46 tests passed)
  • pnpm exec eslint test/e2e-api/members/send-magic-link.test.js
  • pnpm test:single test/unit/shared/config/loader.test.js (8 tests passed)
  • pnpm test:single test/unit/server/services/members/middleware.test.js (19 tests passed)
  • pre-commit lint and secret scanning passed
  • git diff --check

ref #20767

Request integrity validation has been available behind an opt-in config flag since 2024 and is now ready to protect installations by default.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c7db0d65-1706-4028-8821-92c2ec1b9bed

📥 Commits

Reviewing files that changed from the base of the PR and between 13415e0 and 623979e.

📒 Files selected for processing (1)
  • ghost/core/core/server/services/members/service.js

Walkthrough

Adds the default verifyRequestIntegrity configuration property set to true, defers request integrity provider initialization until member service startup, and updates member authentication end-to-end tests to inject and validate integrity tokens across magic-link and OTC verification requests.

Suggested reviewers: mike182uk

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: enabling default protection against spam member signups.
Description check ✅ Passed The description clearly relates to the changes and explains the integrity-token and default-setting updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/enable-request-integrity-by-default

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Jul 14, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 13415e0

Command Status Duration Result
nx run ghost:test:ci:integration ✅ Succeeded 2m 43s View ↗
nx run ghost:test:integration ✅ Succeeded 2m 52s View ↗
nx run ghost:test:legacy ✅ Succeeded 2m 56s View ↗
nx run @tryghost/admin:build ✅ Succeeded 2m 34s View ↗
nx run ghost:test:e2e ✅ Succeeded 2m 26s View ↗
nx run-many --target=build --projects=tag:publi... ✅ Succeeded <1s View ↗
nx run ghost-monorepo:lint:boundaries ✅ Succeeded 7s View ↗
nx run-many -t lint -p ghost,ghost-monorepo ✅ Succeeded 23s View ↗
Additional runs (3) ✅ Succeeded ... View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-07-14 17:53:36 UTC

@9larsons
9larsons marked this pull request as ready for review July 14, 2026 16:57
ref #20767

Enabling integrity verification by default requires member API acceptance requests to include valid tokens before exercising their intended behavior.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
ghost/core/test/e2e-api/members/send-magic-link.test.js (1)

15-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make the agent patch more robust against arbitrary requests.

This monkey-patch applies to all POST requests made by the agent and assumes body is always provided when .body() is called. To prevent unexpected test errors if a test sends an empty body or makes a POST request to another endpoint, consider scoping the interception to the magic-link URL and providing a fallback for the body argument.

♻️ Proposed defensive refactor
-    agent.post = (url, options) => {
-        const request = post(url, options);
-        const setBody = request.body.bind(request);
-
-        request.body = body => {
-            const integrityToken = Object.hasOwn(body, 'integrityToken')
-                ? body.integrityToken
-                : membersService.requestIntegrityTokenProvider.create();
-
-            return setBody({...body, integrityToken});
-        };
-
-        return request;
-    };
+    agent.post = (url, options) => {
+        const request = post(url, options);
+
+        if (typeof url === 'string' && !url.includes('/send-magic-link')) {
+            return request;
+        }
+
+        const setBody = request.body.bind(request);
+
+        request.body = body => {
+            const payload = body || {};
+            const integrityToken = Object.hasOwn(payload, 'integrityToken')
+                ? payload.integrityToken
+                : membersService.requestIntegrityTokenProvider.create();
+
+            return setBody({...payload, integrityToken});
+        };
+
+        return request;
+    };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/test/e2e-api/members/send-magic-link.test.js` around lines 15 -
33, Update addIntegrityTokenToPostRequests so the patched agent.post
interception only modifies requests targeting the magic-link endpoint, leaving
other POST requests unchanged. Make the request.body wrapper tolerate an omitted
or undefined body by defaulting to an empty object before checking or adding
integrityToken.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@ghost/core/test/e2e-api/members/send-magic-link.test.js`:
- Around line 15-33: Update addIntegrityTokenToPostRequests so the patched
agent.post interception only modifies requests targeting the magic-link
endpoint, leaving other POST requests unchanged. Make the request.body wrapper
tolerate an omitted or undefined body by defaulting to an empty object before
checking or adding integrityToken.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8c596820-d975-465d-b632-812b369f806a

📥 Commits

Reviewing files that changed from the base of the PR and between a2bdf34 and 6662690.

📒 Files selected for processing (1)
  • ghost/core/test/e2e-api/members/send-magic-link.test.js

ref #20767

Acceptance requests now obtain tokens through the public endpoint, matching the frontend flow without coupling tests to the server token provider.
@9larsons
9larsons enabled auto-merge (squash) July 14, 2026 17:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
ghost/core/test/e2e-api/members/send-magic-link.test.js (1)

24-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard against null or undefined body parameters.

If a future test calls .body() without arguments or passes null, Object.hasOwn will throw a TypeError. Adding a defensive check prevents potential test suite crashes. As a bonus, {...undefined, integrityToken: token} gracefully evaluates to { integrityToken: token }, ensuring the token is still sent even if the test omits a body.

🛠️ Proposed refactor
-    request.body = body => {
-        const token = Object.hasOwn(body, 'integrityToken') ? body.integrityToken : integrityToken;
-        return setBody({...body, integrityToken: token});
-    };
+    request.body = body => {
+        const token = body && Object.hasOwn(body, 'integrityToken') ? body.integrityToken : integrityToken;
+        return setBody({...body, integrityToken: token});
+    };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/test/e2e-api/members/send-magic-link.test.js` around lines 24 -
27, Update the body override callback to safely handle null or undefined body
values before calling Object.hasOwn, while preserving any explicitly provided
integrityToken and injecting the default token otherwise. Ensure omitted bodies
still produce a request body containing integrityToken.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@ghost/core/test/e2e-api/members/send-magic-link.test.js`:
- Around line 24-27: Update the body override callback to safely handle null or
undefined body values before calling Object.hasOwn, while preserving any
explicitly provided integrityToken and injecting the default token otherwise.
Ensure omitted bodies still produce a request body containing integrityToken.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b913e680-35d9-40fe-8dca-d1f6b02cc4af

📥 Commits

Reviewing files that changed from the base of the PR and between 6662690 and 13415e0.

📒 Files selected for processing (1)
  • ghost/core/test/e2e-api/members/send-magic-link.test.js

ref #20767 (comment)

The provider needs the theme session secret, so it must be created after settings are initialized rather than when the service module is loaded.
@9larsons
9larsons merged commit bd15edc into main Jul 14, 2026
44 checks passed
@9larsons
9larsons deleted the codex/enable-request-integrity-by-default branch July 14, 2026 17:59
@markstos

Copy link
Copy Markdown
Contributor

Thanks. Now that it's official, will it become documented here:

https://docs.ghost.org/config

To give folks a hint under what conditions they might want to disable it?

It seems like according to @cathysarisky there may be some integrations with the "magic link" endpoint in the wild that may break when its enabled:

https://www.spectralwebservices.com/blog/sign-in-changes-afoot/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants