Skip to content

Conversation

cmraible
Copy link
Collaborator

@cmraible cmraible commented Aug 12, 2025

ref https://linear.app/ghost/issue/ENG-2506/its-possible-to-bypass-paywall-on-a-post-by-constructing-a-preview

There was a bug in the post preview route that bypassed the normal redirect to the published post. The preview route should only be accessible for posts that are not published. This was caused by the post's status being stripped in some cases, which bypassed the logic to redirect to the published post's normal URL, and instead allowed the preview content to render in its entirety.

Copy link
Contributor

coderabbitai bot commented Aug 12, 2025

Walkthrough

  • In previews.js, when a member_status is present, the frame is flagged with apiType = 'content' and isPreview = true.
  • A new utility isPreview(frame) is exported from utils/index.js to detect preview frames.
  • In serializers/output/utils/clean.js, attrs.status is no longer removed for Content API preview frames; it’s still removed for non-preview frames. Other attribute deletions and null normalization remain unchanged.
  • An e2e test was added to verify that published post redirects behave the same when a member_status query parameter is provided.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-paywall-bug

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@cmraible cmraible changed the title Fixed published post preview route 🐛 Fixed published post preview route Aug 12, 2025
@cmraible cmraible changed the title 🐛 Fixed published post preview route 🐛 Fixed preview route for published post allowing access to members content Aug 12, 2025
@cmraible cmraible marked this pull request as ready for review August 12, 2025 04:17
@cmraible cmraible changed the title 🐛 Fixed preview route for published post allowing access to members content 🐛 Fixed preview route for published post bypassing redirect to the post's URL Aug 12, 2025
@cmraible cmraible changed the title 🐛 Fixed preview route for published post bypassing redirect to the post's URL 🐛 Fixed preview route for published post bypassing redirect to post URL Aug 12, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
ghost/core/core/server/api/endpoints/previews.js (1)

21-23: Setting frame.isPreview for member_status previews is correct and fixes status stripping

Marking preview frames ensures clean.post preserves attrs.status for Content API previews, restoring the redirect behavior for published posts accessed via preview URLs. Nice, tight fix that aligns with the new serializer logic.

As a small clarity nit, consider adding an inline comment to explain why isPreview is set here (it’s consumed by serializers to preserve status and support redirect logic):

     // only set apiType when given a member_status to preserve backwards compatibility
     // where we used to serve "Admin API" content with no gating for all previews
     frame.apiType = 'content';
+    // Mark as preview so serializers can preserve attrs.status (used for redirect decisions)
     frame.isPreview = true;
ghost/core/core/server/api/endpoints/utils/index.js (1)

50-52: Defensively handle undefined frames in isPreview

Minor hardening: use optional chaining so the helper gracefully handles unexpected undefined/null frames without throwing.

-    isPreview: (frame) => {
-        return frame.isPreview === true;
-    }
+    isPreview: (frame) => {
+        return frame?.isPreview === true;
+    }
ghost/core/test/e2e-frontend/preview_routes.test.js (1)

148-155: Good added coverage for published redirect with member_status=paid

This validates the regression path and ensures parity with the non-parameter case. Consider extending coverage to the other allowed values to fully exercise the validation matrix: ?member_status=free and ?member_status=anonymous.

Example additions:

it('should redirect published posts to their live url with ?member_status=free', async function () {
    await request.get('/p/2ac6b4f6-e1f3-406c-9247-c94a0496d39d/?member_status=free')
        .expect(301)
        .expect('Location', '/short-and-sweet/')
        .expect('Cache-Control', testUtils.cacheRules.year)
        .expect(assertCorrectFrontendHeaders);
});

it('should redirect published posts to their live url with ?member_status=anonymous', async function () {
    await request.get('/p/2ac6b4f6-e1f3-406c-9247-c94a0496d39d/?member_status=anonymous')
        .expect(301)
        .expect('Location', '/short-and-sweet/')
        .expect('Cache-Control', testUtils.cacheRules.year)
        .expect(assertCorrectFrontendHeaders);
});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9f9e6a and ad83c79.

📒 Files selected for processing (4)
  • ghost/core/core/server/api/endpoints/previews.js (1 hunks)
  • ghost/core/core/server/api/endpoints/utils/index.js (1 hunks)
  • ghost/core/core/server/api/endpoints/utils/serializers/output/utils/clean.js (1 hunks)
  • ghost/core/test/e2e-frontend/preview_routes.test.js (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
ghost/core/test/e2e-frontend/preview_routes.test.js (3)
ghost/core/test/e2e-frontend/advanced_url_config.test.js (2)
  • request (8-8)
  • testUtils (3-3)
ghost/core/test/unit/frontend/web/middleware/frontend-caching.test.js (1)
  • testUtils (3-3)
ghost/core/test/unit/frontend/services/routing/controllers/previews.test.js (1)
  • testUtils (3-3)
ghost/core/core/server/api/endpoints/previews.js (1)
ghost/core/test/unit/api/endpoints/previews.test.js (5)
  • frame (24-24)
  • frame (31-31)
  • frame (38-38)
  • frame (45-45)
  • frame (54-54)

@cmraible cmraible merged commit 0d0e5bd into main Aug 12, 2025
26 of 48 checks passed
@cmraible cmraible deleted the fix-paywall-bug branch August 12, 2025 04:33
cmraible added a commit that referenced this pull request Aug 12, 2025
…RL (#24653)

ref
https://linear.app/ghost/issue/ENG-2506/its-possible-to-bypass-paywall-on-a-post-by-constructing-a-preview

There was a bug in the post preview route that bypassed the normal
redirect to the published post. The preview route should only be
accessible for posts that are not published. This was caused by the
post's status being stripped in some cases, which bypassed the logic to
redirect to the published post's normal URL, and instead allowed the
preview content to render in its entirety.
cmraible added a commit that referenced this pull request Aug 12, 2025
…RL (#24653)

ref
https://linear.app/ghost/issue/ENG-2506/its-possible-to-bypass-paywall-on-a-post-by-constructing-a-preview

There was a bug in the post preview route that bypassed the normal
redirect to the published post. The preview route should only be
accessible for posts that are not published. This was caused by the
post's status being stripped in some cases, which bypassed the logic to
redirect to the published post's normal URL, and instead allowed the
preview content to render in its entirety.
cmraible added a commit that referenced this pull request Aug 12, 2025
…RL (#24653)

ref
https://linear.app/ghost/issue/ENG-2506/its-possible-to-bypass-paywall-on-a-post-by-constructing-a-preview

There was a bug in the post preview route that bypassed the normal
redirect to the published post. The preview route should only be
accessible for posts that are not published. This was caused by the
post's status being stripped in some cases, which bypassed the logic to
redirect to the published post's normal URL, and instead allowed the
preview content to render in its entirety.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants