GHSA-7c2g-p23p-4jg3 CVE-2026-33677 GO-2026-4846
Vikjuna: Webhook BasicAuth Credentials Exposed to Read-Only Project Collaborators via API
- Severity
- 6.5 / 10
- Medium · CVSS 3.1
- Exploit chance (EPSS)
- 0.30%
- Chance of exploitation in the next 30 days. Higher than 23.03% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
- Fix
2.2.1- Upgrade code.vikunja.io/api to this version or later
Description
Summary
The GET /api/v1/projects/:project/webhooks endpoint returns webhook BasicAuth credentials (basic_auth_user and basic_auth_password) in plaintext to any user with read access to the project. While the existing code correctly masks the HMAC secret field, the BasicAuth fields added in a later migration were not given the same treatment. This allows read-only collaborators to steal credentials intended for authenticating against external webhook receivers.
Details
When listing project webhooks, the ReadAll method in pkg/models/webhooks.go (line 203) only requires project read access:
// pkg/models/webhooks.go:203-244
func (w *Webhook) ReadAll(s *xorm.Session, a web.Auth, _ string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
p := &Project{ID: w.ProjectID}
can, _, err := p.CanRead(s, a) // Only requires read permission
if err != nil {
return nil, 0, 0, err
}
if !can {
return nil, 0, 0, ErrGenericForbidden{}
}
// ... fetches webhooks from DB ...
for _, webhook := range ws {
webhook.Secret = "" // HMAC secret is masked
// BasicAuthUser and BasicAuthPassword are NOT masked
if createdBy, has := users[webhook.CreatedByID]; has {
webhook.CreatedBy = createdBy
}
}
return ws, len(ws), total, err
}
The Webhook struct defines both fields with JSON serialization tags, so they are included in API responses:
// pkg/models/webhooks.go:63-64
BasicAuthUser string `xorm:"null" json:"basic_auth_user"`
BasicAuthPassword string `xorm:"null" json:"basic_auth_password"`
The BasicAuth fields were added in migration 20260123000717 ("Add basic auth to webhooks"), but the credential masking logic at line 238 was not updated to include these new fields.
The same issue exists in the user webhook listing at pkg/routes/api/v1/user_webhooks.go:65, where Secret is masked but BasicAuth fields are not. This is lower impact since users only see their own webhooks.
PoC
- As User A (project admin), create a project and a webhook with BasicAuth credentials:
# Create a webhook with BasicAuth on project 1
curl -X PUT "http://localhost:3456/api/v1/projects/1/webhooks" \
-H "Authorization: Bearer $TOKEN_A" \
-H "Content-Type: application/json" \
-d '{
"target_url": "https://external-service.example.com/hook",
"events": ["task.created"],
"secret": "my-hmac-secret",
"basic_auth_user": "service-account",
"basic_auth_password": "S3cretP@ssw0rd!"
}'
- As User B (read-only collaborator on the same project), list webhooks:
curl -s "http://localhost:3456/api/v1/projects/1/webhooks" \
-H "Authorization: Bearer $TOKEN_B" | jq '.[0] | {secret, basic_auth_user, basic_auth_password}'
- Expected output (secret is masked, but BasicAuth is leaked):
{
"secret": "",
"basic_auth_user": "service-account",
"basic_auth_password": "S3cretP@ssw0rd!"
}
Impact
- Credential theft: Any user with read-only access to a project can steal BasicAuth credentials configured on that project's webhooks. These credentials may grant access to external services (CI/CD systems, notification endpoints, third-party APIs).
- Lateral movement: Stolen credentials could be reused to authenticate against external systems that the webhook receiver protects.
- Broad exposure surface: Credentials are exposed to all project readers, including users granted access through team shares and link shares (with read+ permission level).
Recommended Fix
In pkg/models/webhooks.go, add masking for BasicAuth fields alongside the existing Secret masking (around line 237):
for _, webhook := range ws {
webhook.Secret = ""
webhook.BasicAuthUser = ""
webhook.BasicAuthPassword = ""
if createdBy, has := users[webhook.CreatedByID]; has {
webhook.CreatedBy = createdBy
}
}
Apply the same fix in pkg/routes/api/v1/user_webhooks.go (around line 64):
for _, w := range ws {
w.Secret = ""
w.BasicAuthUser = ""
w.BasicAuthPassword = ""
if createdBy, has := users[w.CreatedByID]; has {
w.CreatedBy = createdBy
}
}
Affected versions
Go
| Package | Vulnerable | Fixed in |
|---|---|---|
| code.vikunja.io/api | < 2.2.1 |
2.2.1 |
| code.vikunja.io/api | all versions |
No fix yet |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
code.vikunja.io/api Go → 2.2.1
- go
go get code.vikunja.io/api@v2.2.1 && go mod tidy
Severity breakdown
CVSS 3.1 6.5
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- Low
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Weakness
CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
How to prevent it
- Architecture and Design: Compartmentalize the system to have "safe" areas where trust boundaries can be unambiguously drawn. Do not allow sensitive data to go outside of the trust boundary and always be careful when interfacing with a compartment outside of the safe area. Ensure that appropriate compartmentalization is built into the system design, and the compartmentalization allows for and reinforces privilege separation functionality. Architects and designers should rely on the principle of least privilege to decide the appropriate time to use privileges and the time to drop privileges.
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-m687-p538-r5hp Vikunja: Permissive Cross-domain Security Policy trusts every localhost origin which should not be trusted | Medium | Also affects code.vikunja.io/api |
| GHSA-fmmf-xq98-g327 Vikunja: Write-level project members can delete admin-tier link shares through an unloaded permission check | Medium | Also affects code.vikunja.io/api |
| GHSA-pjr3-86v4-5p7w Vikunja: Explicit lower-permission share on a sub-project is silently overridden by an inherited parent permission (broken access control / privilege-management regression in v2.6.0) | Medium | Also affects code.vikunja.io/api |
| GHSA-hjx8-qv73-f7cm Vikunja: Webhooks and link shares survive every revocation path, so a removed collaborator keeps a live feed | High | Also affects code.vikunja.io/api |
| GHSA-fprf-r6rv-xg99 Vikunja: Saved filter creation with an empty filter string recalculates task positions across all tenants | Medium | Also affects code.vikunja.io/api |
| GHSA-4hv6-xc92-j86g Vikunja: WebSocket authentication ignores server-side session state, so revoked sessions keep receiving live pushes | High | Also affects code.vikunja.io/api |
| GHSA-w2ch-4xgr-22ww Vikunja: Task relation deletion does not check read access to the other task, allowing cross-project relation removal | Low | Also affects code.vikunja.io/api |
| GHSA-8wvg-r2j4-3737 Vikunja: Assignee email addresses disclosed to read-only project members via the task assignees endpoint | Medium | Also affects code.vikunja.io/api |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor