[WEB-8103] fix: stop leaking webhook HMAC secret_key on reads - #9382
[WEB-8103] fix: stop leaking webhook HMAC secret_key on reads#9382mguptahub wants to merge 4 commits into
Conversation
…3rj) WebhookEndpoint list/retrieve/patch pass a fields= allowlist that excludes secret_key, but DynamicBaseSerializer.__init__ discards the caller allowlist (fields = self.expand). With WebhookSerializer using fields="__all__" and secret_key only in read_only_fields (read-only is still serialized), the HMAC signing secret leaked on every webhook read (GHSA-83rj-4282-x39v; admin-only). Rather than secret_key = CharField(write_only=True) — which would let a client inject their own secret on create/patch and break the intended one-time reveal — hide it by default and reveal only where intended: - WebhookSerializer.to_representation drops secret_key unless the show_secret_key context flag is set (secure by default). secret_key stays server-generated (default=generate_token) and non-writable. - POST create and WebhookSecretRegenerateEndpoint pass show_secret_key so the secret is still returned once for the caller to configure their receiver; list/retrieve/patch no longer emit it. Add contract regression tests (fail-before verified). Follow-up: the DynamicBaseSerializer.__init__ allowlist bug affects other serializers — tracked separately. Co-authored-by: Plane AI <noreply@plane.so>
|
Linked to Plane Work Item(s) This comment was auto-generated by Plane |
|
Warning Review limit reached
Next review available in: 45 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughWebhookSerializer now hides ChangesWebhook secret_key scoping
Estimated code review effort: 2 (Simple) | ~12 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant WebhookEndpoint
participant WebhookSerializer
Client->>WebhookEndpoint: POST create or regenerate webhook
WebhookEndpoint->>WebhookSerializer: instantiate with show_secret_key True
WebhookSerializer->>WebhookSerializer: check serialization context
WebhookSerializer-->>WebhookEndpoint: serialized data including secret_key
WebhookEndpoint-->>Client: response with secret_key
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
This PR fixes a security issue where webhook HMAC secret_key was being serialized and returned on webhook list/retrieve/patch responses due to a serializer field-allowlist being silently dropped. The change makes secret_key hidden by default in WebhookSerializer and only revealed when explicitly opted-in via serializer context, preserving the intended one-time reveal behavior on create/regenerate.
Changes:
- Hide
secret_keyby default inWebhookSerializer.to_representation, only returning it whencontext["show_secret_key"]is true. - Opt-in to secret reveal for webhook creation and secret regeneration endpoints via
show_secret_key=True. - Add contract regression tests ensuring list/retrieve/patch never leak
secret_key, while create/regenerate still reveal it.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| apps/api/plane/app/serializers/webhook.py | Filters secret_key out of serialized output by default; supports explicit reveal via serializer context flag. |
| apps/api/plane/app/views/webhook/base.py | Passes show_secret_key=True only on create and secret regeneration responses. |
| apps/api/plane/tests/contract/app/test_webhook_secret_key_scope_app.py | Adds regression tests covering both non-leak and reveal-once cases. |
sriramveeraghanta
left a comment
There was a problem hiding this comment.
Solid fix — verified the claims against the codebase before commenting:
DynamicBaseSerializer.__init__does discard the caller'sfields=(apps/api/plane/app/serializers/base.py:16-18), andWebhookSerializerhas no consumer outsideapp/views/webhook/base.py(the one test usage never serializes), so the blast-radius statement holds.- The contract matches the web frontend:
apps/web/core/store/workspace/webhook.store.tsonly readssecret_keyoff the create (createWebhook) and regenerate (regenerateSecretKey) responses — list/retrieve/patch never used it. No client breakage expected. - Test fixtures/routes/markers all line up (
session_client+workspacegive a workspace ADMIN;contractmarker is registered inapps/api/pytest.ini).
Three small, non-blocking suggestions inline.
…resentation Per review (@sriramveeraghanta): - Patch the network boundary (validate_url) instead of the whole private _validate_webhook_url method, so the domain/schema checks still run and the test survives a rename of the private method. - Add an assertion that an explicit fields=("secret_key",) request still hides the key, pinning to_representation as the enforcement point so a future DynamicBaseSerializer._filter_fields fix can't silently re-open the leak. Co-authored-by: Plane AI <noreply@plane.so>
|
Thanks @sriramveeraghanta — addressed in
On your first point: agreed, and to spell it out — |
The previous comment named only DynamicBaseSerializer.__init__ discarding the caller's fields=, which is half the root cause. _filter_fields never removes anything either: it builds `allowed` purely to attach expansion serializers for names not already on the serializer, then returns self.fields unfiltered (serializers/base.py:45-119). So the fields= kwargs in views/webhook/base.py are no-ops on two independent levels. Documented so a future `fields = fields or self.expand` fix isn't assumed to re-activate the allowlists for confidentiality — _filter_fields has to be made restrictive first. The show_secret_key context flag remains the sole enforcement point. Addresses @sriramveeraghanta's review on #9382. 6/6 contract tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
React Doctor found 29 new issues in 27 files · 1 error & 28 warnings · score 72 / 100 (Needs work) · 67 fixed · vs Errors
28 warnings
Reviewed by React Doctor for commit |
Explanations kept unchanged; only the IDs are removed. Co-authored-by: Plane AI <noreply@plane.so>
Fixes WEB-8103 (MED, admin-only).
Problem
WebhookEndpointlist/retrieve/patch pass afields=(...)allowlist that deliberately excludessecret_key, butDynamicBaseSerializer.__init__discards the caller allowlist (fields = self.expand). Combined withWebhookSerializerusingfields="__all__"andsecret_keyonly inread_only_fields(read-only fields are still serialized), the HMAC signingsecret_keywas returned on every webhook read. All handlers are@allow_permission([ADMIN], level="WORKSPACE"), so exposure is workspace-admin-only.Fix
The advisory suggests
secret_key = serializers.CharField(write_only=True), but that would (a) let a client inject their own secret on create/patch (a downgrade — the secret is server-generated viadefault=generate_token) and (b) break the intended one-time reveal. Instead:WebhookSerializer.to_representationdropssecret_keyunless ashow_secret_keycontext flag is set — secure by default.secret_keystays server-generated and non-writable.POST createandWebhookSecretRegenerateEndpointpassshow_secret_key=True, so the secret is still returned once for the caller to configure their receiver.WebhookSerializeris only used inapp/views/webhook/base.py(no external API / EE consumer), so the blast radius is contained.Tests
tests/contract/app/test_webhook_secret_key_scope_app.py— 6 tests: list/retrieve/patch do not leak, create + regenerate still reveal, and a serializer-level check of both branches of the flag. Fail-before verified (the four no-leak tests returnsecret_keybefore the fix).ruff+manage.py checkgreen.Follow-up
The root
DynamicBaseSerializer.__init__bug (silently discarding thefields=allowlist) also affects other serializers (workspace/project member over-exposure) — tracked as a separate audit.Summary by CodeRabbit
Bug Fixes
secret_keyhidden by default, including when explicitly requested.Tests