Documentation menu

Security

Render untrusted text through a normal template hole:

// src/greeting.ts
import { html } from '@gyral/core';

export const greeting = (name: string) => html`<p>Hello, <bdi>${name}</bdi>.</p>`;

Gyral writes child strings as text and escapes them in server output. Attribute values are escaped too. This protects the HTML boundary; it does not decide whether a URL, CSS value, request or uploaded file is safe for your application.

Trusted markup and URLs

raw() accepts trusted author HTML. It does not sanitize it. Never pass a user's comment, translation, query parameter or network response to raw() without a separately chosen sanitization policy. Use ordinary template holes for text and explicit elements for rich text.

For links from untrusted data, parse the URL at the boundary and allow only the schemes and origins your application needs. Escaping href cannot make a javascript: URL safe. Treat interpolated CSS, page styles and head scripts as trusted author code too. Follow the OWASP XSS prevention guidance when choosing allowed contexts and sanitizers.

Form requests and CSRF

Authenticate and authorize each state-changing request on the server, and validate the submitted data there even when the browser already validated it. Browser validation improves the interface; it is not an authorization boundary.

For a cookie-authenticated app, use your server's CSRF protection. Gyral's HTTP driver can carry a token supplied in the document:

// src/api.ts
import { csrfFromMeta, makeHttpDriver } from '@gyral/http';

export const api = makeHttpDriver({ headers: csrfFromMeta('csrf-token') });

The server renders the session's token in a meta element named csrf-token and validates the x-csrf-token request header. A native no-JavaScript form post needs the server's corresponding hidden-field token mechanism. Restrict this driver to your own trusted API; do not send a token to arbitrary destinations. SameSite cookies are useful defense in depth, but are not a universal replacement for server CSRF checks. See OWASP's CSRF guide.

Hydration seeds and secrets

SSR sends model state, declared props and store state to the browser so hydration can resume. Those values are public to anyone who receives the page. Keep credentials, private backend data and privileged tokens out of them. Use JSON-safe values; functions, DOM nodes and cyclic objects cannot be faithfully restored. Gyral diagnoses unsafe seed values and escapes JSON for its HTML context; escaping does not make a secret private.

defineForm(schema, { secret: [...] }) prevents named fields from being echoed in rejected form values. Password-like fields are secret by default. This does not redact arbitrary copies you put in state, application logs or your own error payloads. Return only the information the browser needs. See Forms.

A strict Content Security Policy

The SSR package can add hashes for component and page styles while preserving your other directives:

// server/page.ts
import { html } from '@gyral/core';
import { renderPage } from '@gyral/ssr';

export const response = renderPage({
  title: 'Account',
  body: html`<main><h1>Account</h1></main>`,
  csp: {
    directives: {
      'default-src': "'self'",
      'script-src': "'self'",
      'object-src': "'none'",
      'base-uri': "'none'",
      'frame-ancestors': "'none'",
      'trusted-types': 'gyral',
      'require-trusted-types-for': "'script'",
    },
  },
});

Where Trusted Types exist, Gyral parses templates through the policy named gyral. That policy accepts trusted author markup; it is not a sanitizer. Allow its name in your CSP. If the page refuses the policy, development warns; an enforced HTML sink still rejects the operation. Support varies by browser; keep CSP's other protections in place.

Style hashes from the build

renderPage({ csp }) hashes the styles of the components imported by the server. A server or proxy that only builds the header, without importing your components, can take the hashes from the build instead. After the client build (gyralVitePreset({ components: true })), and before .vite is deleted, writeComponentStyleManifest from @gyral/ssr/static imports the app's component modules in a separate Node process and writes their real <style> hashes to a JSON file:

// scripts/style-manifest.ts
import { writeComponentStyleManifest } from '@gyral/ssr/static';

await writeComponentStyleManifest({
  root: '.',
  clientDir: 'dist/client',
  outFile: 'dist/server/component-styles.json',
});

The file is { "schema": 1, "hashes": ["'sha256-…'"] }. The server passes the hashes as componentStyleHashes, to contentSecurityPolicy() or renderPage({ csp }):

// server/csp.ts
import { readFileSync } from 'node:fs';
import { contentSecurityPolicy } from '@gyral/ssr';

const manifest = JSON.parse(readFileSync('dist/server/component-styles.json', 'utf8')) as {
  readonly schema: 1;
  readonly hashes: readonly string[];
};

export const csp = await contentSecurityPolicy({
  componentStyleHashes: manifest.hashes,
  directives: { 'default-src': "'self'", 'script-src': "'self'", 'object-src': "'none'" },
});

The build runs your component modules, so they must be safe to import in Node; split a browser-only module from its component definition first. Pass vite with aliases or plugins the modules need. Ship the JSON with the HTML and assets of the same build, and regenerate it with every release: an old hash blocks new styles.

On a static host, deliver CSP as an HTTP response header where the host allows it. A <meta http-equiv> policy is the fallback for hosts that can't set headers, and it can't set frame-ancestors (Deploying). Review the SSR CSP options and style-attribute policy. Test the production page under its actual headers rather than relying on the development server.

Uploaded files

Use FormData so the browser generates the multipart boundary; do not set its content type by hand. On the server, enforce file count and size limits, validate content, and control storage names and download permissions. A filename or MIME type supplied by the client is untrusted. The file-upload recipe shows the transport boundary.