Threats affecting pkg:golang/github.com/go-vikunja/vikunja
60 threats
Most recently published first
| Severity | CVSS | Signals | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|---|
| Medium | 5.1 | EPSS 0.2% | CVE-2026-115507vikunja · Cross-site Scripting Medium5.1EPSS 0.2%CVE Database V5CVE-2026-115507 Vikunja versions 2.2.0 through 2.7.0 contain an HTML injection vulnerability in the CreateConversationalHeader function. Authenticated users can inject malicious HTML into notification emails by manipulating display names or task/project titles. This affects task-comment and task-mention notification emails sent to project members. The vulnerability was fixed in version 2.7.1. | CVE Database V5 | ||
| High | 7.2 | EPSS 0.1% | High7.2EPSS 0.1%CVE Database V5CVE-2026-111922 Vikunja versions from 2.2.0 up to but not including 2.7.0 have a permissive Cross-Origin Resource Sharing (CORS) policy that trusts all localhost and 127.0.0.1 ports with credentials. This misconfiguration allows an attacker controlling a page on any localhost port to send credentialed requests to the token refresh endpoint and obtain the bearer JWT token, potentially taking over the logged-in user's account. | CVE Database V5 | ||
| Medium | 5.3 | EPSS 0.2% | CVE-2026-111921vikunja · Incorrect Authorization Medium5.3EPSS 0.2%CVE Database V5CVE-2026-111921 Vikunja versions from 0.13.0 up to but not including 2.7.0 have an incorrect authorization vulnerability. This flaw allows project members with Write permission to delete admin-level link shares by exploiting a permission check that reads an unloaded share object. Attackers can send DELETE requests to project share endpoints with sequential share IDs to revoke admin links and invalidate share sessions. | CVE Database V5 | ||
| Medium | 5.3 | EPSS 0.2% | CVE-2026-111920vikunja · Improper Privilege Management Medium5.3EPSS 0.2%CVE Database V5CVE-2026-111920 Vikunja version 2.6.0 has an improper privilege management vulnerability in the getProjectAccessForUser function. This flaw causes the system to incorrectly calculate a user's effective project permissions by taking the maximum permission across the project hierarchy, overriding explicit lower permissions on sub-projects. As a result, a collaborator with write access on a parent project but only read access on a child project can improperly modify tasks and content in the child project. | CVE Database V5 | ||
| High | 7.1 | EPSS 0.4% | CVE-2026-111919vikunja · Insufficient Session Expiration High7.1EPSS 0.4%CVE Database V5CVE-2026-111919 Vikunja versions from 0.22.0 up to but not including 2.7.0 have a vulnerability where removed collaborators can continue to receive project data via webhooks they created before their access was revoked. This improper access revocation allows former collaborators to receive task titles and descriptions, including new content created after their removal. | CVE Database V5 | ||
| Medium | 5.3 | EPSS 0.2% | CVE-2026-111918vikunja · Incorrect Authorization Medium5.3EPSS 0.2%CVE Database V5CVE-2026-111918 Vikunja versions from 1.0.0-rc0 up to but not including 2.7.0 have an improper authorization vulnerability in the RecalculateTaskPositions function. Authenticated users without access to any projects can write task_positions rows across all tasks instance-wide. This flaw allows attackers to manipulate their Inbox and create saved filters that persist cross-tenant references, potentially amplifying writes and causing denial of service. | CVE Database V5 | ||
| High | 7.1 | EPSS 0.4% | CVE-2026-111917vikunja · Insufficient Session Expiration High7.1EPSS 0.4%CVE Database V5CVE-2026-111917 Vikunja versions 2.3.0 up to but not including 2.7.0 have a session validation vulnerability where WebSocket access can persist after session revocation. This occurs because the handleAuth function does not verify the JWT sid claim against the sessions table. As a result, attackers with a stolen access token can maintain WebSocket connections and continue receiving notifications even after the session is deleted or TOTP enrollment occurs. | CVE Database V5 | ||
| High | 8.1 | EPSS 0.3% | CVE-2026-62376vikunja · Cleartext Storage of Sensitive Information High8.1EPSS 0.3%CVE Database V5CVE-2026-62376 Vikunja versions prior to 2.4.0 store sensitive tokens such as password-reset, email-confirmation, and account-deletion tokens in plaintext within the user_tokens database table. This cleartext storage allows an attacker with read access to the database to use these tokens to take over user accounts without needing passwords. The issue is fixed in version 2.4.0. | CVE Database V5 | ||
| High | 7.5 | EPSS <0.1% | CVE-2026-62367vikunja · Improper Authentication High7.5EPSS <0.1%CVE Database V5CVE-2026-62367 Vikunja versions 1.0.0 through 2.3.0 contain an improper authentication vulnerability in the OpenID Connect provider's email fallback feature. When enabled, the system links an SSO login to a local account using only the email claim without verifying the email or requiring the account password. This allows an attacker who obtains a token with a victim's email to log in as that victim without their consent. The issue is fixed in version 2.4.0. | CVE Database V5 | ||
| High | 8.1 | EPSS 0.3% | CVE-2026-57458vikunja · Improper Privilege Management High8.1EPSS 0.3%CVE Database V5CVE-2026-57458 Vikunja version 2.3.0 contains an improper privilege management vulnerability where a scoped API token limited to the oauth.authorize permission can be used to obtain a bearer JWT and refresh token without scope restrictions. This allows access to API routes beyond the token's original permissions. The issue is fixed in version 2.4.0. | CVE Database V5 | ||
| High | 7.5 | High7.5GCVE Database Vikunja versions up to 2.5.0 have a vulnerability where the /api/v2 public authentication endpoints lack rate limiting, allowing unauthenticated attackers to perform unlimited credential guessing, account enumeration, and password-reset flooding attacks. This issue does not affect the /api/v1 endpoints, which are properly rate limited. The vulnerability has a high severity with a CVSS score of 7.5. The advisory is a duplicate and references GHSA-6rvj-qwjf-3m4q. No patch links are provided in this data. | GCVE Database | |||
| Medium | 6.5 | Medium6.5GCVE Database Vikunja versions up to 2.5.0 have a vulnerability where the application does not limit archive expansion during data import. Authenticated users can upload specially crafted compressed files that expand massively in memory and disk usage, causing denial of service by exhausting server resources. This vulnerability is fixed starting from version 2.6.0. | GCVE Database | |||
| Medium | 5.3 | EPSS 0.3% | CVE-2026-91983vikunja · Incorrect Authorization Medium5.3EPSS 0.3%CVE Database V5CVE-2026-91983 ## Summary API token permission checks only match the HTTP method and route path. The `expand` query parameter on task read endpoints embeds data from other permission groups (task comments, reactions, time entry counts) without checking whether the token holds those scopes. A token scoped only to `tasks` read permissions can therefore read task comments and reactions it was explicitly not granted. ## Details `models.CanDoAPIRoute` (`pkg/models/api_routes.go`, ~line 440) authorises API tokens purely by comparing `method` and `c.Path()` against the routes stored for each granted permission group. The query string is never consulted. The task read endpoints accept `expand`: - `GET /api/v1/tasks/:task`, `GET /api/v1/tasks` (`pkg/models/tasks.go` ReadOne/ReadAll, `pkg/models/task_collection.go`) - `GET /api/v2/tasks/:id`, `GET /api/v2/tasks`, `GET /api/v2/projects/:project/tasks` (`pkg/routes/api/v2/tasks.go`, `task_collection.go`) Accepted values include `comments`, `comment_count`, `reactions`, `time_entries_count`. `addMoreInfoToTasks` (`pkg/models/tasks.go`, ~line 683) loads and embeds that data. At that layer only a `web.Auth` (the plain owner user, resolved by `auth.GetAuthFromClaims`) is available, so the token's scopes cannot be enforced there either. Result: the `tasks_comments`, `reactions`, and `time_entries` permission groups are advisory for any data reachable through a task expansion. ## Impact A holder of an API token scoped to `tasks: [read_one]` or `tasks: [read_all]` (or `projects_views_tasks: [read_all]`) can read the full bodies of task comments, all reactions, and time entry counts on every task the token owner can access, despite `GET /api/v1|v2/tasks/:task/comments` and the reactions endpoints correctly returning 401 for the same token. The leak is limited to data the token owner can already see, and is read-only. The realistic victim is a user who grants a narrowly scoped token to a third-party integration expecting comments to stay private. ## Proof of Concept Against the test fixtures (`pkg/db/fixtures/api_tokens.yml`, token 1 has `{"tasks":["read_all","update"]}`, plaintext `tk_2eef46f40ebab3304919ab2e7e39993f75f29d2e`): ``` GET /api/v2/tasks/1/comments Authorization: Bearer tk_2eef46f40ebab3304919ab2e7e39993f75f29d2e -> 401 {"code":11,"message":"missing, malformed, expired or otherwise invalid token provided"} GET /api/v2/tasks?expand=comments&filter=id%3D1 Authorization: Bearer tk_2eef46f40ebab3304919ab2e7e39993f75f29d2e -> 200, response items[0].comments contains the full comment objects ``` Same behaviour with `expand=reactions`, and on the v1 endpoints `GET /api/v1/tasks?expand=comments` / `GET /api/v1/tasks/1?expand=comments`. Any user-created token with only `tasks` read permissions reproduces this. ## Recommended Fix Enforce expansions in the single existing choke point, `models.CanDoAPIRoute`: after the method/path match succeeds, read `c.QueryParams()["expand"]` and require the token to hold the owning group's read permission for each value, e.g. - `comments`, `comment_count` -> `tasks_comments.read_all` - `reactions` -> `reactions.read_all` - `time_entries_count` -> `time_entries.read_all` (`subtasks`, `buckets`, `is_unread` are task-level data and need nothing extra.) Doing this in the middleware covers both v1 and v2 without handler or model changes. Add a table-driven test alongside `pkg/webtests/api_token_method_matching_test.go` asserting a `tasks`-only token gets 401 with `expand=comments` and 200 once `tasks_comments.read_all` is added. | CVE Database V5 | ||
| Medium | 4.3 | EPSS 0.3% | CVE-2026-91982vikunja · Insufficiently Protected Credentials Medium4.3EPSS 0.3%CVE Database V5CVE-2026-91982 ### Summary Once a user has TOTP enabled, the API still hands back the raw shared secret to anyone holding that account's access token. Reading it doesn't ask for the password, even though disabling TOTP does. So a stolen token, an XSS, or a browser left open is enough to copy the second factor into your own authenticator and keep generating valid codes indefinitely. ### Details `GET /api/v1/user/settings/totp` returns the full TOTP object, including the secret field and the otpauth:// provisioning URL. `GET /api/v1/user/settings/totp/qrcode` renders the same secret as a QR image. Neither requires re-authentication, the access token alone is enough. This is inconsistent with the rest of the flow: `POST /api/v1/user/settings/totp/disable` calls CheckUserPassword before it will turn TOTP off. So the destructive action is gated behind the password, but reading out the secret that backs the second factor isn't. There's also no reason for the secret to be readable at all once enrollment is finished, the client only needs it during setup. The fix is to stop returning secret/url/the QR code once enabled is true (only expose them during the enrollment window). Requiring the password on the read, like disable already does, would also be reasonable. ### PoC The attacker needs the victim's access token, from an XSS, a leaked/stolen token, or an unlocked session. TOTP must already be enabled on the account. 1. With the victim's bearer token, call: ``` GET /api/v1/user/settings/totp Authorization: Bearer <victim_token> ``` The response contains the shared secret and the otpauth:// URL: ``` {"secret": "<base32 secret>", "enabled": true, "url": "otpauth://totp/..."} ``` 2. Paste that secret (or scan `/api/v1/user/settings/totp/qrcode`) into any authenticator app. 3. It now produces the same 6-digit codes as the victim's device. Nothing is logged and the victim gets no notification. ### Impact This defeats the purpose of the second factor. 2FA is supposed to survive exactly this situation, a compromised password or a hijacked session, but a single read of the account's own settings hands over the shared secret with no step-up check and no trace. Combined with a known or later-recovered password, it's a durable account takeover that TOTP was meant to prevent. | CVE Database V5 | ||
| Medium | 4.3 | EPSS 0.3% | CVE-2026-91981vikunja · Information Exposure Medium4.3EPSS 0.3%CVE Database V5CVE-2026-91981 ### Summary A link-share token, the credential you hand out to let someone view a shared project, can call the v2 user-search endpoints, which it was never meant to reach. The v1 versions of the same endpoints correctly reject link-share tokens; the v2 versions don't. As a result, anyone with a share link can list every member of the shared project (and its parent projects), and can test whether any username exists anywhere on the instance. ### Details Two endpoints are affected: `GET /api/v2/projects/{id}/users/search` only checks project.CanRead, which a link share satisfies on its own project. With no parameter it returns the full user list, walking up the parent-project chain. `GET /api/v2/users?q=<name>` is a global search. It needs a query term, but any term works as an existence oracle: a hit confirms the username exists somewhere on the instance and returns the display name. Emails are correctly stripped from these responses, so the exposure is limited to identities (usernames and display names), but that's still an instance-wide directory that a share-link holder should never see. The fix is to resolve the caller with GetFromAuth in both handlers, so link-share tokens are rejected the same way v1 already rejects them. ### PoC 1. Get a link-share token. With any account that owns a project, create a read-only link share and note the hash, then exchange it: ``` POST /api/v1/shares/<hash>/auth {"password":""} ``` The response contains a token, a link-share JWT. 2. Confirm v1 blocks it (the fixed behavior). With that token: ``` GET /api/v1/projects/<id>/projectusers Authorization: Bearer <link_share_token> ``` → rejected: the token isn't a user token. 3. Now the vulnerable v2 endpoints, same token: ``` GET /api/v2/projects/<id>/users/search Authorization: Bearer <link_share_token> ``` → 200, full list of everyone with access to the project and its parents (id, username, name). ``` GET /api/v2/users?q=admin Authorization: Bearer <link_share_token> ``` → 200, a non-empty result confirms the username exists on the instance. Iterate q over a wordlist to enumerate accounts. ### Impact Information disclosure. A link share is a low-trust credential meant to expose one project's contents; here it also exposes the membership of that project's parents and lets the holder probe for any username across the whole instance. Since share links are often forwarded or semi-public, this hands an untrusted recipient a map of internal users and a username-validation oracle useful for phishing and credential stuffing. No account is required beyond possessing a share link. Emails are not exposed. | CVE Database V5 | ||
| Medium | 4.3 | EPSS 0.3% | CVE-2026-91980vikunja · Information Exposure Medium4.3EPSS 0.3%CVE Database V5CVE-2026-91980 ### Summary When you share a project with a team, the API lets you attach any team on the instance, including teams you have nothing to do with, as long as you're an admin of the project. Listing a project's teams then returns each team's full member roster. So any logged-in user can spin up a throwaway project, attach team IDs one by one, and read back the name, description, and complete member list of every team on the instance. Normally you can only see teams you belong to; this path ignores that. ### Details Sharing a project with a team is a two-step flow: you PUT a team to the project, then you GET the project's teams to see them. The share endpoint only enforces that you're an admin of the target project, it doesn't care whether the team you're referencing is one you're allowed to see. Any existing team ID is accepted. The listing endpoint is even more permissive: read access to the project is enough, which you obviously have on a project you created. And its response isn't just a list of team names, it includes each team's description, creator, and a full members array with every member's username, display name, and admin flag. Put together, an attacker who owns a single project can walk the team ID space and pull the roster of every team in the instance, including teams they aren't a member of. This contradicts how team visibility works everywhere else in the product, where you only ever see teams you belong to. Emails are stripped from the roster, so what leaks is identities and org structure rather than contact details. The fix is to require that the caller actually has access to the team they're attaching, not just to the project. ### PoC The attacker needs nothing but an ordinary account. 1. Create a throwaway project you own: 2. Attach team IDs one by one (sweep the range you want): ``` PUT /api/v1/projects/<your_project_id>/teams {<SNIP>, "team_id": <N>, <SNIP>} ``` 3. Read them back with their rosters: ``` GET /api/v1/projects/<your_project_id>/teams ``` Each entry contains the team name, description, creator, and a members array with every member's username, display name, and admin flag, for teams you are not a member of. ### Impact Information disclosure of the instance-wide team directory: team names, descriptions, and full membership, for teams the attacker has no relationship with. This maps out the organisation's group structure and who belongs where, useful for targeting and social engineering. Any authenticated account is enough; no share link, no admin rights, no victim interaction. | CVE Database V5 | ||
| Medium | 6.5 | EPSS 0.4% | CVE-2026-91979vikunja · Uncontrolled Resource Consumption Medium6.5EPSS 0.4%CVE Database V5CVE-2026-91979 ### Summary Vikunja lets you export your data as a zip and import it back. The import doesn't limit how large the archive expands to, how many files it contains, or how much storage one user can consume, and it appears to hold the whole archive in memory while processing it. Because the archive is compressed, a ~20 MB upload made of highly compressible content expands to tens of gigabytes once imported. A single crafted upload is enough to exhaust the server's memory or fill its disk and knock the whole instance offline. It can be repeated or run in parallel, since nothing stops a user from starting several imports at once. ### Details The import accepts an archive in the same layout the built-in export produces: a manifest describing projects and tasks, plus the attachment files those tasks reference. Each individual file inside the archive is size-checked, but there's no ceiling on the total uncompressed size or on the number of files, and there's no per-user storage quota anywhere. On top of that, the server seems to buffer every attachment in memory before writing it out, so the peak memory cost is the sum of all decompressed files at once, not one at a time. Compression is the key factor: a file full of a repeating byte (e.g. zeroes) shrinks by roughly a thousand to one. So an archive that fits under the upload size limit (around 20 MB) can describe on the order of a thousand ~20 MB attachments, which the server then expands to tens of gigabytes in RAM and writes to disk. When memory runs out the process is killed; when the disk fills, the instance (and anything else on that host) stops working. Nothing serialises imports, so a user can also fire several in parallel to reach the limit faster. The fix is to cap the total uncompressed size and the file count per archive, stream each file to disk instead of buffering them all in memory, enforce a per-user storage quota, and refuse a new import while one is already running for that user. ### PoC The attacker needs an ordinary account that can use the import feature. 1. Build a zip in the export format containing one project with for example ~200 tasks, each referencing an attachment, and ~200 attachment files, each 18 MB of zero bytes. Filled with zeroes, the whole thing compresses to roughly 4 MB (under the upload limit). ```python #!/usr/bin/env python3 import argparse, json, os, zipfile CHUNK = b"\0" * (1024 * 1024) def build(out, n, size, version): tasks = [{"title": f"t{i}", "attachments": [{"file": {"id": i, "name": "a"}}]} for i in range(1, n + 1)] data = json.dumps([{"title": "bomb", "tasks": tasks, "child_projects": []}]).encode() with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z: z.writestr("VERSION", version) z.writestr("data.json", data) for i in range(1, n + 1): with z.open(f"files/{i}", "w") as f: for _ in range(size): f.write(CHUNK) raw = n * size * 1024**2 print(f"[+] {out}: {os.path.getsize(out)/1024**2:.2f} MiB " f"({raw/1024**3:.1f} GiB uncompressed)") p = argparse.ArgumentParser() p.add_argument("-o", "--out", default="bomb.zip") p.add_argument("-n", "--files", type=int, default=200) p.add_argument("-s", "--size-mib", type=int, default=18) p.add_argument("--version", default="v2.5.0") a = p.parse_args() build(a.out, a.files, a.size_mib, a.version) ``` 3. Upload it to the import endpoint: ``` PUT /api/v1/migration/vikunja-file/migrate (multipart file upload: the crafted zip) ``` 4. Watch the server. Memory climbs into 3.5 GB within seconds and the disk fills with the expanded files. On a typical host the process is OOM-killed or the disk fills, and the instance stops responding for everyone. Repeat, or send several uploads at once, to reach the limit reliably. (You can derive the exact archive layout in step 1 by first exporting your own data once and inspecting the zip it hands back.) ### Impact Denial of service (availability). One authenticated user, with a single upload, can drive the server into tens of GB of memory and disk usage, killing the process or filling the disk. This takes the whole instance down for every user, and if the disk is shared with other services on the host, those go down too. No admin rights and no victim interaction are required, any account that can import data is enough. Recovery needs an operator to reclaim memory/disk and clear the partially written import. | CVE Database V5 | ||
| High | 8.7 | EPSS 0.7% | High8.7EPSS 0.7%CVE Database V5CVE-2026-91973 ### Summary The `/dav`, `/.well-known`, and `/feeds` groups are registered on the root Echo instance with only BasicAuth and no rate limiter. CalDAV BasicAuth accepts the plain account password, so password guessing over `/dav` is unbounded and never returns 429, while `/api/v1/login` is throttled from the tenth attempt. The only anti-brute-force control on the instance is therefore bypassable. ### Details `pkg/routes/routes.go` (~lines 238-249) registers `/.well-known`, `/dav`, and `/feeds` with `middleware.BasicAuth(...)` and nothing else; `registerCalDavRoutes` adds no limiter. `pkg/routes/caldav/auth.go` (~lines 88-93) falls through to `user.CheckUserCredentials` with the plain account password when no CalDAV token matches. In contrast, `/register`, `/login`, etc. are wrapped by `unauthRateLimit()` — an unconditional 10/min/IP pre-auth floor that ignores `ratelimit.enabled` (default false). TOTP-enabled accounts and bot users are correctly refused, so this targets password-only accounts. ### PoC (verified at runtime against v2.5.0) ``` POST /api/v1/login x25 wrong passwords -> 429 from attempt 2 (throttled) PROPFIND /dav/principals/{user}/ x60 wrong passwords -> 401 x60, 429 x0 GET /feeds/notifications.atom x30 wrong passwords -> 401 x30, 429 x0 PROPFIND /dav/... with correct password -> 207 (proves the 401s are real auth failures) ``` ### Impact The anti-brute-force floor guarding `/login` is entirely absent on `/dav`, `/feeds`, and `/.well-known`, giving an unbounded credential-guessing surface against account passwords. (bcrypt caps throughput to a few guesses/second, but nothing caps the number of attempts.) Reported as an authentication-control bypass, not a DoS. ### Fix Apply the unconditional pre-auth rate-limit floor to the `/dav`, `/.well-known`, and `/feeds` groups. | CVE Database V5 | ||
| High | 8.7 | EPSS 0.6% | High8.7EPSS 0.6%CVE Database V5CVE-2026-91972 ### Summary `registerAPIRoutesV2` never applies the unconditional pre-auth rate-limit floor (`unauthRateLimit()`) to the v2 public routes — it passes that limiter only to `/api/v2/ws` — and otherwise relies on `setupRateLimit`, which registers nothing when `ratelimit.enabled` is false (the default). So on a stock install every v2 pre-auth endpoint (login, register, password-reset token, oauth token) is unthrottled, while its v1 twin is throttled. ### Details `unauthRateLimit()` -> `perMinuteIPRateLimit("noauth", RateLimitNoAuthRoutesLimit)` (`pkg/routes/rate_limit.go`, ~lines 100-118) is an unconditional per-IP floor (default 10/60s) that deliberately ignores `RateLimitEnabled`, which is why v1's pre-auth routes are throttled even with the global limiter off. v1 applies it: `ur := a.Group(""); ur.Use(unauthRateLimit())` (`pkg/routes/routes.go` ~line 459). `registerAPIRoutesV2` (~lines 405-431) passes the `unauthRateLimit()` instance only to `/api/v2/ws`; its auth routes get only `setupRateLimit(a, ...)`, which is config-gated and registers nothing by default. ### PoC (verified at runtime against v2.5.0) ``` POST /api/v1/login x25 -> 429 from attempt 5 POST /api/v2/login x25 -> 403 x25, 429 x0 POST /api/v1/user/password/token -> 429 (throttled) POST /api/v2/user/password/token x20 -> 404 x20, 429 x0 ``` Request bodies are byte-identical across versions (shared `user.Login` / `user.PasswordTokenRequest`). Both v2 endpoints reach their handlers (403/404, not route-404), so the comparison is valid. ### Impact The pre-auth rate-limit floor — the instance's only default anti-brute-force / anti-abuse control — is absent on all v2 public endpoints. Enables unbounded credential guessing, account-enumeration probing, and password-reset flooding on a default install. Reported as an authentication-control bypass, not a DoS. ### Fix Apply `unauthRateLimit()` to the v2 public route group, matching v1. | CVE Database V5 | ||
| High | 7.1 | EPSS 0.4% | CVE-2026-91971vikunja · Uncontrolled Resource Consumption High7.1EPSS 0.4%CVE Database V5CVE-2026-91971 ### Summary The 50-megapixel decode guard exists only on the task-attachment preview path. Avatar and project-background uploads decode uploaded images with no pixel cap. Worse, the avatar resize fixes the output height at 1024 and derives the width from the aspect ratio, so a tiny extreme-aspect-ratio PNG expands to an enormous output image — an input-side pixel cap would not catch it. ### Details The only `maxPixels` check (50MP) is in `TaskAttachment.GetPreview` (`pkg/models/task_attachment.go` ~lines 332-338). No such check guards: - avatar upload: `pkg/modules/avatar/upload/upload.go` (~lines 82, 137, 141) - project background: `pkg/modules/background/handler/background.go` (~lines 174, 269, 289) `imaging.Resize(img, 0, 1024, imaging.Lanczos)` (upload.go ~line 141) fixes height=1024 and derives width from the aspect ratio: a 20000x10 input yields a ~2,048,000 x 1024 output (~2.1 billion pixels), so a few-hundred-byte file drives huge CPU and memory. ### PoC (verified at runtime against v2.5.0) ``` PUT /api/v1/user/settings/avatar/upload avatar=8000x8000 PNG (64MP, 192KB) -> 200 (accepted; exceeds the 50MP attachment cap) PUT /api/v1/user/settings/avatar/upload avatar=20000x10 PNG (681 bytes) -> 200 after ~19.6s of server processing ``` Contrast (guard present): uploading the 64MP PNG as a task attachment and requesting its preview returns in ~1ms without decoding — `GetPreview` rejects it via `maxPixels` and falls back to the raw file. ### Impact A small crafted upload drives disproportionate CPU and memory on the avatar and background paths. Repeated or concurrent requests can exhaust server resources. Amplification comes from both the missing input pixel cap and the height-fixed resize, so an input-side cap alone is insufficient. ### Fix Apply a pixel-dimension cap (as on the attachment path) to the avatar and background decode paths, and bound the resize output dimensions (cap width as well as height). | CVE Database V5 |
Page 1 of 3