Skip to content

Set-Cookie rewrite in compose/seatable-server.yml has no effect; embedded bases show no images #93

Description

@christophdb

Summary

The Set-Cookie rewrite in compose/seatable-server.yml:64 never takes effect. Every cookie keeps the Django defaults, SameSite=Lax and no Secure, so a base embedded on another domain loads but shows no images or attachments.

caddy_0.header.Set-Cookie: '(.*) "$1; SameSite=None; Secure"'

Responses through Caddy and responses taken directly from the backend nginx are byte for byte identical, on HTTP/1.1 and HTTP/2 alike. It affects sessionid, dtable_csrftoken and saml_session.

SeaTable Cloud is not affected — cloud.seatable.io serves its cookies correctly with SameSite=None; Secure on the same 6.2.13. The difference is in the deployment configuration, not in the application.

Why it does not work

Caddy's headers handler applies response operations to the still empty header map before calling the next handler, unless the block is deferred. For set operations that makes no difference, which is why CSP, HSTS and X-Frame-Options all arrive correctly. A replace operation, however, finds no Set-Cookie to replace at that point — the reverse proxy sets it afterwards.

The generated configuration carries no deferred flag, as served by the Caddy admin API under /config/apps/http/servers/srv0/routes/0/handle/0/routes/0/handle/0. Adding it to that one handler at runtime is enough to make the rewrite work:

# as shipped
set-cookie: sessionid=…; HttpOnly; Max-Age=86400; Path=/; SameSite=Lax
# same handler, "deferred": true
set-cookie: sessionid=…; HttpOnly; Max-Age=86400; Path=/; SameSite=Lax; SameSite=None; Secure

What breaks

An embedded page carries its own token inline in the HTML (window.app.accessToken), so the frame and the data do not depend on cookies. Assets do. Images and attachments are served by dtable_asset_access, and can_access_asset() reaches for the Django session in three places:

check source
can_access_file_through_external_link request.session['external_link']
has_dtable_asset_cache_read_permission request.session session key
can_access_asset_through_external_app request.session['external_app']

The other way in, @bind_token_user, needs an Authorization header, which a browser cannot send for <img src=…>. So in a cross-site iframe the browser withholds the SameSite=Lax cookie — worse, it refuses to store it in the first place, so external_link never reaches the session at all. The same asset request, with and against without the session cookie:

with session cookie   200  image/png   size=7      image is served
without cookie        200  text/html   size=6174   "Permission denied."

Net effect for a self-hosted installation: an embedded base renders, the data appears, and every image and attachment stays blank.

Suggested fix

Move the cookie line into a header block of its own and defer it:

-      caddy_0.header.Set-Cookie: '(.*) "$1; SameSite=None; Secure"'
+      # Cookie attributes need their own deferred header block: a replace
+      # operation only takes effect while the response is being written.
+      caddy_0.header_1.defer: ""
+      caddy_0.header_1.Set-Cookie: '`^(.*?)(?:;\s*SameSite=[^;]*)?(?:;\s*Secure)?$` `$1; SameSite=None; Secure`'

Two things worth knowing before changing it:

Do not simply add defer to the existing block. defer applies to the whole block, and that block also sets the CSP. The routes /dtable/forms/*, /dtable/external-links/*, /dtable/external-apps/*, /apps/custom/* and /external-apps/* remove it again with header.-Content-Security-Policy, which only works because the CSP is set first. Deferring the whole block puts the CSP back afterwards and breaks the very embedding this is meant to fix.

The regex replaces rather than appends. With defer alone the result is SameSite=Lax; Secure; SameSite=None; Secure. That works — the last value wins per RFC 6265bis — but the anchored expression above produces a clean SameSite=None; Secure.

Verified on a 6.2.13 instance: cookies come out with SameSite=None; Secure, / keeps its CSP, and the embed routes stay CSP-free.

A better option, and what it depends on

Dropping the label and setting the attributes where they belong, in Django — SESSION_COOKIE_SECURE, SESSION_COOKIE_SAMESITE, CSRF_COOKIE_SECURE, CSRF_COOKIE_SAMESITE — removes the regex across all cookies, the ordering pitfall around defer, and puts the setting somewhere greppable instead of hiding it in a proxy label. Since SESSION_COOKIE_SECURE arrives with 7.0 through seafileltd/dtable-web#6291 anyway, that would be the consistent path.

Two things have to be decided along with it, otherwise that move trades one problem for another.

SameSite=Lax on sessionid is a real CSRF defence today. Relaxing it to None — which is what embedding needs — leaves only the Django CSRF token, and CSRF_TRUSTED_ORIGINS currently reads:

CSRF_TRUSTED_ORIGINS = ["https://*", "http://*", "chrome-extension://*"]

That trusts effectively any origin, so the origin check contributes nothing. Loosening SameSite without tightening this would fix the embedding and weaken CSRF at the same time. Whether CSRF_TRUSTED_ORIGINS is narrowed in the same step is a product decision, not a technical detail, and it is the reason this is worth deciding rather than just doing.

The Django settings do not reach every cookie. Several cookies are written by hand with no flags at all, so they bypass SESSION_COOKIE_* entirely:

# seahub/auth/middleware.py, seahub/saml/views.py, seahub/oauth/views.py and others
response.set_cookie('seahub_auth', request.user.username + '@' + api_token.key)
# seahub/dtable/views.py
response.set_cookie('access-token', access_token)

seahub_auth carries username@api_token, a bearer credential, and gets neither HttpOnly nor Secure. Observable on any instance with GET /accounts/login/?source=partner, which answers with Set-Cookie: REGISTRATION_SOURCE=partner; Path=/ — the same flagless call shape. Fixing the defaults without also passing secure, httponly and samesite at these call sites would leave the most sensitive cookie untouched.

Remaining limitation

Browsers that block third-party cookies outright rather than partitioning them — Chrome in incognito, Safari with ITP — will still not show the images after this fix. Firefox is fine, because Total Cookie Protection partitions and the whole exchange happens inside the one iframe. The durable answer would be to stop authorizing assets by cookie; the token is already in the page as window.app.accessToken.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions