GHSA-p5vg-v7mj-f6q4: Go/github.com/frain-dev/convoy vulnerability

Published Sep 18, 2026
·
Updated

Summary frain-dev/convoy (all versions up to and including v26.6.2, no patch available) lets any authenticated caller who is authorized on at least one project read ANY OTHER project's "Source" record by ID via GET /api/v1/projects/{projectID}/sources/{sourceID} -- regardless of whether that Source actually belongs to the project named in the URL. The response includes the Source's full PubSub broker configuration in plaintext (AMQP/Kafka/SQS/Google credentials), with no redaction.

Details Handler.GetSource (api/handlers/source.go) resolves the caller's authorization against the {projectID} in the URL via retrieveProject(), then calls sources.Service.FindSourceByID(ctx, project.UID, sourceID) (internal/sources/impl.go). That function accepts a projectID parameter but never uses it:

func (s Service) FindSourceByID(ctx context.Context, projectID, id string) (datastore.Source, error) { row, err := s.repo.FetchSourceByID(ctx, common.StringToPgText(id)) ... }

The underlying SQL query (internal/sources/repo/queries.sql, fetchSourceByID) has no projectid predicate at all:

SELECT ... FROM convoy.sources AS s LEFT JOIN convoy.sourceverifiers sv ON s.sourceverifierid = sv.id WHERE s.id = $1 AND s.deletedat IS NULL

So the {projectID} in the URL only gates "is the caller authorized to view a project" -- it never re-validates that the fetched Source actually belongs to that project. LoadSourcesPaged (the list endpoint) does correctly scope by project; only the single-item GetSource lookup is affected. SourceResponse{datastore.Source} embeds the full database record with no redaction, so if the leaked Source is an AMQP/Kafka type, its pubsub..auth.password field (a live, plaintext broker credential) is returned verbatim.

Proof of Concept 1. As a test account, create "Victim Project" and an AMQP Source in it with a known broker password (verified against a real RabbitMQ broker, not just stored -- Convoy performs a live connectivity check before persisting an AMQP source). 2. As the same or a different authenticated caller, create a completely separate "Attacker Project". 3. Call GET /api/v1/projects/{attackerprojectid}/sources/{victimsourceid} -- i.e. a request whose URL and authorization check only ever resolve the Attacker project. 4. Verified in a Docker lab (convoy v26.6.2): the request returns 200 with the Victim project's full Source object, including pubsub.amqp.auth.password matching the secret set in step 1 exactly. The response's own projectid field is the Victim project's id, never the Attacker project id used in the URL/authorization check. 5. Negative control: an otherwise-identical request for a made-up source id under the same Attacker project returns 404, confirming this is a real, ID-specific hit and not a blanket-200 endpoint.

(Community Edition license-gates orglimit=1/userlimit=1, so this PoC used one account with two projects rather than two separate companies -- the vulnerable query performs no project-ownership check regardless of license tier, so a licensed multi-org deployment has the identical exposure between genuinely different tenants.)

<img width="1100" height="830" alt="1livepocevidence" src="https://github.com/user-attachments/assets/b2d18d3f-8e70-40ec-ad7f-7f0ac1fdebe0" />

Impact Any authenticated user or project-scoped API key holder who has legitimate access to at least one project on a Convoy instance can read any other project's Source configuration by ID, including plaintext third-party message-broker credentials (AMQP/Kafka/SQS/Google PubSub). In a multi-tenant deployment this is a direct cross-customer credential leak.

Fix Add a projectid predicate to the fetchSourceByID query (and audit other single-item repository lookups reachable via a {projectID} route for the same pattern), or re-check source.ProjectID == project.UID in the handler before returning the record.

Affected / Patched Affected: <= v26.6.2 (latest release at time of report). No patched version available.

Affected Software

1 affected componentFixes available
go/github.com/frain-dev/convoy<0.9.3-0.20260724092134-1cc67cd16fb1
0.9.3-0.20260724092134-1cc67cd16fb1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/frain-dev/convoy to a version that resolves this vulnerability.

    Fixed in 0.9.3-0.20260724092134-1cc67cd16fb1

Event History

Sep 18, 2026
Advisory Published
via GitHub·05:17 PM
Data Sourced
via GitHub·05:17 PM
DescriptionWeaknessAffected Software

Frequently Asked Questions

1

What level of access is required to exploit this issue?

An attacker must be authenticated and authorized on at least one project. They can then request Source records associated with other projects if they know or can supply the target Source ID.

2

What sensitive information may be exposed?

The affected response returns the full Source PubSub broker configuration without redaction. This can include credentials for AMQP, Kafka, SQS, and Google services.

3

Does authorization for the project in the request URL restrict the returned Source?

No. Authorization is checked for the project named in the URL, but the Source lookup does not constrain the query by project ID, so a Source from another project can be returned.

4

Is a vendor patch available?

No patch is available according to the advisory.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203