Repository navigation
fix(api): PostTemplatesTags queries database and begins transaction before team ownership check (tag enumeration oracle) #3573
Description
Problem
In POST /templates/tags (packages/api/internal/handlers/template_tags.go), the control plane handler initiates a database transaction via a.sqlcDB.WithTx(ctx) and executes client.GetTemplateWithBuildByTag before validating whether the authenticated user's team owns the requested template (if aliasInfo.TeamID != team.ID).
This ordering defect causes two distinct issues:
-
Tag Enumeration / Information Disclosure Oracle:
When a user sendsPOST /templates/tagstargeting a template owned by a different team:- If the requested
tagdoes not exist on that template:GetTemplateWithBuildByTagreturnssql.ErrNoRows, which triggersErrTemplateNotFound, responding with404 Not Found("Template '<target>' with tag '<tag>' not found"). - If the requested
tagexists on that template: the query succeeds, and only afterwards at line 115 isaliasInfo.TeamID != team.IDchecked, responding with403 Forbidden("You don't have access to sandbox template '%s'").
An unauthorized tenant can therefore determine whether arbitrary private build tags (e.g.,staging,v2.0.0-rc1,hotfix) exist on other teams' templates by probing the endpoint and observing the difference between HTTP404and HTTP403.
- If the requested
-
Unnecessary Database Load and Transaction Allocation:
Every unauthorized or invalid tag request against another tenant's template allocates a Postgres transaction connection from the pool (WithTx), acquires read locks, executes queries, and then rolls back upon failure.
Root Cause
In packages/api/internal/handlers/template_tags.go:L75-L122, tag resolution and database transaction creation precede the team ownership authorization check:
// Current flow in template_tags.go:
aliasInfo, err := a.templateCache.ResolveAlias(ctx, templateID)
if err != nil { ... }
// BUG: Transaction started and DB queried BEFORE checking aliasInfo.TeamID == team.ID
txErr := a.sqlcDB.WithTx(ctx).Exec(func(queries *queries.Queries) error {
build, err := queries.GetTemplateWithBuildByTag(ctx, ...)
if err != nil {
return ErrTemplateNotFound // Returns 404 to unauthorized caller if tag missing
}
...
if aliasInfo.TeamID != team.ID {
return a.sendAPIStoreError(c, http.StatusForbidden, ...) // Returns 403 only if tag exists
}
})In contrast, peer handlers such as DeleteTemplatesTags (template_tags.go:L139) and GetTemplatesTemplateIDTags (template_tags.go:L21) enforce if aliasInfo.TeamID != team.ID immediately after alias resolution:
| Handler | Authorization Check Timing | Tag Enumeration Vulnerable? |
|---|---|---|
GET /templates/{templateID}/tags |
Immediate after ResolveAlias |
No |
DELETE /templates/tags |
Immediate after ResolveAlias |
No |
POST /templates/tags (Current) |
Deferred after GetTemplateWithBuildByTag DB query |
Yes (404 vs 403 Oracle) |
POST /templates/tags (Expected) |
Immediate after ResolveAlias |
No (403 Forbidden) |
Reproduction Steps
- Create a template
template-Aunderteam-1with tagv1.0.0. - Authenticate as an unrelated user
team-2. - Send
POST /templates/tagstargetingtemplate-Awithtag: "v9.9.9"(non-existent).- Observed:
HTTP 404 Not Found({"code": 404, "message": "Template 'template-A' with tag 'v9.9.9' not found"})
- Observed:
- Send
POST /templates/tagstargetingtemplate-Awithtag: "v1.0.0"(existing tag).- Observed:
HTTP 403 Forbidden({"code": 403, "message": "You don't have access to sandbox template 'template-A'"})
- Observed:
- Expected: Both requests must return
HTTP 403 Forbiddenwithout leaking metadata about tag existence.
Technical Context
- File affected:
packages/api/internal/handlers/template_tags.go - Subsystem: Control Plane API / Templates & Tags
- Impact: Medium (Security/Privacy: cross-tenant metadata disclosure & unnecessary database transaction overhead)
Proposed Changes
| # | Change | File(s) Affected | Complexity |
|---|---|---|---|
| 1 | Move if aliasInfo.TeamID != team.ID check immediately after ResolveAlias before WithTx |
packages/api/internal/handlers/template_tags.go |
Trivial |
| 2 | Add unit test TestPostTemplatesTags_RejectsOtherTeamTemplate verifying 403 Forbidden |
packages/api/internal/handlers/template_tags_test.go |
Low |
Activity
[-]fix(api): PostTemplatesTags queries template and begins transaction before team ownership check[/-][+]fix(api): PostTemplatesTags queries database and begins transaction before team ownership check (tag enumeration oracle)[/+]ryan-sana commentedon Oct 26, 2026
Resolved by the merged port of @chill-czar’s fix from #3574 in the canonical source repository. Template ownership is now checked before starting a transaction or resolving the source tag, with regression coverage for the early rejection.