Skip to content

AI Copilot: gate comment search on parent-post readability - #804

Merged
epeicher merged 4 commits into
trunkfrom
opensta-152-openstation-missing-authorization-in-the-copilot-comment
Sep 12, 2026
Merged

AI Copilot: gate comment search on parent-post readability#804
epeicher merged 4 commits into
trunkfrom
opensta-152-openstation-missing-authorization-in-the-copilot-comment

Conversation

@epeicher

@epeicher epeicher commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

A Subscriber can no longer read comments attached to private, draft, or password-protected posts through the Copilot's comment search abilities.

desktop-mode/search-comments and desktop-mode/search-comments-by-post authorized on the comment's moderation status alone: get_comments() ran with status => approve and nothing about the parent post. Both abilities are read-only and REST-exposed, so Core dispatches them over GET to any logged-in user, who received the comment text and the private parent's title. Approval is a moderation decision about the comment; it has never meant the discussion is visible.

Comment results are now gated on the parent post's readability, mirroring Core's WP_REST_Comments_Controller::check_read_post_permission():

  • search_comments drops every row whose parent the caller cannot read. A password-protected parent requires the password satisfied or edit_post (post_password_required() honours the wp-postpass cookie Core's password form sets after the correct password is entered, matching Core's comments controller; the ability itself has no password input), a publicly viewable parent passes, a parent of a non-viewable post type (an internal/admin-only CPT) requires edit_post (for a public status, read_post resolves to plain read, so the capability alone cannot gate those), and any other parent requires read_post.
  • search_comments_by_post refuses the whole request up front when the target post is not readable, returning an empty batch without the parent title, and with the same error shape for a nonexistent and an unreadable post.
  • Orphaned comments (comment_post_ID of 0) are dropped rather than judged against the global $post that get_post( 0 ) falls back to.
  • The final entity record re-checks readability on the model-supplied id, for the comment branch and the post/page branch. Model output is untrusted (a search turn can be driven by attacker-controlled content), so hydration cannot assume the id came out of a filtered tool result. Unapproved comments hydrate only for callers with edit_comment. Unreadable ids resolve to null, indistinguishable from nonexistent ones.

The gate is per-caller, not a blanket publish filter: an administrator still finds comments on private posts. The abilities GET path and the Copilot's own search loop run through the same fetch handlers, so both are covered at the source. As in Core's comments controller, rows are filtered after the query, so total still counts all approved matches; the returned items and titles are what carry content. The ability descriptions and docs/javascript-reference.md document the caller-scoped filtering and the pagination semantics.

Validation

  • 8 regression tests: a Subscriber against private, password-protected, and non-viewable-CPT parents, an administrator still seeing private-post comments, the by-post tool on a private post returning no items and no title, the orphaned-comment guard against a seeded global $post, and entity hydration refusing unreadable posts, hidden comments, and unapproved comments.
  • Full PHPUnit suite green: 3,044 tests, 23,091 assertions (13 pre-existing skips).
  • Password, status, and post-type semantics verified line by line against Core's WP_REST_Comments_Controller as the reference behavior.
  • phpcs -n clean on the touched source; npm run build produced no further diffs.
Open WordPress Playground Preview

The search-comments and search-comments-by-post abilities authorized on
the comment's moderation status alone, so any logged-in user could read
approved comments attached to private, draft, and password-protected
posts, along with the private parent's title. Filter results by the
caller's ability to read the parent post, mirroring Core's
WP_REST_Comments_Controller::check_read_post_permission().

Copilot AI 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.

🟡 Changes recommended

Parent checks remain bypassable through custom post types and final entity hydration.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds parent-post authorization to AI comment search and regression coverage.

Changes:

  • Filters comment results by parent readability.
  • Rejects unreadable by-post searches.
  • Adds subscriber/admin permission tests.
File summaries
File Description
includes/ai-copilot/search.php Adds parent visibility gates.
tests/phpunit/tests/aiNativeSearch.php Adds authorization regression tests.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread includes/ai-copilot/search.php Outdated
Comment thread includes/ai-copilot/search.php
Comment thread includes/ai-copilot/search.php
Comment thread includes/ai-copilot/search.php
Review follow-ups: a published post of a non-viewable post type no
longer counts as readable (publish alone is not visibility, and
read_post resolves to plain read for public statuses, so those parents
now require edit_post); the final entity builder re-checks readability
on the model-supplied id for both the comment and post branches, since
model output is untrusted, and also honours the comment's moderation
status. Adds regression tests for the orphaned-comment guard, the
non-viewable CPT case, and entity hydration, and documents the
caller-scoped filtering in the ability descriptions and the JS
reference.
…ion-missing-authorization-in-the-copilot-comment

Copilot AI 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.

🟡 Changes recommended

A valid post-password cookie bypasses the stated edit_post requirement.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread includes/ai-copilot/search.php
A caller holding a valid wp-postpass cookie has already entered the
correct password, and post_password_required() honouring it is Core
parity (the comments REST controller reads the same cookie), not a
bypass. The docblock overclaimed that the password could not be
supplied at all; correct it and pin the intended behaviour with a
regression test so a future hardening pass does not lock out readers
the author gave the password to.

Copilot AI 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.

🟢 Approval recommended

The authorization checks now cover search, by-post lookup, and untrusted entity hydration with focused regression tests and updated contracts.

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@epeicher
epeicher enabled auto-merge (squash) September 12, 2026 09:47
@epeicher
epeicher merged commit 00f63b2 into trunk Sep 12, 2026
8 checks passed
@epeicher
epeicher deleted the opensta-152-openstation-missing-authorization-in-the-copilot-comment branch September 12, 2026 09:55
epeicher added a commit that referenced this pull request Sep 12, 2026
…ion-missing-authorization-to-authenticated

#804 landed the same class of fix in parallel, so the two sides had grown
their own readability helper. Collapse onto trunk's: openstation_ai_can_read_post()
is a superset of the one this branch added — it takes an id or an object,
refuses id 0 rather than letting get_post() fall back to the global $post,
and already gates the post type, which is what the last review round asked
for here.

Trunk also settled the shape of the comment-search gate, and it settled it
against the one this branch had built. Filtering parent status and post
type in the query made `total` exact, but a query var cannot say "readable
by THIS caller": trunk pins an Administrator still finding comments on
private posts, and a reader who entered a post password still reading that
post's discussion. Both are right, and both are lost to a blanket
publish-only filter. The per-row filter stays, the counter skew is now
stated where it happens, and the agents-security checklist says which of
the two shapes a new gate should reach for.

What survives from this branch on top of trunk: the post/page pin on the
entity card (a viewable CPT row passes the read gate and still has no card
to land in), the moderation verdicts following `moderate_comments`, and
the wp-admin edit link following `edit_comment`.
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