Skip to content

Closes #41 Implemented SSRF prevention - #58

Merged
barry01-hash merged 16 commits into
Eduvault-stellar:mainfrom
p3ris0n:feat/prevent-ssrf
Jul 22, 2026
Merged

Closes #41 Implemented SSRF prevention#58
barry01-hash merged 16 commits into
Eduvault-stellar:mainfrom
p3ris0n:feat/prevent-ssrf

Conversation

@p3ris0n

@p3ris0n p3ris0n commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Harden Webhook Registration and Delivery

Closes #41

Overview

This PR implements critical operational safety, security, and observability features for outbound webhook delivery across the EduVault marketplace. Previously, webhooks were sent in-memory without validation of the destination URL or payload signatures. This PR hardens the egress path to strictly prevent SSRF and DNS rebinding attacks, introduces versioned HMAC payload signatures with overlapping key rotation, and implements a resilient background delivery system with exponential backoff and dead-lettering.

Key Changes

1. SSRF Protection and Strict Egress Controls

File: src/lib/webhooks/dispatcher.js

  • Replaced naive fetch wrapper with a highly restrictive custom dispatcher.js.
  • Enforces https:// protocol and standard secure ports (443, 8443).
  • DNS Resolution & Filtering: Manually resolves domain names via DNS and blocks requests to private IPv4 networks (e.g., 10.x, 192.168.x), IPv6 local scopes, loopbacks, and reserved subnets to thwart SSRF.
  • DNS Rebinding Prevention: Directs the HTTP connection directly to the verified IP address, passing the original hostname as the Host and SNI headers to guarantee the target IP doesn't shift between resolution and connection (TOCTOU attacks).
  • Implements strict 5-second timeouts and a 1MB response size limit.
  • Securely limits and validates redirects.

2. Versioned HMAC Signatures & Rotating Secrets

File: src/lib/webhooks/signature.js

  • All webhook payloads are now wrapped in a standardized schema containing a stable UUID event id, type, created timestamp, and data.
  • Generates a v1 SHA-256 HMAC signature using the raw payload and a timestamp, appended to the Eduvault-Signature header (e.g., t=...,v1=...).
  • Overlapping Key Rotation: Supports multiple active signing secrets simultaneously, allowing users to safely rotate secrets without dropping events.
  • Replay Protection: Verification checks include a 5-minute clock drift tolerance to prevent replay attacks, comparing signatures with crypto.timingSafeEqual to avoid timing attacks.

3. Resilient Background Delivery & Retries

Files: src/lib/backend/schemaContracts.js, src/lib/webhooks/sender.js, src/lib/backend/webhookWorker.js

  • Schema: Added webhooks and webhook_deliveries collections to the database.
  • Webhook events are no longer sent synchronously. They are enqueued into the webhook_deliveries collection as pending.
  • Created webhookWorker.js to poll and process pending deliveries.
  • Bounded Backoff & Jitter: Automatically retries failed HTTP requests utilizing exponential backoff (e.g., 2s, 4s, 8s) combined with jitter.
  • Dead-Lettering: Classifies a delivery as dead_letter after 5 failed attempts.

4. User-Facing Webhook Management APIs

Files: src/app/api/webhooks/...

  • Built full CRUD capabilities for webhook registration and endpoint observability.
  • GET /api/webhooks: Lists registered endpoints with redacted secrets.
  • POST /api/webhooks: Registers a new endpoint, revealing the secret only once.
  • DELETE /api/webhooks/[id]: Soft deletes/disables a webhook.
  • POST /api/webhooks/[id]/rotate: Facilitates overlapping key rotation by expiring the old secret in 24 hours while generating a new primary secret.
  • GET /api/webhooks/[id]/deliveries: Inspect deliveries, attempts, and error reasons for troubleshooting.
  • POST /api/webhooks/[id]/deliveries/[deliveryId]/replay: Manually replay an event without mutating the original payload.

Security Considerations

  • Comprehensive SSRF mitigations prevent the EduVault backend from querying internal network services, databases, or cloud metadata endpoints.
  • DNS Rebinding protection ensures DNS cannot shift mid-request.
  • Strong timing-safe HMAC cryptography guarantees data integrity and origin authenticity.
  • Secrets are securely persisted, redacted in GET endpoints, and rotated gracefully.

Testing

  • Tests implemented in tests/backend/webhooks.test.mjs.
  • Covers SSRF IP filtering (127.0.0.1, 169.254.x.x, ::1, ::ffff:127.0.0.1, etc.).
  • Covers signature validation, replay tolerances, and key rotation scenarios.
  • All webhook backend tests pass successfully.

Breaking Changes

  • The webhookUrls string array on the users collection is deprecated. A seamless JIT migration path was introduced in sender.js to migrate active legacy URLs into the new webhooks collection on-the-fly when events are broadcast.

Closes #<WEBHOOK_ISSUE_NUMBER>

@p3ris0n
p3ris0n force-pushed the feat/prevent-ssrf branch from cefeeac to 6143515 Compare July 21, 2026 20:49
@barry01-hash

Copy link
Copy Markdown
Contributor

Very nice contribution

@barry01-hash
barry01-hash merged commit f6f9e22 into Eduvault-stellar:main Jul 22, 2026
5 of 6 checks passed
barry01-hash pushed a commit that referenced this pull request Jul 25, 2026
Prerequisite for #101: required status checks cannot be turned on while the
backend suite is red, and it was red in 8 places.

Five of those were one bug. `getStorage()` resolved AsyncLocalStorage via
`eval('require("async_hooks")')`, which only exists in CommonJS and webpack
bundles. Under native ESM `require` throws ReferenceError, so the catch
dropped the server into the browser fallback: a no-op store whose `getStore()`
always returns null. Every ESM entry point — the indexer runner, the workflow
worker, anything under `node --test` — was therefore losing correlation IDs
and traceparents silently while still appearing to work. Resolution now tries
`process.getBuiltinModule("node:async_hooks")` first, which is synchronous,
invisible to webpack's static analysis, and works under both module systems.
The import has to stay dynamic because this module is reachable from client
bundles via checkoutService -> CheckoutInvoice -> CartDrawer.

Degrading to no-op storage on a server is now recorded rather than assumed
harmless, and exposed as `isContextDegraded()` so the condition is
observable instead of silent.

`validateGatewayUrl` returned null instead of throwing for empty input,
because `normalizeExternalUrl` returns null rather than throwing there. A
missing gateway URL passed straight through the validator and reached callers
as null where they expect a verified string.

The remaining two were stale tests, not product bugs. The rate limiter became
Redis-backed and async in #58; its test still called it synchronously and
compared `undefined` to `true`. Rewritten against the current contract,
covering the security-relevant half that is testable without Redis: whether an
outage fails open or closed. The material validation fixture used
`example.com`, which the SSRF hardening in #58 correctly rejects; switched to
an allowlisted host.

Backend suite: 416 pass, 0 fail (was 405/8).
Obiajulu-gif added a commit to Obiajulu-gif/eduvault that referenced this pull request Jul 30, 2026
…d test environment isolation (Eduvault-stellar#103) (#1)

* feat: authenticated streaming delivery without exposing permanent IPFS locations (Eduvault-stellar#3)

Implements a three-layer authenticated delivery system:

1. Token Service (src/lib/delivery/token.js)
   - HMAC-SHA256 signed tokens bound to buyer + material + expiry
   - Optional single-use nonce for replay protection
   - Optional IP binding for additional security
   - Timing-safe signature verification

2. Streaming Proxy (src/lib/delivery/stream.js)
   - ReadableStream-based proxy with backpressure support
   - RFC 7233 range requests for partial content/resume
   - Upstream timeout handling (no corrupt partial responses)
   - Client disconnect detection via AbortSignal
   - 5GB file size limit enforcement
   - Private gateway URL support

3. Audit Service (src/lib/delivery/audit.js)
   - Safe-field filtering (no secrets logged)
   - MongoDB persistence + stdout logging
   - Queryable audit records with material/buyer filters
   - Delivery statistics aggregation

API endpoints:
- POST /api/delivery/token - Issue short-lived delivery token
- GET /api/delivery/stream - Authenticated streaming proxy
- GET /api/download - Refactored to return token instead of CID

Security improvements:
- CIDs never exposed to clients
- Cache-Control: private, no-store on all delivery responses
- Tokens expire after 15 minutes (configurable, max 1 hour)
- Cross-user cache isolation
- Deployment guide for private IPFS gateway

Tests: 42 passing covering token issuance/verification, range requests,
file size validation, audit logging, cache isolation, multi-GB
simulation, client disconnect, and replay protection.

* Closes Eduvault-stellar#41 Implemented SSRF prevention

* chore: fix CI Lint & Build Backend

* chore: resolve CI fixes

* fix: resolve CI workflow issues across lint, tests, and build

Fixes applied:
- Add missing closing brace in checkout initiate route
- Remove duplicate return statements in upload route from bad merge
- Fix missing closing brace in horizonClient getDynamicBaseFee
- Fix duplicate imports/variables and malformed JSX in UploadWizard
- Remove useEffect calling setState synchronously in CreatorProfileSettings
- Reorder validateSplits before useEffect in PayoutSplits
- Fix apostrophe escaping in LearningProgress and verification page
- Fix Horizon.Server mock to use function constructor for vitest
- Replace node:crypto with browser-safe random generator in context.js
- Lazy-load AsyncLocalStorage to avoid webpack bundling issues
- Add @opentelemetry/api mock alias for vitest
- Add webpack fallbacks for node modules in next.config.mjs

* fix(accessibility): meet WCAG 2.2 AA across critical wallet and marketplace flows

- Add skip-to-content link and proper landmark regions
- Fix focus trap with restoration, visible focus, and no traps
- Add correct roles, names, relationships, and live announcements to toasts
- Add keyboard operability to modals, drawers, and dropdowns with Escape behavior
- Add aria-labels, aria-expanded, aria-selected to interactive elements
- Add reduced-motion and forced-colors support in globals.css
- Add drag-drop keyboard accessibility and upload progress announcements
- Add automated axe accessibility tests for critical components

Closes Eduvault-stellar#47

* fix: resolve failing tests

* chore: fix CI soroban check

* chore: reimplemented CI fixes

* fix: make indexer projections lossless

* feat: add resumable upload sessions

* chore: Fix Lint and Build (Back & Frontend) CI Issues

* chore: fixed backend telemetry CI fail

* chore: Fix Backend: Lint and Build CI2

* Fixed: Backend CI

* feat(indexer): decode versioned Soroban events

* fix(auth): bind access checks to session wallet

* fix(purchases): verify finalized on-chain settlement

* test(soroban): exercise purchase flow with real SAC

* feat(indexer): add resumable ledger reconciliation

* feat(security): use distributed atomic rate limits

* feat(security): quarantine uploads before publication

* feat: add observability audit logging for issue 63

* fix: bind checkout intents to payment terms (Eduvault-stellar#35)

* fix: address ci regressions

* fix: stabilize ci test harness

* fix: clear remaining ci failures

* feat(accounting): immutable double-entry ledger with Stellar reconciliation

Introduce an append-only accounting subsystem under src/lib/ledger that proves
money-related views reconcile against Stellar settlement and the operational
collections, so reprocessing events or changing business rules can no longer
silently alter balances, duplicate revenue, or produce creator statements that
disagree with the chain.

Core:
- money.js: asset-aware integer stroop amounts (BigInt), decimal parsing with
  precision limits; floating-point arithmetic is prohibited.
- accounts.js / journal.js: chart of accounts, balanced-per-asset validation,
  immutable frozen transactions, idempotency keyed by
  (network, txHash, opIndex, eventType), and reversal/adjustment construction.
- postingRules.js: versioned posting rules for purchases (net-fee and
  platform-funded-discount) and proportional refunds; rule version stored on
  each transaction so policy changes never re-interpret history.
- balances.js: creator earnings, platform revenue, refund liabilities, and
  available/pending balances derived from the ledger, not mutable counters.

Operations:
- reconciliation.js: match journal vs finalized Stellar operations and the
  purchase collection, classify discrepancies, and return idempotent replay
  candidates.
- backfill.js: checkpointed, dry-runnable backfill of legacy purchases with
  explicit ambiguous-row handling.
- period.js: reproducible period-close snapshots (hashed) with late-event
  detection so closed periods cannot silently change.
- repository/{memory,mongo}.js: append-only repositories with a unique
  idempotency index; service.js exposes record{Purchase,Refund,Reversal}.

Tests (39, all passing): property-based invariants via fast-check (balancing,
precision, value conservation across arbitrary partial refunds) plus scenario
coverage for duplicates/out-of-order, multi-asset, reversal chains, concurrent
posting, reconciliation classes, and backfill restart.

Docs: docs/accounting/LEDGER.md.

Closes Eduvault-stellar#28

* chore: fix conflicts

* critical-wallet-and-marketplace-flows

* chore: fixing backend CI2

* chore: fixing backend CI2

* ci: add unified quality workflow

* Harden browser security and wallet signing

* fix(indexer): harden Stellar event indexer for replay and resume (Eduvault-stellar#83)

The repository already had an indexer (Eduvault-stellar#6, Eduvault-stellar#7, Eduvault-stellar#122, #384); what it lacked
was working failure handling. This is a hardening pass over the existing
`src/lib/indexer/` rather than a rewrite.

indexer_ledger_lag was a constant. The expression
`Math.max(0, latestLedger - previousLedger === 0 ? 0 : 0)` evaluates to 0 on
both branches of the ternary, so IndexerLedgerLagHigh (critical, fires above
50) could never fire. Lag is now the real distance from the chain tip, which
needs no extra RPC call: getEvents already returns `latestLedger`, and that
is now tracked separately from the highest ledger actually applied. A short
page reports 0, so a quiet chain does not alert.

Recovery could grant entitlements for a material that does not exist.
`operationToEvent` never set materialId but still emitted purchase.completed,
so the projection upserted purchases and entitlement_cache on
`{materialId: undefined, buyerAddress}`. The purchases unique index has a
`$type: "string"` partial filter and skipped these, but
entitlements_buyer_material_unique does not, so every recovered payment for a
buyer collapsed onto one null-material row marked active. Recovery now
resolves materialId from the owning purchase and reports unmatchable payments
as orphans instead of writing them. Two related fixes: only inbound payments
are considered (forAccount returns both directions, so an outbound refund
became a purchase with the platform account as buyer), and the "already
indexed?" query now checks both `transactionHash` (written by the checkout
path) and `chainTxHash` (written by the indexer) rather than only the latter,
which had made every app-path purchase look missing.

Dead-letter accounting ran backwards. The retry increment sat on the
`skipped` branch, which means the event had already applied successfully, and
drove healthy replays toward the terminal `failed` state while the cleanup
delete was gated on `!skipped` and never ran. Both outcomes are now treated
as successes that retire the row. Unidentifiable events get a content-hashed
dead-letter id instead of Math.random(), which had written a fresh row per
attempt so retryCount never reached the ceiling. reprocessDeadLetters no
longer sweeps terminal rows by default and records each attempt.

The indexer did not run locally as documented. Each event applies inside a
transaction, which Mongo only permits on a replica set, but docker-compose
ran a standalone mongod, so the first event threw. Compose now starts a
single-node replica set, and the indexer detects an unsupported-transaction
error once per process and falls back to non-transactional writes rather than
dead-lettering everything on a developer machine. `npm run indexer:stellar`
is now an actual service with a poll loop, exponential backoff on RPC
failure, and SIGINT/SIGTERM shutdown; `:once` preserves the previous
single-batch behaviour for cron and rebuilds.

Tests could not observe any of the above. The existing double keys documents
as `_id || materialId:buyerAddress` and models no constraints, so E11000 is
unrepresentable and a mid-batch crash unsimulatable. Adds a fake Mongo that
enforces the real unique indexes from schemaContracts and injects write
faults, plus coverage for checkpoint resume and event replay (AC4). Both the
lag and dead-letter fixes were verified to fail against the old code.

Not addressed here: chain reorganisation handling. It depends on whether
entitlement_cache is authoritative or derived, and `createEntitlement`
currently writes it straight from the purchase API, which makes it both.
Raised on the issue.

* feat: add Stellar/Soroban entitlement verification & gated download access

- Implement buildHasEntitlementXdr for Soroban simulation checks
- Ensure RPC failures fail open/gracefully to cached/DB entitlements
- Add integration test suite for Soroban entitlement gate

Closes Eduvault-stellar#85

* chore: update package-lock.json

* feat: GDPR-compliant data export and staged account deletion

- Add personal data inventory with retention rules (retentionPolicy.js)
- Build authenticated data export service with versioned JSON manifest,
  48h expiry, and capability token for private download URL
- Implement 6-state deletion machine: pending_reauth → cooling_off →
  executing → completed (with cancel and fail/retry paths)
- Add obligation checker blocking deletion on in-flight purchases or
  unsettled ledger credits
- Anonymize retained financial/audit records (purchases, entitlement_cache,
  materials, reviews) preserving referential integrity
- Execute deletion in 7 idempotent steps with per-step progress recording
- Unpin Pinata/IPFS objects (avatar always; materials only if no active buyers)
- Add Privacy & Data section to /dashboard/settings with export and
  deletion panels covering scope, timing, and irreversible effects
- 45 backend tests across 6 suites (all pass)
- Document retention rules, backup/analytics coverage in
  docs/privacy-data-retention.md

API routes:
  POST/GET /api/privacy/export
  GET      /api/privacy/export/download
  POST/GET /api/privacy/deletion
  POST     /api/privacy/deletion/cancel

* Closes Eduvault-stellar#84 Moving from ERC-721 prototype to native Soroban

* fix(auth): harden profile/materials API auth, wallet normalization, and validation

* chore: fixing failing Lint CI

* fix(observability): restore context propagation broken under native ESM

Prerequisite for Eduvault-stellar#101: required status checks cannot be turned on while the
backend suite is red, and it was red in 8 places.

Five of those were one bug. `getStorage()` resolved AsyncLocalStorage via
`eval('require("async_hooks")')`, which only exists in CommonJS and webpack
bundles. Under native ESM `require` throws ReferenceError, so the catch
dropped the server into the browser fallback: a no-op store whose `getStore()`
always returns null. Every ESM entry point — the indexer runner, the workflow
worker, anything under `node --test` — was therefore losing correlation IDs
and traceparents silently while still appearing to work. Resolution now tries
`process.getBuiltinModule("node:async_hooks")` first, which is synchronous,
invisible to webpack's static analysis, and works under both module systems.
The import has to stay dynamic because this module is reachable from client
bundles via checkoutService -> CheckoutInvoice -> CartDrawer.

Degrading to no-op storage on a server is now recorded rather than assumed
harmless, and exposed as `isContextDegraded()` so the condition is
observable instead of silent.

`validateGatewayUrl` returned null instead of throwing for empty input,
because `normalizeExternalUrl` returns null rather than throwing there. A
missing gateway URL passed straight through the validator and reached callers
as null where they expect a verified string.

The remaining two were stale tests, not product bugs. The rate limiter became
Redis-backed and async in Eduvault-stellar#58; its test still called it synchronously and
compared `undefined` to `true`. Rewritten against the current contract,
covering the security-relevant half that is testable without Redis: whether an
outage fails open or closed. The material validation fixture used
`example.com`, which the SSRF hardening in Eduvault-stellar#58 correctly rejects; switched to
an allowlisted host.

Backend suite: 416 pass, 0 fail (was 405/8).

* chore: fixing failing Lint CI

* chore: fixing failing Lint CI

* ci(Eduvault-stellar#101): establish enforceable quality gates and preview smoke tests

Builds out the CI gates issue Eduvault-stellar#101 asks for. The issue's premise — that there
is no workflow enforcing quality — is out of date (ci.yml landed the day
before it was filed), so this hardens what exists rather than starting over,
and fixes the things that made the existing setup unenforceable.

Lockfile integrity. Every workflow ran `npm install`, which silently rewrites
package-lock.json rather than failing on drift — and the lockfile WAS drifted:
`npm ci` failed from a clean clone with 8 packages missing from the lock. CI
now runs `npm ci`, and `scripts/check-lockfile.mjs` fails the build on drift
or on a competing lockfile reappearing. Regenerated package-lock.json so it
satisfies package.json. Declared npm as the one supported package manager
(`packageManager` field) and removed the stale bun.lock and pnpm-lock.yaml,
both last touched months ago, which otherwise made the installed tree depend
on which tool you happened to run.

Least privilege and concurrency. Only ci.yml declared `permissions:` or
`concurrency:`; the other six inherited the repo default and could not cancel
superseded runs. All eight workflows now declare `contents: read` and a
concurrency group. The previous `cancel-in-progress: true` was unconditional
and would cancel runs on the default branch too; it is now guarded so main and
develop always complete and record a status. The scheduled backup never
cancels mid-run.

Duplicate triggers. ci.yml triggered on bare `push:` plus `pull_request:`, so
every in-repo PR ran the whole suite twice. Push is now scoped to main and
develop.

Migration and integration coverage. `test:migrations` and `test:integration`
both existed and neither ran in CI. A new migrations job runs the migrations
forward against a MongoDB service container; integration tests run in the main
job.

License policy. `scripts/check-licenses.mjs` fails on newly introduced
GPL/AGPL/SSPL/unlicensed production dependencies. Two pre-existing transitive
offenders (@lobstrco/signer-extension-api GPL-3.0, ua-parser-js AGPL-3.0) are
allowlisted with annotations and flagged for maintainer legal review, so the
gate does not block on state that predates it while still catching new ones.

Type-check. `npm run typecheck` runs checkJs but stays advisory (non-blocking):
the JSDoc-annotated JS produces ~3000 findings, almost all missing
annotations, so a blocking gate would wall off every merge. Kept visible to be
driven down in its own pass.

Preview smoke tests. scripts/smoke-preview.mjs plus preview-smoke.yml verify
the three required paths against a deployed preview — landing renders, the auth
boundary returns 401/403 unauthenticated, /api/health is live — triggered by
deployment_status or manually, using no secrets.

Also fixed 3 pre-existing lint errors: scripts/backup.mjs,
create-backup-manifest.mjs and restore.mjs each had a blank line before their
shebang, putting `#!` on line 2 where it is a parse error, so `npm run lint`
was already red on main.

docs/ci-and-quality-gates.md documents the gates, the fork-safety model, the
recommended required checks, and the emergency-bypass procedure.

* feat(materials): model and enforce the material lifecycle as an explicit state machine

Replaces the ad hoc "publish sets status" write path with a centralized
domain service (src/lib/materials/materialLifecycle.js) that governs every
material status change: draft -> published -> closed/canceled.

- Defines the allowed transition graph and rejects any other (from, to)
  pair with a typed, non-mutating 409.
- Centralizes transition preconditions (publishing checklist, caller
  ownership/role, no confirmed purchases before canceling a published
  listing) so every write path shares the same rules.
- Generic material updates (PUT /api/materials) now explicitly reject a
  status field — status can only change through the publish/close/cancel
  routes.
- Persists an immutable material_status_history record (actor, previous
  status, next status, reason, timestamp) per transition.
- Guards every transition with findOneAndUpdate against the expected
  current status so concurrent requests can't double-apply a transition;
  proven with a race test against a real in-memory store.
- Repeating the current status is idempotent (no-op, no history write).
- Backfills status on legacy materials via migration 004 without changing
  their effective status.
- Updates status badges and the creator dashboard to show only the
  actions valid from the material's current state.
- Adds docs/material-lifecycle.md with the state diagram and transition
  table.

Closes Eduvault-stellar#111

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Closes Eduvault-stellar#108 Implemented a durable Stellar event indexer for escrow projections

* server-side-authorization

* server-side-authorization

* server-side-authorization

* fix(test): implement integration test suite fixes for issue Eduvault-stellar#103

---------

Co-authored-by: gelluisaac <isaacgellu6@gmail.com>
Co-authored-by: Promise Raji <isaacsamson88@gmail.com>
Co-authored-by: Netty-kun <182667336+Netty-kun@users.noreply.github.com>
Co-authored-by: bigdella <bigdella@users.noreply.github.com>
Co-authored-by: prissca <nwoguvictoriachiamaka@gmail.com>
Co-authored-by: Obiajulu-gif <okoyeemmanuel998@gmail.com>
Co-authored-by: Henry Ebubechukwu <henryebube62@gmail.com>
Co-authored-by: Pri_ss_ca <136065253+prissca@users.noreply.github.com>
Co-authored-by: Ayinkx <142127428+Ayinkx@users.noreply.github.com>
Co-authored-by: Victor Edeh <edehvictor715@gmail.com>
Co-authored-by: spagero763 <afolabiayomide870@gmail.com>
Co-authored-by: Bamzy123 <stephenomotos@gmail.com>
Co-authored-by: Segun Akinola <105325916+Primex-hub@users.noreply.github.com>
Co-authored-by: 0takuc0mrade <junep059@gmail.com>
Co-authored-by: storm-beyndtech <khameleonstorm@gmail.com>
Co-authored-by: soomtochukwu <onwuajuesesomtochukwu@gmail.com>
Co-authored-by: JClark011 <joelclar700@gmail.com>
Co-authored-by: DevNetlife <DevNetlife@users.noreply.github.com>
Co-authored-by: christopherdominic <chriseze0@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.

[Webhooks][Security] Prevent SSRF and add replay-safe signed delivery

2 participants