Skip to content

calternal_plugin_files::user_bytes

Response headers for user bytes: every route that streams a file a user wrote (downloads, versions, thumbnails, public links, video) builds its headers here, so one rule decides what a browser may do with them.

Threat (#107): these bytes are served on the app’s own origin. A collaborator with edit access to a shared folder can plant report.html or chart.svg with script. If the owner opens its inline URL and the browser renders it as a page, the script runs as the owner and can call the API with the owner’s session. Three independent layers stop that:

  1. SANDBOX_POLICY on every response. sandbox gives a document an opaque origin and blocks script, so even a page that renders cannot use the session. default-src 'none' stops every fetch it could start. The image, media and inline-style sources keep a plain image, video or text view working when someone opens the URL directly.
  2. X-Content-Type-Options: nosniff, so a polyglot (a GIF header followed by HTML) is never sniffed into HTML.
  3. Active types (HTML, XHTML, XML and XSLT, JavaScript, and SVG outside an <img>) are never sent inline with their real type: they go out as text/plain; charset=utf-8, which shows the source. Quick Look reads text through fetch, so it sees the same bytes.

The policy is on attachments too: it costs nothing and covers a browser that ignores Content-Disposition.

The public link routes (public.rs: download, preview, thumbnail) use the same helpers; the link page head uses strip_unsafe_name_chars.

Source: crates/plugins/files/src/user_bytes.rs

pub enum Disposition

How the browser should treat the bytes.

Variants

  • Inline: Show in the page (Quick Look, <img>, <video>, note images).
  • Attachment: Save as a file.

Implements: Clone, Copy, Debug, PartialEq, Eq

pub fn requested(inline_query: bool, request: &HeaderMap) -> Self

inline when the request asks for it with ?inline=true (already parsed by the caller) or the x-calternal-inline: true header.

Source: crates/plugins/files/src/user_bytes.rs:39

pub fn content_disposition(disposition: Disposition, name: &str) -> String

A Content-Disposition value with an ASCII filename fallback and the exact UTF-8 name in filename* (RFC 6266, RFC 8187). No byte of the name can end the quoted string or the header: quotes, backslashes and non-ASCII become _ in the fallback and percent escapes in filename*.

Source: crates/plugins/files/src/user_bytes.rs:88

pub fn entity_tag(hash: &str, guessed: &str, served: &str) -> String

The entity tag for one representation of a file. A neutralized inline response (text/plain instead of HTML or SVG) is a different representation of the same bytes, so it gets a different tag. With one tag for both, a browser that cached the text answer for a navigation revalidates it for an <img> load, gets 304, and reuses the text (Chromium keeps one cache entry per URL and revalidates on a Vary mismatch).

Source: crates/plugins/files/src/user_bytes.rs:181

pub fn file_headers(
builder: Builder,
disposition: Disposition,
name: &str,
served: &str,
) -> Builder

The full header set for a user file: the served type (from served_type), the Content-Disposition for name, Vary: Sec-Fetch-Dest on inline responses (the type can depend on it), and guard.

Source: crates/plugins/files/src/user_bytes.rs:193

pub fn guard(builder: Builder) -> Builder

Adds the headers every user-bytes response carries, whatever its type: nosniff and SANDBOX_POLICY. Use it alone for bytes the server generated from user files (thumbnails, HLS) or whose type is fixed.

Source: crates/plugins/files/src/user_bytes.rs:168

pub fn guessed_type(name: &str) -> String

The type guessed from a file name (the same guess the Index stores).

Source: crates/plugins/files/src/user_bytes.rs:121

pub fn indexed_type(mime: Option<&str>, name: &str) -> String

Prefer the MIME value verified and stored by the Files Index. Name-based fallback is for legacy and generated paths with no indexed content (#851).

Source: crates/plugins/files/src/user_bytes.rs:127

pub fn served_type(guessed: &str, disposition: Disposition, request: &HeaderMap) -> String

The content type to send. An attachment keeps its type (the browser saves it). An inline active type becomes PLAIN_TEXT. SVG keeps its type only for an <img> load (Sec-Fetch-Dest: image), where the browser runs no script; a navigation, a fetch or an unknown browser gets text.

Source: crates/plugins/files/src/user_bytes.rs:143

pub fn strip_unsafe_name_chars(name: &str) -> String

Removes control and bidirectional format characters from a name, so invoice\u{202E}txt.exe cannot read as invoiceexe.txt. Download names and link previews share this rule.

Source: crates/plugins/files/src/user_bytes.rs:72

pub const PLAIN_TEXT: &str

The content type that replaces an active type on an inline response.

Source: crates/plugins/files/src/user_bytes.rs:35

pub const SANDBOX_POLICY: &str

The Content-Security-Policy for every response that carries user bytes.

Source: crates/plugins/files/src/user_bytes.rs:32