fix(api): PostTemplatesTags queries database and begins transaction before team ownership check (tag enumeration oracle)
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 1.6k
- Forks
- 438
- PR merge metrics
- No merged PRs in 30d
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 |
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/api/internal/handlers/template_tags.go at PostTemplatesTags and compare its authorization order with GetTemplatesTemplateIDTags and DeleteTemplatesTags. Then inspect or add the focused case in packages/api/internal/handlers/template_tags_test.go and run the relevant API tests. Done means unauthorized requests consistently return 403 without querying the tag or starting a transaction.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, postgres
- Domain
- authorization, backend-api-design, databases, security
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100