523 Commits
Author SHA1 Message Date
kolaente 179ed7f204 fix(mcp): validate meta-tool arguments, hydrate from the read_one spec, bound error text 2026-08-29 02:10:17 +02:00
kolaente 684b50a1b7 fix(handler): keep the v1 pagination error texts and drop the dead branch 2026-08-29 02:10:17 +02:00
kolaente dffb57c023 fix(mcp): reject 2^63 floats and accept integral floats on pointer fields 2026-08-29 02:10:17 +02:00
kolaente 81bdfb9629 fix(mcp): require every writable field on updates without a read_one
projects_users_update and projects_teams_update can't be hydrated, so an
omitted permission silently downgraded the share to 0.
2026-08-29 02:10:17 +02:00
kolaente 0154597e4c fix(mcp): hydrate before update, validate the whole model, align the list envelope
Partial updates went to models that write a fixed Cols list, so
tasks_update {done:true} blanked description, priority and colour. Update
now reads the row first, like v2 AutoPatch. Also: integral floats accepted
for integer args, validation errors bounded to 200 runes per field, list
envelope renamed to match apiv2.Paginated, users_search shares it.
2026-08-29 01:42:20 +02:00
kolaente 00eb50b79c test(mcp): tighten catalog scope assertions and restore registry state after tests 2026-08-29 01:27:15 +02:00
kolaente f71fce7b99 refactor(mcp): drop the ToolSuffix alias and cut comment noise 2026-08-29 01:00:54 +02:00
kolaente 08ab38836b fix(mcp): apply the REST layer's guards in Dispatch
- page/per_page default and clamp like the REST handler; page < 1 dropped
  the LIMIT clause entirely
- config-gated resources are unreachable through do_action
- create/update run the model's valid: tag rules
- read_all returns {items, result_count, total_items, page, per_page} and
  strips emails from user rows
2026-08-29 00:48:55 +02:00
kolaente 03c1f0fc7f fix(mcp): gate users_search on scopes a token can actually hold
The users and projects_users_search groups don't exist; the routes collect
as other:users and projects:users_search, so PermissionsAreValid rejected
any real token and the tool was unreachable. Also strip emails on the
project-scoped search path.
2026-08-29 00:36:12 +02:00
kolaente be37d944f1 refactor(api-tokens): share one HasPermission lookup across caldav, feeds and mcp 2026-08-29 00:36:12 +02:00
kolaente 95bc56de8a refactor(mcp): drop unused RequiredCreate and duplicate op tests 2026-08-29 00:34:27 +02:00
kolaente 1f0ed6d3dc fix(mcp): require the parent id on parent-scoped listings
projects_views_read_all had no way to name a project, so it always ran
against project 0 and failed CanRead.
2026-08-29 00:34:27 +02:00
kolaente d914a561d6 fix(mcp): build the server per request instead of per session
Stateful streamable-HTTP cached the initialize request's context, so every
later call on a session ran as whoever opened it — any mcp:access token plus
a leaked Mcp-Session-Id gave full impersonation, and revoked tokens kept
working through their session. Stateless mode rebuilds the tool set from the
token on each request; resources register at startup instead of lazily.
2026-08-29 00:33:56 +02:00
kolaente 71634ba8c8 feat(mcp): expose expand on tasks_read_all 2026-08-29 00:05:16 +02:00
kolaente 2cba279af5 feat(mcp): add users_search tool for assignee and share discovery 2026-08-29 00:02:46 +02:00
kolaente 17d52933d7 fix(mcp): appease lint and pin user1's fixture token set
TestHumaAPIToken pinned user1's owned tokens to {1,2}, broken since the
MCP fixture tokens 9-11 landed; the exact set now includes them.
2026-08-28 23:56:04 +02:00
kolaente 09bf29acd5 feat(mcp): add find_action/do_action catalog for long-tail resources
Catalog-tier resources stay out of tools/list; agents discover them via
find_action (scope-filtered, schemas on demand) and invoke them through
do_action, which funnels into the same Dispatch path — schema validation
and the per-call scope re-check apply identically.

Wave 1: task labels, task relations (subtasks), team members, project
user/team shares, project views. Deliberately absent: api tokens,
webhooks, link shares, buckets and positions (v1 token scopes don't map
onto (group, op) permissions), saved filters (nested filter object).

Adds IdentityFields for records not addressed by their id (team members
go by team + username) and treats readOnly+param fields as arguments
(REST reads them from the URL; MCP has no URL).
2026-08-28 23:54:56 +02:00
kolaente c9f56c97cb feat(mcp): derive tool schemas and dispatch from model struct tags
Replaces the hand-written per-resource plumbing (input wrapper structs,
per-resource install functions, per-op Inputs maps) with a generic layer:

- schema.go reflects each op's input schema from the model's existing
  json/doc/readOnly/valid/minLength/param/query tags — the same contract
  the Huma-backed /api/v2 reads. URL-bound param fields become required
  JSON arguments; models without an exposed id (task assignees) are
  identified by their param fields.
- apply.go applies arguments presence-based: only keys the caller sent
  are written, so an explicit zero clears a field and an omitted key
  leaves it untouched — replacing the pointer tri-state wrappers.
- Registration is now a pure declaration (name, model, ops); the SDK's
  low-level AddTool with runtime-built schemas replaces the generic
  typed handlers, collapsing the six install functions into one loop.
- tasks_read_all now exists, backed by models.TaskCollection, exposing
  the filter/sort_by/order_by query surface.

inputs.go and its wrapper structs are deleted; behavior is pinned by the
unchanged mcp webtests.
2026-08-28 23:53:52 +02:00
kolaente 2904f0d4aa fix(mcp): allow update wrappers to clear booleans and numerics
copyByJSONTag previously skipped any IsZero value, which made it
impossible for tasks_update / projects_update to flip done from true
to false, reset priority/percent_done to 0, or unarchive a project.

A non-nil pointer src is now the unambiguous "caller supplied this"
signal: dereferenced values are written through even when zero, while
value-typed src fields keep the partial-update semantics. The
affected wrapper fields (Done, IsArchived, IsFavorite, Priority,
PercentDone, RepeatAfter, RepeatMode, BucketID,
CoverImageAttachmentID, ParentProjectID, Position) move to pointer
types so the JSON Schema still marks them optional.
2026-08-28 23:53:52 +02:00
kolaente 38d5ce1698 feat(mcp): expose remaining v1 resources via mcp tools
Registers tasks, labels, teams, task_comments and task_assignees through
the MCP tool surface, completing the v1 resource list from the plan:

  * tasks    : create / read_one / update / delete (read_all omitted;
               models.Task.ReadAll is a stub — TaskCollection is OOS)
  * labels   : full CRUD
  * teams    : full CRUD
  * tasks_comments  : full CRUD, install-time gated on
                      config.ServiceEnableTaskComments
  * tasks_assignees : create / read_all / delete only (REST exposes no
                      read_one or update)

Per-resource input wrappers carry the path-param fields (task_id,
user_id) explicitly so MCP callers can provide them as JSON args.
installToolsForToken fans out to one installer per resource; the
generics-bound addTool keeps per-(resource, op) call sites at compile
time. The api_tokens.yml fixture extends token 11 to cover the new
scopes; token count stays at 5 for user 1 so existing token-listing
tests are unaffected.

Integration tests per resource cover tools/list visibility, at least
one successful create or read_all, and a permission denial scenario.
2026-08-28 23:53:52 +02:00
kolaente 532b0665ad feat(mcp): enforce per-tool api token scopes
Filter MCP tool visibility and invocation by the requesting API token's
(group, permission) scopes. tools/list now returns only the tools the
token's APIPermissions authorise; tools/call additionally re-checks the
scope in the dispatcher as defence-in-depth, so a session created with
one token cannot be reused to invoke tools that token never had access to.

The per-session filter runs at session-init via the StreamableHTTPHandler
getServer factory (which the SDK calls once per session, before caching
the *mcp.Server). The dispatcher check runs on every tools/call and
returns ErrScopeDenied, which the AddTool wrapper renders as an IsError
tool result.
2026-08-28 23:53:52 +02:00
kolaente bf2a644a63 feat(mcp): expose projects via mcp tools
Wires the projects resource into the MCP server end-to-end. The five
project tools (create, read_one, read_all, update, delete) are now
visible in tools/list and dispatch through handler.Do* like the REST
layer.

- Add ProjectCreateInput / ProjectUpdateInput in inputs.go with
  jsonschema tags covering only the writable fields the model honours
  (title, description, identifier, hex_color, parent_project_id,
  position, is_archived, is_favorite); computed fields like Owner and
  MaxPermission are intentionally absent so the SDK-reflected schema
  stays narrow.
- Add resources.go with a sync.Once-guarded RegisterResources(), and an
  installTools helper that registers tools per (resource, op) on the
  *mcp.Server via a generic addTool[In inputAdapter] helper. The
  handler maps domain failures (permission denials, missing rows,
  validation) to IsError tool results per the SDK convention.
- Add DispatchTyped in dispatcher.go so the AddTool handler can hand a
  pre-unmarshalled wrapper to the dispatcher without a JSON
  round-trip. The existing Dispatch (raw JSON path) delegates to a
  shared dispatchPrepared.
- Wire RegisterResources() + installTools() into newServer() so each
  new MCP session inherits the static tool set.
- Add fixture token 11 (mcp:access + projects:*) for the full-scope
  integration tests; bump TestAPIToken_ReadAll's expected count.
- Refresh TestMCP_ToolsListEmpty into
  TestMCP_ToolsListReturnsRegisteredResources, asserting the five
  projects_* tools are present (Task 6 will introduce scope-based
  filtering of this list).
- Add pkg/webtests/mcp_projects_test.go covering tools/list,
  create/read_one/read_all/update/delete happy paths, schema-validation
  failure on missing required title, permission denial on a forbidden
  project, and nonexistent-id lookup.
2026-08-28 23:53:52 +02:00
kolaente 675ecc8d5a feat(mcp): add per-tool input wrappers 2026-08-28 23:53:52 +02:00
kolaente d998d3b6c9 feat(mcp): add resource registry and dispatcher
Define the Op bitmask, the Resource struct, the package-level Register
function, and the Dispatch entry point that future tasks will use to
expose CRUD resources over MCP. No resources are registered yet.

Op carries the CRUD-op identity, knows its api-token permission string
(matching apiTokenRoutes exactly), and knows its tool-name suffix.
Resource.Inputs maps each enabled op to a pointer-to-zero of the wrapper
type the dispatcher will allocate and unmarshal into. Register validates
the resource shape and populates a tool-name lookup table so the
dispatcher never has to string-parse names like task_comments_read_all.

Dispatch threads the user from ctx, allocates a fresh wrapper, unmarshals
arguments, asks the wrapper to copy itself onto a fresh model via the
inputAdapter seam (which Task 4 will populate with real implementations),
and forwards to the corresponding handler.Do* function. The Do* calls go
through a swappable crudFuncs struct so the unit tests can verify
dispatch routing without standing up the database.
2026-08-28 23:53:52 +02:00
kolaente a1d21dff6d feat(mcp): add streamable-http endpoint skeleton
Mount /api/v1/mcp (and /api/v1/mcp/*) inside the authenticated route
group. Reject JWT-authed requests with 401 (token-only policy), reject
API tokens without the mcp:access scope with 403, and propagate the
authed *user.User + *models.APIToken to r.Context() via typed keys so
downstream tool handlers can pull them out without depending on Echo.

The MCP protocol — JSON-RPC framing, Mcp-Session-Id management, SSE
streaming — is delegated to github.com/modelcontextprotocol/go-sdk
v1.6.1. tools/list returns {"tools": []} since no tools are registered
yet.
2026-08-28 23:53:52 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>kolaente
972eab94a2 fix(deps): update module github.com/yuin/goldmark to v2 (#3627)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/yuin/goldmark](https://redirect.github.com/yuin/goldmark)
| `v1.8.2` → `v2.0.0` |
![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fyuin%2fgoldmark/v2.0.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fyuin%2fgoldmark/v1.8.2/v2.0.0?slim=true)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/438) for more information.

---

### Release Notes

<details>
<summary>yuin/goldmark (github.com/yuin/goldmark)</summary>

###
[`v2.0.0`](https://redirect.github.com/yuin/goldmark/releases/tag/v2.0.0)

[Compare
Source](https://redirect.github.com/yuin/goldmark/compare/v1.8.5...v2.0.0)

- initial official release of v2

###
[`v1.8.5`](https://redirect.github.com/yuin/goldmark/releases/tag/v1.8.5)

[Compare
Source](https://redirect.github.com/yuin/goldmark/compare/v1.8.4...v1.8.5)

- fix:
[#&#8203;568](https://redirect.github.com/yuin/goldmark/issues/568)

###
[`v1.8.4`](https://redirect.github.com/yuin/goldmark/releases/tag/v1.8.4)

[Compare
Source](https://redirect.github.com/yuin/goldmark/compare/v1.8.3...v1.8.4)

fix: disable svg in data:image urls

###
[`v1.8.3`](https://redirect.github.com/yuin/goldmark/releases/tag/v1.8.3)

[Compare
Source](https://redirect.github.com/yuin/goldmark/compare/v1.8.2...v1.8.3)

**Full Changelog**:
<https://github.com/yuin/goldmark/compare/v1.8.2...v1.8.3>

- fix:
[#&#8203;556](https://redirect.github.com/yuin/goldmark/issues/556)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/go-vikunja/vikunja).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC40Ni4wIiwidXBkYXRlZEluVmVyIjoiNDQuNDYuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: kolaente <k@knt.li>
2026-08-27 20:34:29 +00:00
8656f03aae fix(openid): pin token endpoint auth method instead of probing (#3607)
Vikunja leaves `oauth2.Config.Endpoint.AuthStyle` at
`AuthStyleAutoDetect`, so `golang.org/x/oauth2` tries
`client_secret_basic`, retries with `client_secret_post` on *any* error,
and reports only the second error — turning every token endpoint failure
(a wrong client secret, say) into a bogus "the client registration does
not allow client_secret_post" complaint.

Style now comes from the provider's
`token_endpoint_auth_methods_supported` discovery value. Provider
advertises neither: keep autodetect, so nothing changes for providers
with a thin discovery document.

Side effect: one token request per login instead of two, no more
spurious failed-auth entry in provider logs on every successful login.

Reported at
https://community.vikunja.io/t/authelia-could-not-authenticate-against-third-party/4770

## How to verify

1. Register a Vikunja client in an OIDC provider that only accepts
`client_secret_basic` (Authelia: `token_endpoint_auth_method:
'client_secret_basic'`).
2. Configure that provider under `auth.openid.providers` in Vikunja, but
put a deliberately wrong value in `clientsecret`.
3. Log in through that provider and read the Vikunja log.
4. **Expected:** the error names the real problem — an `invalid_client`
/ "client secret did not match" message from the provider.
**Before this PR:** the error claimed the client registration does not
allow `client_secret_post`, which was never configured on either side.
5. Fix `clientsecret` to the correct value and log in again.
6. **Expected:** login succeeds, and the provider's access log shows a
single `POST /token` per login rather than a rejected one followed by an
accepted one.

---------

Co-authored-by: kolaente <k@knt.li>
2026-08-27 12:43:11 +00:00
kolaente 374045e5bf fix(projects): sub-resource permission checks no longer get the un-archive exception
Buckets, webhooks, backgrounds and task duplication checked write access
through Project.CanUpdate on a bare stub, whose zero IsArchived matched the
un-archive carve-out. With is_archived now set on descendants too, that
made whole archived subtrees writable. Use CanWrite instead.
2026-08-20 11:58:06 +02:00
kolaente 103a5d4635 refactor(projects): dedupe effective parent lookup, tidy error ordering and comments
Also adds a negative control to the structure-import archive test.
2026-08-20 11:58:06 +02:00
kolaente 5732a435a5 fix(migration): cascade archived state to descendants on structure import
Exports written before archiving cascaded carry unflagged children under
archived parents; with the column now authoritative nothing heals that.
2026-08-20 11:58:06 +02:00
kolaente 691e5c9e61 feat(migration): add planka migrator routes on v2
Credentials migrators are verified synchronously before the async migration is
queued (CredentialsChecker). Routes live on v2 only; the listener learns the
migrator via a route-free RegisterMigratorForEvents, which the oauth v2
registrar uses as well. Shared status/migrate registration moved to
migration_shared.go.
2026-08-20 08:45:42 +02:00
kolaente 0358a820df feat(migration): convert planka data to vikunja structure
Everything lands under a "Migrated from Planka" root project; projects with
several boards become a parent with one child per board. Lists become kanban
buckets (trash skipped), closed/archived cards are done, labels keep their
Planka colours, checklists, custom fields and link attachments are rendered
into the description, comments are prefixed with the author unless it is the
importing user. No assignees, memberships or other users' data.
2026-08-20 08:45:42 +02:00
kolaente e4d9aad870 feat(migration): fetch planka projects and boards
Pages archived cards and comments (Planka requires both cursor fields, pages
until empty with a hard cap, partial results are kept), reads base custom
field definitions from the projects payload and rejects Planka v1 payloads.
2026-08-20 08:45:42 +02:00
kolaente 86da987f8a feat(migration): add planka api client
Authenticates with an API key, a JWT or username + password (Planka checks
Bearer before X-Api-Key, so only one header is ever sent; downloads use the
accessToken cookie). The synchronous credential check has one 15s deadline and
no retries. Redirects to another host or to plain http are refused for the
api, followed without credentials for attachment downloads. Response bodies
are capped, url userinfo is stripped, pending login steps (totp, terms) and
non-planka endpoints are reported as distinct client errors (142xx).
2026-08-20 08:45:42 +02:00
kolaente 4655496e2e feat(migration): client-aware get and size-limited download helpers, bounded json decode
Migrators that talk to a user-supplied host need a client with a redirect
policy, a cap on response bodies and no retries on permanent errors
(utils.ErrDoNotRetry). Diagnostic body reads in the shared helpers are capped.
2026-08-20 08:45:42 +02:00
kolaente 3b00c8ed7e fix(migration): keep imported tasks done when they are placed in an imported bucket
Creating a done task puts it in the view's default done bucket; moving it into
the imported bucket flipped it back to open. The imported state (incl. done_at)
is restored in bulk after the task loop.
2026-08-20 08:45:42 +02:00
kolaente 716220a21e fix(migration): don't panic on events for unregistered migrators 2026-08-20 08:45:42 +02:00
kolaente f63d007056 feat(keyvalue): add PutWithTTL to the storage interface
Redis passes the ttl to SET; the memory backend tracks expiry per key and
treats expired keys as absent on every access, sweeping never-read keys
at most once a minute.
2026-08-19 17:29:35 +02:00
kolaente f79d1793b6 fix(keyvalue): escape glob metacharacters in redis ListKeys prefix
Pre-existing: gravatar DelPrefix passes a username into the SCAN pattern,
so a user named * could flush every user's cache.
2026-08-19 17:29:35 +02:00
kolaente 417b396623 fix(keyvalue): dedupe keys returned by redis SCAN
SCAN may return the same key more than once, which made prefix-based
counts such as the active users metric overreport.
2026-08-19 17:29:35 +02:00
kolaente bd1f95bb96 fix(migration): map exported assignees to the importing user instead of failing
Vikunja-file exports carry task assignees with user ids from the source
instance. On import those ids were looked up on the target instance and
permission-checked, which fails with "User does not have access to the
project" whenever the id belongs to someone else (e.g. importing a
self-hosted export into Vikunja Cloud). Since ba980b1b8 that error aborts
the whole import.

Match assignees against the importing user by email, then username, and
drop everyone else – foreign user ids have no meaning on this instance.

Fixes #3476
2026-08-19 15:10:01 +02:00
kolaente e007a1da70 fix(api/v2): don't require read_one for autopatch's internal GET
Huma's AutoPatch implements PATCH as an internal GET + PUT re-dispatched through
the router, so both legs re-enter the API token middleware. Scoping the GET leg
like a client request made every /api/v2 PATCH additionally demand the
resource's read_one permission, so a token scoped to tasks: update could never
patch a task.

Mark the re-dispatched requests with the route the client request was matched
against, and skip the scope check only for a GET leg that carries no query
string and resolves to that exact route. Anything looser is exploitable: echo
routes on the raw path while autopatch re-dispatches the decoded one, so an
encoded slash steers the unchecked leg onto a deeper route and an encoded
question mark smuggles a query onto it.

Fixes #3528
2026-08-19 13:01:59 +02:00
kolaente 044106508a fix: use caller's session in LoadFileMetaByID to prevent connection pool deadlock
Querying the global engine while the request already holds an open
transaction acquires a second pool connection. Under concurrent
attachment downloads (e.g. a kanban board full of image previews) all
pool connections end up held by transactions that each wait for an
extra connection that can never be freed, hanging every request until
restart.

The redundant meta load in the upload avatar provider is removed
entirely; the decoded image is all that path uses.
2026-08-16 17:57:52 +02:00
kolaente 937ee313b1 fix(migration): don't fail todoist migration on undownloadable attachments
Todoist returns opaque identifiers instead of urls in file_url for
attachments it does not host itself (mail attachments for example).
Passing those to the http client failed with "unsupported protocol
scheme" and aborted the entire migration.

Skip attachments without an http(s) url and log-and-continue when a
single download fails instead of failing the whole migration.
2026-08-04 21:21:05 +02:00
kolaente dbe664dec5 fix(security): reject non-user tokens in GetUserFromClaims
GetUserFromClaims read the id claim straight into User.ID without checking the
token type. A link share JWT carries the raw, positive share id in that claim
and reaches every authenticated route, so the only thing preventing
impersonation was the incidental absence of a username claim in link share
tokens. Adding one would have reintroduced the confusion with a positive id,
bypassing the GetID negation entirely.

AuthTypeUser moves to pkg/user, which parses the claims and cannot import
pkg/modules/auth, so the value exists once.
2026-08-04 13:40:11 +02:00
kolaente ba980b1b8e fix(migration): abort import on unexpected task creation error
Non-empty-title errors from task creation fell through silently and the
loop kept using a task with ID 0, attaching relations and labels to id
0. Return the error instead; empty titles keep being skipped.
2026-08-02 16:21:50 +02:00
kolaente 9b40ad9b46 fix(dump): reuse single stdin reader for all restore prompts
Each prompt created its own bufio.Reader over os.Stdin. The first
reader buffers ahead, so with piped input the later readers hit EOF
because the remaining lines were already sitting in the first
reader's buffer.
2026-07-31 20:57:30 +02:00
kolaente 79b241c211 fix(dump): skip directory entries in dump zip when restoring
Vikunja's own dumps only contain file entries, but manually repacked
dumps (e.g. with zip -r after editing the VERSION file) contain
directory entries like "database/", which failed database file name
validation and aborted the restore.

Fixes https://github.com/go-vikunja/vikunja/issues/3380
2026-07-31 20:57:30 +02:00
Mauandkolaente d86af5c9b6 fix(avatar): bounds-check size before narrowing int64 to int
GetAvatar converted a caller-supplied size int64 directly to int via
int(size) for imaging.Resize, with no check that the value was
non-negative or within int's range. size traces back to an
unauthenticated query parameter; on 32-bit builds a large int64
would silently truncate/wrap when narrowed, and a negative value
would feed straight into the resize call. Reject out-of-range values
before the conversion instead of letting it wrap silently.
2026-07-29 22:53:46 +02:00
Mauandkolaente eb31ee3780 fix(migration/csv): cap CSV import buffer to the configured upload limit
DetectCSVStructure, PreviewImport, and MigrateWithConfig each did
make([]byte, size) with size taken directly from the caller (an
uploaded file's declared size) with no upper bound, letting a
malicious or corrupt size value force an arbitrarily large
allocation. Cap it to the server's existing configured max upload
size before allocating.
2026-07-29 22:53:46 +02:00