Link share authenticated users could call ReadAll on link shares,
which leaked hash credentials for other shares on the same project.
This allowed permission escalation from read-only to write/admin.
Add a check at the top of ReadAll() that rejects link-share-authenticated
callers, mirroring the pattern in CanRead() and canDoLinkShare().
Update tests to expect 403 Forbidden for all link share permission levels.
Fixes GHSA-8hp8-9fhr-pfm9
The SSRF protection is now tested at the shared utility level in
pkg/utils/httpclient_test.go. The webhook-specific SSRF tests were
duplicating the same checks since getWebhookHTTPClient() delegates
to NewSSRFSafeHTTPClient().
When `forceuserinfo: true`, `mergeClaims` discards `vikunja_groups`
and `extra_settings_links` claims fetched from the userinfo endpoint,
failing team sync for opaque tokens.
Fixes team sync for OIDC providers using opaque tokens.
Add config-level exclusions for G117 (secret-named struct fields),
G101 in test files, G702/G704 in magefile, and goheader in plugins.
Add inline #nosec comments for specific G703/G704 false positives
in export, dump/restore, migration, and avatar code.
CheckIsArchived() previously skipped checking a child project's own
IsArchived flag when ParentProjectID > 0, immediately recursing to
only check the parent. This allowed write operations on individually
archived child projects whose parent was not archived.
Now the function loads the project from the database first, checks its
own IsArchived flag, and only then recurses to check parent projects.
Return ErrAccountLocked for locked users instead of ErrAccountDisabled.
Also skip profile updates and avatar sync for disabled/locked users
found during OIDC login — HandleCallback rejects the auth anyway.
The username and email uniqueness checks don't need status filtering —
they just need to know if the name/email exists regardless of account
status. Use getUser (which skips the status check) instead of the
public wrappers, reducing cyclomatic complexity back under the threshold.
Make isErrUserStatusError public and replace all verbose
!IsErrAccountDisabled(err) && !IsErrAccountLocked(err) checks
with the shorter IsErrUserStatusError(err) call.
checkAPITokenAndPutItInContext now returns 401 Unauthorized when the
token owner's account is disabled or locked, instead of a 500 error.
Also fixes the API token test to match the actual middleware behavior.
getUser now returns ErrAccountDisabled or ErrAccountLocked (alongside
the full user object) for users with StatusDisabled or StatusAccountLocked.
Callers that need disabled/locked users discard the error; all others
propagate it automatically.
GHSA-94xm-jj8x-3cr4
Separate from ErrAccountDisabled so callers can distinguish between
admin-disabled accounts (no recovery) and locked accounts (recoverable
via password reset).
Store a unix timestamp instead of a boolean, and treat entries older
than 90 seconds as expired. A background goroutine lazily cleans up
expired keys after each successful validation to prevent unbounded
growth in the keyvalue store.
Store used TOTP passcodes in the keyvalue store after successful
validation. On subsequent validation attempts, check if the passcode
was already used for the same user and reject it with
ErrTOTPPasscodeUsed. This prevents replay attacks where an intercepted
TOTP code could be reused within its 30-second validity window.
Add TestTOTPPasscodeCannotBeReused which verifies that a valid TOTP
passcode cannot be used twice within its validity window. Also add
ErrTOTPPasscodeUsed error type for the new behavior.
Add a TOTP fixture for user1 with a known secret to enable
testing TOTP validation logic. Update InitTests to load the
totp fixture alongside users and user_tokens.
Add a CalDAV token fixture (kind=4) for user10 who has TOTP enabled,
and implement the previously-skipped test proving token-based auth
still works when TOTP is active.
Internal callers (reactions) look up comments by ID without knowing
the task. The IDOR protection is still effective because ReadOne
always has TaskID set from the URL parameter.
RemoveProjectBackground previously used checkProjectBackgroundRights
which only checks CanRead, allowing read-only users to delete project
backgrounds. Added checkProjectBackgroundWriteRights that checks
CanUpdate and use it in RemoveProjectBackground.
Ref: GHSA-564f-wx8x-878h
Proves that a user with read-only access to a project can delete its
background image. The test expects a 403 Forbidden but the operation
proceeds because RemoveProjectBackground only checks CanRead.
Adds fixture entry giving user 15 read-only access to project 35
(which has a background_file_id).
Ref: GHSA-564f-wx8x-878h
Add task_id check to getTaskCommentSimple so that a comment can only
be loaded if it actually belongs to the task specified in the URL.
Previously, any valid comment ID could be read through any accessible
task endpoint.
Ref: GHSA-mr3j-p26x-72x4
Proves that a user can read a comment from an inaccessible task by
supplying an accessible task ID in the URL. Comment 18 belongs to
task 34 (owned by user 13), but testuser1 can read it via task 1.
Ref: GHSA-mr3j-p26x-72x4
File.File is now io.ReadCloser (no Seek). Buffer the file bytes
once via io.ReadAll, then use bytes.NewReader for both DecodeConfig
and Decode. Test updated to use io.NopCloser instead of afero.
Replace the github.com/spf13/afero dependency with a purpose-built
FileStorage interface (Open, Write, Stat, Remove, MkdirAll) with three
implementations: localStorage (with basePath), s3Storage (with key
prefix), and memStorage (for tests).
Each implementation owns its base path — callers pass only file IDs.
Delete s3fs.go, change File.File from afero.File to io.ReadCloser,
and fix duplication flows to buffer content for seeking.
http.ServeContent requires io.ReadSeeker which S3 files don't support.
Add S3 branch using io.Copy with explicit headers, matching the pattern
already used in task attachment downloads.
Refs: #2347
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the third-party afero-s3 library (and its temporary fork) with
a minimal in-tree afero.Fs implementation (~200 lines) that only supports
the three S3 operations Vikunja actually uses: Open (GetObject), Remove
(DeleteObject), and Stat (HeadObject).
The s3File implementation lazily opens a GetObject stream on first Read
and supports Seek by closing and re-opening the stream from the new offset,
matching the behavior of the fixed afero-s3 fork.
All other afero.Fs/File methods return ErrS3NotSupported since they are
never called in the S3 code path (writes already use direct PutObject).
This removes the fclairamb/afero-s3 dependency and the temporary replace
directive in go.mod entirely.
Closes#2347
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolves issue #475 by modifying CalDAV discovery so Apple Reminders can
use /dav/projects/ as the home set without exposing that synthetic path
as a real task list, preserving the existing principal-based flow. This
is because Apple Reminders defaults back to the /dav/projects/ URL,
rather than accepting the /dav/principals/username/ URL specified in
Vikunja.
Resolves#475
Block webhook requests to non-globally-routable IP addresses by default.
Uses net.Dialer.Control hook to validate resolved IPs against IANA
Special Purpose Registries after DNS resolution, preventing DNS rebinding.
Configurable via webhooks.allownonroutableips (default: false).
When a user enables two factor authentication, all existing sessions are
now invalidated, requiring re-authentication. This prevents pre-existing
sessions from bypassing 2FA. The frontend now shows a notice explaining
the logout before the user confirms, and properly logs out after enabling.
Ref: GHSA-pgc7-cmvg-mvp4
The `expand` query parameter only supported the `expand[]=foo` array
format, but the swagger docs described it as a plain string parameter.
This adds support for both formats (`expand=foo` and `expand[]=foo`),
matching the existing pattern used by `sort_by` and `order_by`
parameters.
Closes#2408
---------
Co-authored-by: kolaente <k@knt.li>
- Fixes `removeStaleRelations` in CalDAV storage provider to only remove
relations of kinds explicitly declared in the incoming VTODO's
`RELATED-TO` properties
- When a VTODO has no `RELATED-TO` at all (e.g., a parent task from
Tasks.org), no relations are removed — they were auto-created as
inverses by child tasks
- When a VTODO declares specific relation kinds (e.g.,
`RELATED-TO;RELTYPE=PARENT`), only relations of that kind are checked
for staleness; other kinds (like auto-created `subtask` inverses) are
preserved
Fixes#2383
---------
Co-authored-by: kolaente <k@knt.li>
The tasks_labels_bulk route was not recognized as a CRUD route by
isStandardCRUDRoute, causing it to be processed as a non-CRUD route
and registered in the wrong apiTokenRoutes group. API tokens with
tasks_labels permissions could not access the bulk endpoint, resulting
in a 401 error.
Fixes https://github.com/go-vikunja/vikunja/issues/2375
When deleting a user via CLI (`vikunja user delete <id> -n`), the user
row was deleted first, then `notifications.Notify` was called. But
`Notify` calls `User.ShouldNotify()` which queries the database to check
the user's status — and since the row was already deleted within the same
transaction, it returned `ErrUserDoesNotExist`.
Move the notification call before the `DELETE` so the user row still
exists when `ShouldNotify` checks it.
Closesgo-vikunja/vikunja#2335
When running as a systemd service, the binary is often in /usr/local/bin
but WorkingDirectory points to a data directory like /var/lib/vikunja.
Previously, rootpath defaulted to the binary's directory, causing
Vikunja to attempt creating files/db in /usr/local/bin.
Now os.Getwd() is preferred, which respects systemd's WorkingDirectory
and is the intuitive default for all deployment scenarios.
Add comprehensive e2e tests for user-level webhook CRUD operations
and update existing project webhook tests to use LoadFixtures() for
cleanup instead of manual DELETE queries.
Add CRUD endpoints for user-level webhooks under /user/settings/webhooks.
Users can create webhooks that fire on user-directed events (task.reminder.fired,
task.overdue, tasks.overdue). Includes proper permission checks via
canDoWebhook which loads webhook ownership from DB.