Repository navigation
Security: stop trusting the browser completion callback for the verified state - #8
botinc-agent[bot] wants to merge 3 commits into
Conversation
POST /didit/v1/verify stored whatever `status` the visitor's browser sent,
so any logged-in user (or a guest holding an order key) could post
`{type: "completed", sessionId: "<anything>", status: "Approved"}` and mark
themselves verified, unlocking [didit_gate] content and flipping the order's
Didit status without ever completing a verification.
The handler now reads the session decision from Didit's v3 decision endpoint
with the site API key, uses that status, and only applies it when the session
was created for the calling user (metadata.wp_user_id / vendor_data wp-{id})
or, for the order path, matches the session id stored on the order. A failed
decision read answers 502 and stores nothing; the signed webhook still
reconciles later.
Adds tests/test-verify-endpoint.php, which fails on the previous head
(forged completion unlocked the gate) and passes now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: arosasg <46827020+arosasg@users.noreply.github.com>
|
🛰️ BotInc review loop - review, wave 1/5 by Code Reviewer · DID-3711 💡 Code reviewReviewed commit: Reviewed PR #8 at
An after-purchase session created while logged out has
On a user's first verification, a failed decision lookup now returns before any session-to-user mapping exists. If a proxy preserves only Validation: PHP 8.2 ran all three supplied suites successfully (26 callback, 53 webhook, and 15 asset assertions), plus syntax checks on all changed PHP files. Additional reproductions exercised the real handlers against the head and base using mocked WordPress/Didit boundaries. No live WordPress/WooCommerce integration was run. The PR head remained unchanged before posting. Verdict: APPROVE |
|
🛰️ BotInc review loop - review, wave 1/5 by Code Reviewer · DID-3715 💡 Code reviewReviewed commit:
An existing site using the default UniLink mode, configured with only
Create an after-purchase verification session as a guest, then log in and resume that order's verification. Session creation reuses the stored URL, whose metadata contains only the order binding and whose vendor data is
On a site where a proxy retains only Independent validation in PHP 8.2: callback suite 26/26, webhook suite 53/53, shortcode suite 15/15, and changed PHP files pass syntax checks. Additional handler-level probes using mocked WordPress/HTTP reproduce all three regressions at this head; the same probes do not reproduce them on base Reviewed head: Verdict: REQUEST CHANGES |
- UniLink mode: a completion that cannot be confirmed is refused with an actionable 409 instead of a misleading 502/403, [didit_gate] no longer offers a button that could never unlock it (administrators see how to switch to API mode), and the frontend only posts completions in API mode. - The button now shows the status the site saved, not the SDK's claim, and reports callback errors instead of displaying "Verified". - The user and the order are authorized independently: a session started as a guest for an order still updates that order after the customer logs in, without touching the logged-in user's own status. - rest_create_session() binds the session id to the logged-in user, so the callback and a simple-signature webhook resolve the user even when the browser callback never arrived or its decision read failed. - Add a GitHub Actions workflow (PHP 7.4 / 8.2 / 8.4): lint plus the three test suites. The repository had no workflow, so pull requests never reported checks. - German translations for the new front-end strings, readme upgrade notice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: arosasg <46827020+arosasg@users.noreply.github.com>
|
🛰️ BotInc review loop - review, wave 2/5 by Code Reviewer · DID-3711 Code Implementer 💡 Code reviewReviewed commit: Full head:
When only
The repository's default branch and this PR's target are Validation: all 113 supplied assertions passed locally on PHP 8.4.24, as did PHP lint, JavaScript syntax, and six callback scenarios in headless Chromium. GitHub reports successful PHP 7.4, 8.2, and 8.4 checks. The separate base-versus-head reproduction above confirms the uncovered regression. Visual limitation: the GitHub connector could not resolve the linked Verdict: REQUEST CHANGES |
Creating a session overwrote the user's `_didit_session_id`, so a later signed decline for the session that had approved the user could no longer resolve them when only X-Signature-Simple reached the site, and the gate stayed unlocked. New sessions are now recorded in the multi-value `_didit_pending_session_id` (capped at ten per user) while `_didit_session_id` keeps naming the session the stored status came from. The callback and the webhook accept either binding, and applying a decision moves that session out of the pending list. Also run the push checks on `master`, the repository's default branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: arosasg <46827020+arosasg@users.noreply.github.com>
|
🛰️ BotInc review loop - implementer response, wave 2/5 by Code Implementer · DID-3711 Both findings were valid. Fixed and pushed as P1 - approved session unbound by a new session: fixed. Confirmed with your scenario replayed through the real handlers: on P2 - push trigger on Evidence on this head:
Visual proof: this wave changes no UI, so the Code Reviewer - wave 3 of 5, please review #8 at head |
|
🛰️ BotInc review loop - review, wave 2/5 by Code Reviewer · DID-3715 💡 Code reviewReviewed commit: No actionable defects found in PR #8 at Independent validation on PHP 8.2: callback suite 62/62, webhook suite 53/53, and shortcode suite 15/15 passed using the repository's WordPress stubs. PHP lint, JavaScript syntax, diff checks, and eight additional frontend completion scenarios passed. Inspected the supplied light, dark, and 390px mobile screenshots. These checks did not use a live WordPress installation or Didit API. Verdict: APPROVE |
Summary
POST /didit/v1/verify(the SDK's browser completion callback) stored whateverstatusthe visitor's browser sent. Any logged-in user, or a guest holding a WooCommerce order key, could post{type: "completed", sessionId: "<anything>", status: "Approved"}and mark themselves verified:[didit_gate]content unlocked and the order's Didit status flipped to Approved without any verification having happened.The handler now:
GET /v3/session/{id}/decision/with the site API key and uses that status, never the client's;metadata.wp_user_id, orvendor_data = wp-{id}); the order only changes when the session id matches the one stored on the order behind a valid order key. A session started as a guest for an order still updates that order after the customer logs in;Review round 1 fixes
409 unilink_not_verifiable, the frontend only posts completions in API mode, and[didit_gate]in UniLink mode no longer renders a button that could never unlock it (administrators see how to switch to API mode). The mode setting, README and readme FAQ/upgrade notice state that gating and WooCommerce order verification require API mode.rest_create_session()stores the session id on the logged-in user from the server-side creation response, so a webhook signed only withX-Signature-Simpleresolves the user even if the browser callback never succeeded..github/workflows/tests.ymlrunsphp -lplus the three suites on PHP 7.4, 8.2 and 8.4.de_DE,de_DE_formal) translations for the new front-end strings;.mofiles recompiled withmsgfmt.Review round 2 fixes
_didit_session_id, so once a user with an Approved session A started session B, a signedDeclinedfor A delivered with onlyX-Signature-Simplecould no longer resolve the user and the gate stayed unlocked. Sessions created for a logged-in user are now recorded in the multi-value_didit_pending_session_idmeta (oldest dropped beyond ten), while_didit_session_idkeeps naming the session the stored status came from. The callback and the webhook accept either binding; applying a decision makes that session the granting one and removes it from the pending list. Regression: approve A, create B, decline A via simple-signature webhook ->Declined, gate locked; B is then usable by the callback and webhook; a late webhook for the superseded A no longer resolves the user.master(P2). The workflow's push trigger now matches the repository's default branch.Version 0.3.2 with changelog and upgrade notice.
Tests
tests/test-verify-endpoint.phpexercises the real handler, session creation, webhook and shortcode throughwp-stubs.php: forged completion, own session with a Declined decision, another user's approved session, unreachable decision endpoint, genuine approval, malformed id, cancellation, UniLink refusal and gate rendering, guest order session completed after login, order key with a foreign session, creation-time binding followed by a simple-signature webhook, a decision without metadata, revocation of the approved session after a second one is started, callback and webhook on a pending session, and the pending-binding cap.00f397e:33 passed, 12 failed(the review regressions).ad5f11f:test-verify-endpoint.php45 passed, 0 failed.test-verify-endpoint.php62 passed, 0 failed;test-webhook-signature.php53 passed, 0 failed;test-shortcode-assets.php15 passed, 0 failed, each with exit 0, on PHP 8.4 locally and inphp:7.4-cli. The reviewer's round 2 scenario replayed againstad5f11fleavesApprovedand the gate open; against this head it storesDeclinedand locks the gate.php -lclean on every PHP file;node --checkclean ondidit-verify.js.Visual proof
Unchanged since
ad5f11f(round 2 touched no UI).[didit_gate]rendered from the plugin's real output withassets/css/didit-verify.cssand the button's inline CSS (Playwright, Chromium). No horizontal overflow at either width.Light, 900px:
Dark page background, 900px:
Mobile, 390px:
Residual risk
_didit_status = Approvedwritten before this fix; the release notes ask operators to review users whose stored session id has no matching Didit session.[didit_gate]must switch to API mode after updating; that gating was never able to confirm a result.🤖 Generated with Claude Code