Commit Graph
14767 Commits
Author SHA1 Message Date
kolaente c549e7ff51 feat(audit): audit full personal data export requests
Around 38 events are registered for audit logging, including every admin
action, but a full personal data export left no trace at all. It is
dispatched from both v1 and v2, so one registration covers both.
2026-07-28 18:05:34 +02:00
kolaente 9fbce2154b fix(license): refuse redirects and use the SSRF-safe http client for checks
The license servers are hardcoded, but the check client followed redirects
without any policy and dialed without the SSRF guard, so a hijacked or
poisoned license host could forward the license key to an internal address.
Redirects are refused outright rather than capped: the check is a POST to a
fixed JSON API that never redirects.
2026-07-28 17:26:12 +02:00
kolaente 446f722b20 fix(gravatar): route avatar requests through the SSRF-safe http client
avatar.gravatarbaseurl is operator-configurable, so the request destination was
never a fixed constant - pkg/utils/avatar.go already uses the SSRF-safe client
for the same job. The previous 5s timeout is kept as a context deadline so the
configured (30s by default) client timeout does not apply here.
2026-07-28 17:26:12 +02:00
kolaente 5513835fc1 fix(unsplash): route api requests through the SSRF-safe http client
doGet built its own http.Client, unlike its siblings in the same file which
already use utils.NewSSRFSafeHTTPClient(). The previous 10s timeout is kept as
a context deadline so the configured (30s by default) client timeout does not
apply here.
2026-07-28 17:26:12 +02:00
kolaente 0515f2f0db fix(webhooks): bound the error response body read
The webhook target URL is user-configured, so a hostile target can answer
a delivery with a 4xx/5xx carrying an arbitrarily large body. That body
was read whole into memory and written whole to the log.

Cap the read at 4KiB, which is plenty for a diagnostic log line, matching
the LimitReader already used for the license server response.
2026-07-28 17:25:57 +02:00
kolaente e987811a75 fix(auth): gate the v2 login route on local or ldap auth being enabled
v1 only registers /login when local or LDAP auth is enabled, but v2
registered it unconditionally. With auth.local.enabled=false a
pre-existing local password still authenticated on /api/v2/login.

/logout stays unconditional - it terminates any session, OIDC included.
2026-07-28 17:25:37 +02:00
TinkandGitHub 93124ba77b fix(caldav): close username enumeration oracle in basic auth (#3349) 2026-07-28 17:22:58 +02:00
TinkandGitHub 584ddd99a2 fix(oauth2): burn authorization code even when validation fails (#3350) 2026-07-28 17:13:15 +02:00
TinkandGitHub ccc46508b0 fix(security): rate limit the websocket upgrade endpoint (#3348) 2026-07-28 17:12:18 +02:00
kolaente b506dfe792 ci: drop unused contents: write from generate-swagger-docs
The job pushes with the SSH deploy key configured by actions/checkout's
ssh-key input, so GITHUB_TOKEN is never used for writes. Scope it down to
contents: read.
2026-07-28 10:42:15 +02:00
kolaenteandkolaente 2025d8c4f8 fix(api tokens): guard GetTokenFromTokenString against short token strings
GetTokenFromTokenString sliced token[len(token)-8:] without checking the
length, so any string with the "tk_" prefix but shorter than 8 characters
panicked with "slice bounds out of range".

The helper is reachable with attacker-controlled input from three
unauthenticated call sites: the main API bearer-token middleware
(/api/v1 and /api/v2) and CalDAV basic auth on /dav/ and
/.well-known/caldav. Each request was turned into a 500 by the global
panic recovery, at the cost of a full stack unwind, an error-level log
line and a Sentry event. On the main API the token middleware also runs
before the rate limiter, and the CalDAV routes are not rate limited at
all.

The same guard already existed in the feeds auth path; fixing it at the
shared choke point closes all sites at once. Real tokens are the prefix
plus 40 hex characters, so no legitimate token is affected — a too-short
token now gets the same rejection as any other invalid one.
2026-07-28 06:59:04 +00:00
kolaenteandkolaente 50881a1c13 fix(notifications): deliver task deleted notifications again
The listener runs after the deleting transaction committed, so the task
is already soft-deleted when it looks up who to notify. Every task
subscription lookup filters `t.deleted_at IS NULL`, so the subscriber
list came back empty and nobody was notified - not even users with full
access.

The `IsErrTaskDoesNotExist` fallback to project subscribers never
covered for this: the lookup returns an empty slice with a nil error, so
the branch could not fire. Removed rather than repaired - with
soft-deleted tasks included, the CTE resolves project and parent-project
subscriptions on its own, which is strictly more than the fallback did.

Soft-deleted tasks are opt-in, so the reminder crons keep ignoring them.
Permissions come from the project, since the task can no longer carry
them - a subscriber who lost access still gets nothing.
2026-07-28 06:52:13 +00:00
renovate[bot]andkolaente d3352112e2 chore(deps): update actions/stale action to v11 2026-07-28 06:49:24 +00:00
renovate[bot]andkolaente d3b9e6d2f3 chore(deps): update danielroe/provenance-action digest to ec1767b 2026-07-28 06:49:11 +00:00
kolaente 11b4b59ed5 fix(auth): use configured bcrypt rounds everywhere 2026-07-28 08:43:26 +02:00
renovate[bot]andTink 54571fc721 chore(deps): update dev-dependencies 2026-07-28 05:22:32 +00:00
renovate[bot]andTink 47af445d78 chore(deps): update dependency ip-address to v10.2.2 2026-07-28 04:29:46 +00:00
renovate[bot]andTink f0334104ad chore(deps): update dependency undici@6 to v8.9.0 2026-07-28 04:22:31 +00:00
renovate[bot]andTink c93dad77ac chore(deps): update playwright to v1.62.0 2026-07-28 04:02:27 +00:00
renovate[bot]andTink 681249f879 fix(deps): update module github.com/yuin/goldmark to v1.8.5 2026-07-28 03:32:29 +00:00
renovate[bot]andTink cd11dd5272 chore(deps): update dependency tar to v7.5.22 2026-07-28 03:08:47 +00:00
renovate[bot]andTink e3e09a2a6a fix(deps): update module github.com/aws/smithy-go to v1.27.5 2026-07-28 02:32:38 +00:00
renovate[bot]andTink aee99c3ddd fix(deps): update tiptap to v3.29.0 2026-07-28 01:42:32 +00:00
kolaenteandkolaente 05e9bac9e7 test(ratelimit): assert exact statuses instead of absence of 500
Asserting only "not 500" let the test pass on a 404, 401 or 403, so it
would have kept passing if the public v2 endpoints broke another way.
Assert 200 per path plus the remaining budget counting down across them,
which is what proves both unauthenticated requests share one ip key.
2026-07-27 22:31:06 +00:00
kolaenteandkolaente f723a6f027 fix(ratelimit): key by ip when the configured kind is unknown
The default branch of the kind switch logged the misconfiguration and
then continued with an empty key, putting every request of the whole
instance into one shared bucket. Fall back to per-ip limiting instead,
so a typo in ratelimit.kind degrades to the "ip" behaviour.
2026-07-27 22:31:06 +00:00
kolaenteandkolaente 2c9e71a2b3 fix(ratelimit): don't panic on unauthenticated requests
With ratelimit.kind at its default value of "user", the rate limit
middleware logged the error from GetAuthFromClaims and then dereferenced
the nil web.Auth anyway. Every unauthenticated /api/v2 request produces
exactly that state, since v2 attaches the limiter to the single group
serving its public routes too - so enabling rate limiting turned
/api/v2/info, /api/v2/health and /api/v2/login into 500s. v1 is
unaffected because it splits its unauthenticated routes into their own
ip-keyed subgroups before the "user" limiter is attached.

Fall back to keying by IP, matching the "ip" kind and v1's
unauthenticated groups. Authenticated requests are unchanged.
2026-07-27 22:31:06 +00:00
renovate[bot]andTink 9617a9bee8 chore(deps): update dependency postcss to v8.5.23 2026-07-27 22:16:30 +00:00
renovate[bot]andTink 8eddd84beb chore(deps): update dependency undici@7 to v8.9.0 2026-07-27 21:29:47 +00:00
Frederick [Bot] 04170f6b8d chore(i18n): update translations via Crowdin 2026-07-28 00:22:34 +00:00
kolaente 1e081d34a7 chore: cleanup dead code 2026-07-27 23:17:46 +02:00
renovate[bot]andkolaente 3e45d53dba fix(deps): update dependency @sentry/vue to v10.68.0 2026-07-27 11:51:03 +00:00
renovate[bot]andkolaente b95b098409 chore(deps): update danielroe/provenance-action digest to b793421 2026-07-27 11:39:04 +00:00
renovate[bot]andkolaente 3361152015 chore(deps): lock file maintenance 2026-07-27 09:29:38 +00:00
renovate[bot]andkolaente 743e6c88cd chore(deps): update pnpm to v11.17.0 2026-07-27 09:29:16 +00:00
kolaenteandkolaente a59872be2f fix(config): apply deprecated service.jwtsecret to service.secret
service.secret had an unconditional random default, and viper's IsSet
reports defaults as set. The deprecation branch therefore always took the
"both keys are set" path and never copied service.jwtsecret over, so
instances configured with the old key silently got a fresh random secret
on every start — flaky 401s across replicas.

The random fallback is now generated after the config file and env are
read, so an empty service.secret unambiguously means "not configured".
2026-07-26 15:59:40 +00:00
TinkandGitHub a84863d6c3 fix(packaging): stop masking filesystem paths in the systemd unit (#3306) 2026-07-26 16:11:06 +02:00
kolaenteandkolaente e684ac06c5 ci(lint): forbid plain Sync in migrations via forbidigo
tx.Sync/tx.Sync2 in pkg/migration now fails lint; brand-new-table
migrations and the fresh-install initSchema carry an explicit nolint
2026-07-26 13:42:33 +00:00
kolaenteandkolaente ec9e6cfcc7 fix(mage): scaffold new migrations with partialSync
address pr-swarm finding: the make-migration template still emitted plain
tx.Sync, regenerating the index-wipe pattern for the next contributor
2026-07-26 13:42:33 +00:00
kolaenteandkolaente 79c25a94a1 docs(skill): ban plain tx.Sync on existing tables in the migration skill 2026-07-26 13:42:33 +00:00
kolaenteandkolaente 957b114724 fix(migration): recreate indexes dropped by partial-struct sync migrations
Recreates model-declared indexes that are missing, matching by column
set so converted databases with differently-named equivalent indexes
are left alone. Fails with an actionable message if duplicate values
block recreating a unique index.

Fixes #3244
2026-07-26 13:42:33 +00:00
kolaenteandkolaente 73bca9344a fix(migration): stop existing-table struct syncs from dropping indexes
xorm's Sync drops every index and unique constraint the synced struct
doesn't declare (#3244). Adds a partialSync helper (no drops) and
converts all 65 existing-table struct syncs to it — the four v2.4.0
migrations that wiped users/tasks indexes on every upgrade, plus every
historical one back to 2019, each a latent index wipe and a pgloader
2BP01 abort point for installs upgrading from versions predating it.
2026-07-26 13:42:33 +00:00
kolaente 0195322aab fix(db): ping the database on connect so failures report the real error
xorm connects lazily, so nothing touched the database until the ParadeDB probe
ran, and a connection failure surfaced as "could not check for paradedb
extension" followed by "Schema check failed: could not determine the current
schema" - which is how #3287 came in labelled as a schema bug. Ping right after
the engine is built and let the existing "Could not connect to db" callers report
it. The ParadeDB probe moves up alongside it so it no longer runs first.
2026-07-26 15:40:29 +02:00
kolaenteandkolaente aa62c00b78 test: guard parent_project_id in project responses
Route-level tests on both api versions, since the pseudo-projects only get
their parent through the read path. The v2 read body embeds models.Project and
Huma's $schema wrapper copies its fields into a generated struct, so a custom
MarshalJSON on the model would not have applied there — assert the raw JSON
instead of the struct.
2026-07-26 09:39:41 +00:00
kolaenteandkolaente 2e0cd81ce0 fix: always serialize parent_project_id for pseudo-projects
781ffac19 (GHSA-44v6-7fxq-vgf4) turned ParentProjectID into a *int64 with
`omitempty` so a write can tell an omitted field from an explicit 0 (detach,
admin only). That also dropped the key from every response where the pointer is
nil: the Favorites and saved-filter pseudo-projects, which are built in Go and
never set it, plus rows whose nullable parent_project_id column was never
backfilled. Clients parse the field as a plain int, so the whole project list
fails to decode (go-vikunja/app#295).

Keep the pointer and its three-way write semantics, but never hand out a nil:
normalize DB rows in AfterLoad and set the field where the pseudo-projects are
built.

Closes go-vikunja/app#295
2026-07-26 09:39:41 +00:00
renovate[bot]andTink 73925951ea chore(deps): update pnpm to v11.16.0 2026-07-26 08:44:30 +00:00
renovate[bot]andTink a08914de82 chore(deps): update dependency postcss to v8.5.22 2026-07-26 08:34:02 +00:00
renovate[bot]andTink 48ca7caee3 chore(deps): update dependency flatted to v3.4.3 2026-07-26 08:04:27 +00:00
renovate[bot]andTink 713aead8ce fix(deps): update dependency @fortawesome/vue-fontawesome to v3.3.3 2026-07-26 07:05:17 +00:00
renovate[bot]andTink a95cfb5f3e chore(deps): update dependency tar to v7.5.21 2026-07-26 07:05:16 +00:00
renovate[bot]andTink 1963d9b55b fix(deps): update module github.com/prometheus/client_golang to v1.24.1 2026-07-26 07:05:14 +00:00