481 Commits

Author SHA1 Message Date
Andrey Antukh
87c51090b1 Merge remote-tracking branch 'origin/staging' into develop 2026-08-26 20:14:51 +02:00
Pablo Alba
e1a2d0b932
🐛 Fix nitrate sso failure message (#11214) 2026-08-26 17:24:59 +02:00
Andrey Antukh
33f13f9bfd 🐛 Fix test formatting for get-team-invitation-token
AI-assisted-by: longcat-2.0
2026-08-26 14:08:41 +02:00
Andrey Antukh
4adfa5d2f2 🐛 Require edition permissions for get-team-invitation-token
The handler previously allowed any team member (including viewers) to
generate invitation tokens. Now requires at least edition-level
permissions (can-edit, admin, or owner).

Closes #11358

AI-assisted-by: longcat-2.0
2026-08-26 14:08:41 +02:00
Andrey Antukh
7079d33ae1
🐛 Enforce ownership check on tempfile bucket access (#11270)
* 🐛 Enforce ownership check on tempfile bucket access

The upload-tempfile RPC stores profile-id with tempfile objects, but
objects-handler never verified the requester was the owner. Any
authenticated user who knew the UUID could access the tempfile.

Add ownership check: tempfile bucket now requires the request's
profile-id to match the stored profile-id. Returns 404 on mismatch
(not 403) to avoid leaking object existence.

Legacy tempfiles without stored profile-id remain accessible to any
authenticated user for backward compatibility.

Closes #11269

AI-assisted-by: qwen3.7-plus

* ♻️ Extract tempfile-bucket constant and fix docstring indentation

Extract the 'tempfile' bucket string literal into a named constant
(sto/tempfile-bucket) to prevent typos and make future bucket renames
trivial. Updated 9 occurrences across 7 files.

Also fixed minor docstring indentation inconsistency in authenticated?
function.

AI-assisted-by: qwen3.7-plus

* ♻️ Refactor process-bucket! and authenticated? helpers

Replace case with cond in process-bucket! to properly resolve
sto/tempfile-bucket var from another namespace (case does not
evaluate qualified vars at compile time).

Redefine authenticated? in terms of request-profile-id to remove
duplicated lookup logic.

Closes #11269

AI-assisted-by: mimo-v2.5-pro
2026-08-26 13:05:36 +02:00
Andrey Antukh
b33213787e
🐛 Add accumulated storage byte quota for media uploads (#11038)
* 🐛 Add accumulated storage byte quota for media uploads

Add media-storage-bytes-per-team quote to prevent persistent DoS via
repeated uploads. The quota sums storage_object sizes from both
file_media_object (media + thumbnails) and team_font_variant
(otf/ttf/woff1/woff2). Default limit is 20 GiB per team, configurable
via PENPOT_QUOTES_MEDIA_STORAGE_BYTES_PER_TEAM.

The check is invoked in upload-file-media-object before processing,
looking up the team-id via file -> project -> team_id join.

AI-assisted-by: mimo-v2.5-pro

* 🐛 Fix deduplicated storage overcounting in media-storage-bytes-per-team quote

The SQL query sql:get-media-storage-bytes-per-team used UNION ALL
across six SELECT branches that each produce a so_id reference.
When deduplication causes multiple file_media_object or
team_font_variant rows to point at the same storage_object, UNION ALL
counts that objects size once per reference — inflating "used bytes"
and causing false :max-quote-reached rejections.

Change all five UNION ALL to UNION so that duplicate so_id values are
collapsed before the JOIN storage_object / SUM(so.size).

Add a test (media-storage-bytes-quote-deduped) that creates one
storage_object referenced by two file_media_object rows and asserts
the computed usage reflects the deduplicated physical size, not 2x.

AI-assisted-by: mimo-v2.5-pro
2026-08-26 08:33:20 +02:00
María Valderrama
5e1ced03ea
🐛 Fix missing warning when moving a team (#11357) 2026-08-26 08:08:40 +02:00
Andrey Antukh
d655aa9c63 Merge remote-tracking branch 'origin/staging' into develop 2026-08-25 20:46:47 +02:00
Andrey Antukh
b79680eeb7
🐛 Fix asset 404 for unauthenticated share-link viewers (#11342)
PR #11036 added a per-request permission check to the file-media asset
endpoints (/assets/by-file-media-id/:id and the /thumbnail variant) using
bfc/get-file-permissions. Anonymous share-link viewers were then rejected
because they have neither a session nor an access token, the asset URL
carries no share context, and the 2-arg get-file-permissions short-circuits
to nil when profile-id is nil.

Make the asset endpoints share-link aware, mirroring how get-view-only-bundle
already authorizes the same scenario: read the share-id from the query
string, validate it as a UUID, and call the 3-arg perms/get-file-read-permissions
(which chains the existing 2-arg bfc lookup, the bfc share-link fallback,
and the Nitrate org-owner fallback). On the frontend, extend
cf/resolve-file-media with an optional share-id arg and pass it from the
WASM viewer render path using the share-id already present in
[:viewer-local :share-id]. Non-viewer call sites (workspace, clipboard,
code-gen) keep the original URL shape because the new arg defaults to nil.

Closes #11338

AI-assisted-by: minimax-m3
2026-08-25 13:10:55 +02:00
Andrey Antukh
81c3b3cd56
📎 Update copyright name on file header (#11346) 2026-08-25 11:55:10 +02:00
Andrey Antukh
a83a5e2eff Merge remote-tracking branch 'origin/staging' into develop 2026-08-25 09:41:31 +02:00
Andrey Antukh
52573be074
🐛 Enforce share-link page scope in get-page and get-file-fragment RPC commands (#11284)
The get-page RPC command did not validate that the requested page-id was within the share-link's authorized :pages set, allowing share-link holders to read out-of-scope pages. The get-file-fragment command had the same issue.

This fix adds page scope validation to get-page, rejecting requests for pages not in the share-link's :pages set with a :not-found error. For get-file-fragment, share-link access is denied entirely as fragments lack direct page-id mapping.

The fix aligns these commands with the existing correct behavior in get-view-only-bundle, which already filters pages based on share-link scope.

Closes #11281

AI-assisted-by: qwen3.7-plus
2026-08-25 09:37:42 +02:00
Andrey Antukh
64a52d6b04 Merge remote-tracking branch 'origin/staging' into develop 2026-08-23 21:01:31 +02:00
Andrey Antukh
47d599fe34
Persist binfile manifest and emit workspace audit events (#11106) (#11138)
Persist binfile manifest metadata in file_data on import so file
statistics are available at open-workspace time. Emit a new
open-workspace-file audit event enriched with file statistics:
page count, shape count, component count, linked libraries,
design tokens, and whether the file is a shared library.

Closes #11106

AI-assisted-by: mimo-v2.5-pro
2026-08-21 14:37:21 +02:00
Andrey Antukh
7c85837290
🐛 Fix session invalidation on logout to prevent token replay (#11317)
Logout only cleared the auth-token cookie but never deleted the
server-side row because delete-fn read ::id which wrap-authz no longer
sets since 363b4e3778. Make delete-fn delete via ::session/:id attached
by wrap-authz so replayed tokens are rejected (CWE-613, GHSA-mj9f-5cwq-7p3q).

Add regression tests covering invalidation, idempotency and isolation
of other sessions. Fix verified with Red→Green TDD and full backend
suite (677 tests).

Closes #11316

AI-assisted-by: muse-spark-1.2-contributor
2026-08-21 12:55:24 +02:00
Andrey Antukh
dd4a163217
🐛 Remove internal error details from HTTP error responses (#11288)
* 🚑 Remove internal error details from HTTP error responses

PostgreSQL exceptions, I/O exceptions, and unhandled errors were
leaking raw database messages (table names, constraint names,
SQLSTATE codes), filesystem paths, and internal exception details
to API clients via :hint, :state, and :path response fields.

Remove these fields from server-error responses while keeping
full error context in server-side logs for operators.

Closes #11287

AI-assisted-by: mimo-v2.5-pro

* 🚑 Strip internal fields and map PG errors to safe messages

Complete the security fix for GHSA-r8wx-23q6-w3gf by addressing
the incomplete redaction found in code review.

Add strip-internal-fields helper to dissoc :hint, :state, :path,
and :context from error response data in three handlers that
previously passed raw ex-data through to clients:

- handle-error :internal
- handle-exception :default (else branch)
- handle-error :assertion (else branch)

Add pgsql-state->message to map PostgreSQL SQLSTATE codes to safe,
client-facing messages (e.g. 23505 → "A conflicting entry already
exists") instead of returning raw PG error text. Include :message
in all PSQLException response branches.

Add regression tests asserting :hint, :state, :path, :context are
absent from responses for :internal and unhandled ex-info errors.

Closes #11287

AI-assisted-by: mimo-v2.5-pro

* 🚑 Keep :hint in error protocol, fix unsafe sources

Refine the security fix based on code review feedback.

Keep :hint as part of the error protocol — it is essential for
controlled error communication. Remove it from strip-internal-fields
(which now only strips :state, :path, :context).

Fix the actual sources of unsafe :hint values:

- http/middleware.clj: replace (ex-message cause) with safe static
  strings for IllegalArgumentException, RequestTooBigException, and
  EOFException. These :validation errors return ex-data verbatim
  to clients, so raw exception messages were leaking internals.

- PSQLException handler: use :hint instead of :message for the
  SQLSTATE-mapped messages, staying consistent with the error
  protocol.

Update tests to assert :hint is present (with safe static values)
in :internal and unhandled ex-info responses, and absent only from
bare RuntimeException and IOException responses.

Closes #11287

AI-assisted-by: mimo-v2.5-pro
2026-08-21 12:43:23 +02:00
Alejandro Alonso
2dcf1a8a0a Merge remote-tracking branch 'origin/staging' into develop 2026-08-20 09:15:03 +02:00
Andrey Antukh
209aea8365
🐛 Add proper ownership check on managing/deleting shared link on a file (#11290)
* 🐛 Add ownership check to share-link deletion

The delete-share-link RPC command only verified file-level edit
permission but did not check if the caller owned the share-link.
This allowed any file editor to delete share-links created by
other users, disrupting collaborative workflows.

The fix adds an ownership check that allows deletion only by:
- The share-link creator (owner-id matches profile-id)
- File admins (is-admin permission)
- File owners (is-owner permission)

Implemented using TDD:
- RED: Test demonstrates IDOR vulnerability (editor can delete)
- GREEN: Ownership check prevents unauthorized deletion
- All existing tests continue to pass

Closes #11289

AI-assisted-by: qwen3.7-plus

* 🐛 Add test coverage for share-link deletion escape hatches

Address code review feedback for PR #11290:

- Add test for editor deleting their own share-link
- Add test for admin deleting editor's share-link
- Add test for owner deleting editor's share-link
- Remove redundant :is-owner check (already included in :is-admin)
- Add clarifying comment about :is-admin including :is-owner

Closes #11289

AI-assisted-by: qwen3.7-plus
2026-08-19 18:26:35 +02:00
Marina López
ddc98bdd47
Add sso events (#11265) 2026-08-19 07:58:44 +02:00
Andrey Antukh
e72c1869eb
🐛 Validate version parameter in import-binfile (#11107)
Restrict version parameter to supported values (1 or 3) via schema
validation instead of accepting any integer. Add content-based format
detection when version is not provided, using bfc/parse-file-format
to inspect file magic bytes.

Closes #11105

AI-assisted-by: qwen3.7-plus
2026-08-18 15:13:49 +02:00
Andrey Antukh
3be07ccced
🐛 Add minimum validation for total-chunks in upload session (#11104)
The create-upload-session RPC method accepted total-chunks values of 0
or negative numbers without validation, creating inconsistent session
state. Add {:min 1} constraint to the schema to reject invalid values
at input validation.

Closes #11103

AI-assisted-by: qwen3.7-plus
2026-08-18 14:37:51 +02:00
Andrey Antukh
73c0668877
🐛 Verify read access on source file in clone-file-media-object (#11090)
The clone-file-media-object RPC command only checked edit permissions
on the destination file. The source media object was fetched directly
by UUID without verifying the caller had access to the file that owns
it.

This fix adds a read permission check on the source file before
cloning. If the caller lacks read access to the source file, the
operation fails with :not-found to avoid leaking information about
the existence of files/media the caller cannot access.

Closes #11087

AI-assisted-by: qwen3.7-plus
2026-08-18 14:36:50 +02:00
Andrey Antukh
367e4d534c
🐛 Scope assemble-chunks session lookup to profile-id (#11012)
Prevent BOLA in chunked upload assembly by verifying session
ownership. The assemble-chunks function now requires a profile-id
parameter and scopes the upload_session lookup accordingly, matching
the pattern already used by upload-chunk.

All three callers (assemble-file-media-object, create-font-variant,
import-binfile) updated to pass the authenticated profile-id.

AI-assisted-by: mimo-v2.5-pro
2026-08-18 14:36:29 +02:00
Jan Kahmen
162a381aed
🐛 Apply the asset attachment disposition on the s3 backend too (#10989)
29dbf9ab1 marks non public buckets as attachments, which works on the fs
backend because nginx applies those headers to the internally redirected
response. On the s3 backend the handler answers 307 and the client then
fetches the bytes from the object store, so the header set on the redirect
does not reach the response that carries the object.

Sign the disposition into the presigned url as well, so the object store
returns it. It is only signed when the bucket is not public, so urls for
inline served objects are unchanged.

Also cover the disposition in the handler tests, for the non public buckets
and for the public ones that stay inline.

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-18 13:33:24 +02:00
Andrey Antukh
296dd748bd Merge remote-tracking branch 'origin/staging' into develop 2026-08-18 13:19:34 +02:00
Andrey Antukh
5b4a5776cb 🐛 Prevent nil theme in profile updates
Omit nil optional profile fields before frontend schema validation and RPC persistence. Preserve omitted language and theme values in backend updates, and add regression coverage for partial profile saves.

AI-assisted-by: gpt-5.6-luna
2026-08-18 11:18:44 +00:00
Gennadiy Ivashchenko
7f2dc66e86
🐛 Preserve public URI subpath in asset download URLs (#11234)
Join asset download paths relative to PENPOT_PUBLIC_URI so temporary
exports and binary file downloads retain configured subpaths.

Add regression coverage for both URL generation paths.

AI-assisted-by: gpt-5.6-sol
2026-08-17 23:44:43 +02:00
Andrey Antukh
8acb92b782
🐛 Normalize fractional rate-limit reset durations (#11254)
Round bucket reset intervals up to whole milliseconds before adding them to an instant. This prevents Clojure ratios from reaching duration conversion and disabling rate limiting for the request.

Add a regression test for a refill rate that produces fractional milliseconds.

Closes #11253

AI-assisted-by: gpt-5.6-luna
2026-08-17 15:27:06 +02:00
Pablo Alba
ed04d509ed
🐛 Fix bad managed error on backend sso failure (#11247) 2026-08-17 12:40:18 +02:00
Andrey Antukh
29dbf9ab12
🐛 Validate content-type on management upload endpoints (#11026)
Add media type validation to upload-tempfile and upload-org-logo
management endpoints. Both stored user-supplied mtype without
checking against an allowlist. Only image types and PDF are
permitted. Non-public bucket assets now also carry
Content-Disposition: attachment to prevent inline rendering.

AI-assisted-by: mimo-v2.5-pro
2026-08-17 12:13:56 +02:00
Andrey Antukh
5efd9cc3c5
🐛 Prevent admins from granting owner role in team invitations (#11099)
Add role-ceiling check to create-team-invitations and
update-team-invitation-role methods. These RPC methods allowed
team admins to grant or elevate invitations to :owner role,
bypassing the protection that exists in update-team-member-role.

The fix replicates the existing check from update-team-member-role:
reject promotion to :owner when the caller is not an owner.

Closes #11098

AI-assisted-by: qwen3.7-plus
2026-08-17 11:37:51 +02:00
Andrey Antukh
c688cba8d8
🐛 Mock DNS resolution in SSRF tests for environments without public DNS (#11040)
The validate-url-allows-public-{https,http} tests relied on real DNS
resolution of example.com, which fails in containers without public
DNS access. Mock resolve-host to return a known public IP, consistent
with the pattern used by other tests in the same file.

AI-assisted-by: mimo-v2.5-pro
2026-08-17 11:21:25 +02:00
Pablo Alba
350dc14632
🐛 Show a specific error on nitrate reused activation code (#11236) 2026-08-14 09:33:58 +02:00
María Valderrama
c7f036bed0
🐛 Fix organization invitation schema validation for logo URI (#11238) 2026-08-13 13:15:59 +02:00
María Valderrama
d4294bbf1e
🐛 Fix missing membership check in create-team (#11166) 2026-08-11 09:13:13 +02:00
Marina López
0fd2a9d26f
🐛 Secure organization invitation creation (#11164) 2026-08-10 13:45:15 +02:00
Marina López
5d2cb22966
Fetch team organization in a single batch (#11140) 2026-08-10 13:43:27 +02:00
Andrey Antukh
2f04fcddbf
🐛 Invalidate all sessions on profile deletion (#11115)
When a profile is deleted, only the current session was being
invalidated. Other active sessions on different devices remained
functional until the background cleanup task completed.

Add session/invalidate-all helper that deletes all sessions for
a profile by profile_id, and call it from delete-profile before
the response transform. This ensures immediate access revocation
across all devices when an account is deleted.

Closes #11114

AI-assisted-by: qwen3.7-plus
2026-08-07 13:44:13 +02:00
Andrey Antukh
399b00b86d
🐛 Add permission checks to WebSocket subscription handlers (#11054)
* 🐛 Add permission checks to WebSocket subscription handlers

Check file and team read permissions before allowing WebSocket
subscriptions to prevent resource enumeration via presence
notifications.

AI-assisted-by: mimo-v2.5-pro

* 🐛 Fix random backend test failure
2026-08-07 11:24:24 +02:00
Álvaro Tejero Cantero
bc9319eac5 🐛 Port the foreign-font-id test to the uploads API
`create-font-variant-rejects-foreign-font-id` sends `:data`, which
`schema:create-font-variant` no longer accepts: the same commit that
added the test documents that param as removed in 2.18 in favour of
`:uploads`. Both of the test's requests are therefore rejected by params
validation before they reach `check-font-team-ownership!`, which is the
thing the test exists to check. It asserted nothing about ownership and
failed three assertions.

Upload the font through `upload-font-chunked!`, the helper the other
tests in this namespace already use, and pass the session id in
`:uploads`.

`backend-tests.rpc-font-test` is 16 tests, 172 assertions, 0 failures
with this applied.

AI-assisted-by: mixed models
2026-08-07 09:10:04 +02:00
Andrey Antukh
9875db2f82 🐛 Fix media-remote-test falling back to real config in REPL
The info-service-uri-not-configured test used config-get-mock with an
empty map, which falls back to cf/config for missing keys. In a REPL
with real config, media-processing-service-uri is set, causing the code
to attempt an HTTP call instead of raising the expected error.

Use (constantly nil) to ensure cf/get always returns nil, matching the
test intent of simulating an unconfigured service URI.

AI-assisted-by: mimo-v2.5
2026-08-06 20:53:58 +02:00
Andrey Antukh
0702363b5c
🐛 Validate font-id team ownership in create-font-variant (#11014)
Prevent cross-team font injection by checking that when a font-id
already has variants, they belong to the same team. This closes a
BOLA gap where a user with team edit permissions could create a
font variant referencing a font-id from another team.

AI-assisted-by: mimo-v2.5-pro
2026-08-06 18:32:00 +02:00
Elena Torró
688c69b478
🐛 Fix import-binfile schema test (#11118) 2026-08-06 17:05:41 +02:00
Andrey Antukh
81e44afbe3
🐛 Add backend password validation with complexity rules and dictionary check (#11059)
* 🐛 Add backend password validation with complexity rules and dictionary check

Enforce minimum 8-character password length, require at least 1 lowercase
letter, 1 uppercase letter, 1 digit, and 1 special character, and reject
common passwords using Passay library with a 10k-entry wordlist from
SecLists during registration and password change flows.

AI-assisted-by: mimo-v2.5-pro

*  Improve user feedback

When the password is invalid, the user now gets extra indications to make it stronger, so it can be valid.

* 🐛 Fix remove unneeded common password check

The dictionary check is only relevant for passwords that meet all other requirements, but all 10,000 common passwords would fail the character requirements, so this check is not needed

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-08-06 10:17:14 +02:00
Andrey Antukh
b6656ee8dd
🐛 Enable SSRF check for organization SSO provider (#11064) (#11065)
Remove :skip-ssrf-check? true from prepare-organization-sso-provider so
SSRF protection is active when validating organization SSO configs.

The endpoint is already protected by shared-key authentication
(admin-console), but enabling SSRF protection prevents potential misuse
of internal network resources if the shared key were ever compromised
(defense-in-depth).

Add test prepare-organization-sso-provider-does-not-skip-ssrf-check to
verify the SSRF check is not skipped.

AI-assisted-by: qwen3.7-plus
2026-08-05 21:54:33 +02:00
Andrey Antukh
c4dd04353f 🐛 Sanitize SVG files on upload to prevent XSS
Add sanitize-svg function that removes dangerous elements and attributes:
- script tags
- foreignObject elements
- Event handler attributes (onload, onmouseover, etc.)
- javascript: URLs from href/xlink:href attributes

Apply sanitization in process-main-image before storing SVG files.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 21:52:59 +02:00
Andrey Antukh
bf62e59f73
🐛 Add cooldown to prevent duplicate invitation emails (#11063) 2026-08-05 17:53:15 +02:00
Andrey Antukh
5906312dff
🐛 Normalize error response on duplicate file ID (#11050)
Capture unique constraint violation in insert-file! and return
generic :not-found error instead of propagating raw PostgreSQL
exception, preventing file existence oracle.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:52:14 +02:00
Andrey Antukh
25066c2f46
🐛 Require file read permissions for asset endpoints (#11036)
Add authorization check to generic-handler in assets.clj so that
/assets/by-file-media-id/:id and its /thumbnail variant verify the
requesting profile has read access to the parent file. Return 404
(not 403) when access is denied to avoid confirming existence.

Also switch get-file-media-object from db/get to db/get* so that
non-existent media objects return nil instead of raising.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:44:01 +02:00
Andrey Antukh
3d176d5390
🐛 Restrict webhook creation/edit/delete to team members only (#11029)
* 🐛 Restrict webhook edit/delete to team members only

Remove the creator-id fallback from get-webhooks-permissions.
Previously, the webhook creator could always edit/delete their
webhook even after being removed from the team. Now can-edit
comes from team role only — removed users get :not-found.

Webhooks are NOT deleted on member removal; the team owns them
and team admins/owners manage them.

AI-assisted-by: mimo-v2.5-pro

* 🐛 Restrict webhook creation to team editors

Use team role check (check-edition-permissions!) for create-webhook
instead of the custom check that allowed any team member to create
webhooks via creator-id self-match override.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:42:49 +02:00