Architecture And Boundaries

QIP puts a narrow byte boundary inside a normal application.

The host stays responsible for routing, storage, auth, product workflow, and platform APIs. QIP components do small deterministic work behind a WebAssembly boundary: the host gives them bytes, they return bytes, and they do not get ambient access to the machine around them.

This page compares that shape with traditional server rendering and Next.js-style rendering, then looks at each through a security boundary lens.

Legend #

[trusted]       code that can use app privileges
[sandboxed]     code behind a narrower runtime boundary
-->             bytes, events, or HTTP messages passed explicitly
xxx             ambient access that code can use directly

Ambient access means things like filesystem, network, environment variables, secrets, clocks, process globals, database clients, or installed packages.

Traditional Web Page #

A traditional server-rendered page usually has one main application trust boundary.

Browser
  |
  | HTTP request
  v
+----------------------------------------------------------+
| [trusted] Web app process                                |
|                                                          |
|  router -> controller -> template -> HTML response        |
|                                                          |
|  app code xxx database                                   |
|  app code xxx filesystem                                 |
|  app code xxx environment/secrets                        |
|  app code xxx package graph                              |
+----------------------------------------------------------+
  |
  | HTML/CSS/JS
  v
Browser sandbox

This model is direct and easy to deploy. The tradeoff is that templates, helpers, plugins, and dependencies often run with the same authority as the app process. If a library is compromised, or generated code is wrong, it may be able to do whatever the app process can do.

The browser is still a real sandbox, but the server-side rendering work happens before the browser boundary.

Next.js-Style Rendering #

Next.js splits rendering across build time, server time, and browser time. The exact path depends on route mode, caching, server components, client components, actions, and API routes, but the broad shape looks like this:

Build time
------------------------------------------------------------
source + npm packages + config + env
  |
  v
+----------------------------------------------------------+
| [trusted] framework/build process                         |
|                                                          |
|  compile routes, bundles, assets, server code             |
|  build code xxx filesystem                               |
|  build code xxx package graph                            |
|  build code xxx environment                              |
+----------------------------------------------------------+

Request time
------------------------------------------------------------
Browser
  |
  | HTTP request
  v
+----------------------------------------------------------+
| [trusted] Next/server runtime                             |
|                                                          |
|  route -> server render/data/API/action -> response       |
|                                                          |
|  server code xxx database                                |
|  server code xxx filesystem                              |
|  server code xxx environment/secrets                     |
|  server code xxx package graph                           |
+----------------------------------------------------------+
  |
  | HTML + data + JS bundles
  v
+----------------------------------------------------------+
| Browser sandbox                                           |
|                                                          |
|  [sandboxed by browser] client components + hydrated UI   |
|  browser code xxx user page state                         |
|  browser code xxx allowed web APIs                        |
+----------------------------------------------------------+

This model is powerful because one framework coordinates routing, rendering, caching, data loading, and client hydration. It also means a lot of application behavior sits inside the framework and package graph. The server-side parts are trusted application code. They need access to secrets and databases, so they cannot be treated as isolated transforms.

The browser/client split is useful, but it is not the same as isolating a server-side content transform from the server process.

React Server Components are a useful example of this boundary. In December 2025, React disclosed CVE-2025-55182, an unauthenticated remote-code-execution vulnerability in React Server Components. Next.js documented the downstream App Router impact as CVE-2025-66478. The lesson is not that server components are uniquely bad. The lesson is that server-side rendering protocols run inside the trusted server boundary. When that boundary is crossed incorrectly, the blast radius can include application secrets, database access, and the host process.

Browser Trust Boundaries #

Most web security bugs come from confusing three different things:

Cookies, sessions, bearer tokens, origins, HTML, and JavaScript each sit on different parts of that map.

User browser
  |
  | request to https://app.example
  | browser may attach cookies for app.example
  v
+----------------------------------------------------------+
| [trusted] app.example server                             |
|                                                          |
|  session cookie -> account identity                      |
|  CSRF token/origin checks -> request intent              |
|  HTML escaping/sanitizing -> code/content boundary        |
+----------------------------------------------------------+
  |
  | response for app.example
  v
+----------------------------------------------------------+
| Browser origin: https://app.example                      |
|                                                          |
|  JS here can call app.example APIs                       |
|  JS here can read same-origin DOM and non-HttpOnly tokens |
|  browser enforces limits against other origins            |
+----------------------------------------------------------+

The browser does a lot, but it does not know which POST request the user intended. It also cannot tell whether a CMS field was meant to be text, trusted HTML, or executable script unless the application keeps those boundaries clear.

XSS: Untrusted Content Becomes App Code #

Cross-site scripting happens when untrusted content is rendered as active HTML or JavaScript inside a trusted origin.

CMS field, user comment, Markdown, imported feed
  |
  | treated as trusted HTML
  v
+----------------------------------------------------------+
| Browser origin: https://app.example                      |
|                                                          |
|  <script> or event handler now runs as app.example        |
|                                                          |
|  can read DOM                                            |
|  can call same-origin APIs                               |
|  can read tokens stored in JS-visible storage             |
|  can submit requests with existing cookies                |
+----------------------------------------------------------+

An HttpOnly cookie helps because JavaScript cannot read it directly. It does not stop injected JavaScript from sending same-origin requests that automatically include that cookie. A bearer token in localStorage, a global JS variable, or a page-embedded data blob is usually easier for XSS to steal.

The practical rule: untrusted content should stay text until a sanitizer or renderer deliberately turns it into a smaller safe subset. Markdown is not automatically safe. CMS HTML is not automatically safe. QIP output is not automatically safe.

CSRF/XSRF: Surprise Requests Borrow A Session #

Cross-site request forgery happens when another site causes the browser to send a request that carries the user's existing authority.

https://evil.example
  |
  | link, image, form, script-created navigation
  v
User browser
  |
  | request to https://app.example
  | cookies may be attached by the browser
  v
+----------------------------------------------------------+
| [trusted] app.example server                             |
|                                                          |
|  "this user has a session" is not enough                 |
|  server must also ask "did the user intend this action?" |
+----------------------------------------------------------+

The attacker usually cannot read the response across origins. That is still enough if the request changes state: publish, delete, transfer, invite, rotate, buy, subscribe.

Use SameSite cookies, CSRF tokens, method discipline, origin checks, and idempotent GET routes. These are application boundaries, not rendering boundaries.

CMS And User-Generated Content #

A CMS is convenient because non-developers can change production content. It is also a trust decision.

CMS editor / user / imported content
  |
  | content bytes
  v
+----------------------------------------------------------+
| [trusted?] content store                                 |
+----------------------------------------------------------+
  |
  | render as text, sanitized HTML, or trusted HTML?
  v
Browser origin

Treat CMS content as a data source first. Decide what each field is allowed to contain:

QIP can help with the transform part. A QIP Markdown renderer or sanitizer can run without filesystem, network, environment, or secret access. That limits what a bad renderer can do to the host. It does not mean the rendered HTML is safe to mount into a privileged origin.

Confused Deputy #

A confused deputy bug happens when trusted code is tricked into using its authority for less-trusted input.

untrusted input
  |
  | "please do this"
  v
+----------------------------------------------------------+
| [trusted] app/server/browser with real authority          |
|                                                          |
|  has cookies, secrets, filesystem, network, database      |
|  accidentally spends that authority for the attacker      |
+----------------------------------------------------------+

CSRF is one version: the browser has the user's cookies, and an attacker tries to borrow them for an unwanted action. Server-side request forgery is another: the server has network access, and attacker-controlled input chooses where it points. Template injection and rendering RCE follow the same pattern when a renderer with app privileges is made to execute content as code.

QIP is designed to make the deputy smaller. The component does not get ambient authority by default, so there is much less authority for attacker-controlled input to borrow.

Capabilities Instead Of Ambient Context #

Capability-based security starts with a simple rule: code can only use what it has been explicitly given.

Many application tools assume global context. A helper can read process environment. A plugin can import a package. A server component can call application code. A template extension may reach the filesystem or network because it is running inside the app process.

QIP is on the strict side of this spectrum. A component cannot reach out to discover context. The host passes bytes in and the component returns bytes out. If the component needs a setting, pass it as a uniform or include it in the input. If it needs database data, the app should query the database and pass only the relevant bytes.

ambient model:       code -> reaches out for context
capability model:    host -> passes explicit input -> code
QIP default:         host -> bytes/uniforms/events -> component -> bytes

These explicit boundaries are easy to reason about, audit, test, and review.

CSP And Sandboxed Previews #

Content Security Policy is a browser-side blast-radius control. It can restrict where scripts, images, styles, frames, and connections may come from. A good CSP does not replace escaping, sanitizing, or careful content modeling, but it gives the browser a policy to enforce when something slips.

Sandboxed iframes are another useful boundary. Any framework can add sandbox, and QIP should use it where it renders untrusted preview HTML. A sandboxed preview is a place to inspect output without immediately granting it the full authority of the app origin.

app origin
  |
  | preview bytes
  v
<iframe sandbox>
  |
  | rendered output with reduced browser powers
  v
preview document

Use CSP and iframe sandboxing as browser boundaries. Use QIP as the transform boundary before the browser sees the bytes.

Script Tags And Growing Sandboxes #

Loading JavaScript from a server is a different trust decision.

If you load JavaScript with <script src="...">, there is no small component boundary around that script. It runs as page code. It can allocate memory until the browser stops it. It can start more network requests. It can load more code. It runs in a browser sandbox, but the sandbox can grow in size and reach out to fetch more of what it wants.

That is often intentional. Analytics and tracking scripts are added to help product and marketing teams understand behavior. The tradeoff is that the script is not just data. It is code running inside your page's authority.

<script src="https://analytics.example/script.js">
  |
  v
+----------------------------------------------------------+
| Browser origin: https://app.example                      |
|                                                          |
|  third-party script runs as page code                    |
|  can inspect DOM                                         |
|  can send network requests                               |
|  can allocate memory until browser/runtime limits         |
|  can load more code                                      |
+----------------------------------------------------------+

QIP is meant to sit on the stricter side of this spectrum. A component should not assume it can read global page context, discover tokens, call network APIs, or grow into a larger runtime. The host passes input explicitly and reads output explicitly.

The CLI and browser custom elements now expose that boundary as policy. qip run, qip bench, and qip image can reject modules with --max-memory <bytes> and --fixed-memory. <qip-edit> and <qip-play> can do the same before browser compilation with max-memory="<bytes>" and fixed-memory.

Those checks are opt-in. That is deliberate for now: current components do not use memory.grow, but some older artifacts still need explicit memory maxima before a max-memory rule can become painless by default.

How WebAssembly Changes The Shape #

WebAssembly is not HTML and it is not JavaScript. A Wasm module gets linear memory and imported functions. If the host does not import filesystem, network, DOM, clock, or secret access, the module cannot call those things directly.

[trusted] browser JS or server host
  |
  | imports decide capabilities
  v
+----------------------------------------------------------+
| [sandboxed] WebAssembly module                           |
|                                                          |
|  can compute over memory                                 |
|  cannot reach host APIs without imports                  |
+----------------------------------------------------------+

QIP narrows this further. A QIP component is not given WASI by default. It is not given custom host imports by the current runtime. The normal interface is input bytes, optional uniforms/events, render(input_size), and output bytes. Instead of assuming global context, QIP makes context an explicit input.

That means QIP is a useful place to run code you want to review as a transform instead of trusting as application code. It is especially useful for AI-generated components, content transforms, validators, and renderers that should not inherit the app's filesystem, network, database, or secret access.

QIP Component Pipeline #

QIP makes the small transform a separate boundary.

[trusted] host app, qip run, qip dev, qip router, native app, CI
  |
  | explicit input bytes
  v
+------------------+      +------------------+      +------------------+
| [sandboxed]      |      | [sandboxed]      |      | [sandboxed]      |
| component A      | ---> | component B      | ---> | component C      |
|                  |      |                  |      |                  |
| no filesystem    |      | no filesystem    |      | no filesystem    |
| no network       |      | no network       |      | no network       |
| no env/secrets   |      | no env/secrets   |      | no env/secrets   |
| no package graph |      | no package graph |      | no package graph |
+------------------+      +------------------+      +------------------+
  ^                                                     |
  |                                                     v
  +---------------- explicit output bytes --------------+

Each stage receives only the bytes, uniforms, or events the host deliberately passes in. A Markdown renderer does not get a database handle. A QR-code generator does not get network access. A generated validator does not get environment variables or local files.

The host is still trusted. The difference is that the component is not handed the host's privileges just because it is useful code.

QIP Inside An Existing App #

QIP is not trying to replace the application. It fits at the places where a small piece of work should be portable, deterministic, or easier to review.

+----------------------------------------------------------+
| [trusted] product app                                    |
|                                                          |
|  routes, auth, metrics, logging, DB queries, UI state     |
|       |                                                  |
|       | explicit bytes                                   |
|       v                                                  |
|    +-----------------------------------------------+     |
|    | [sandboxed] QIP component or recipe            |     |
|    |                                               |     |
|    | markdown -> HTML                              |     |
|    | SVG -> bitmap                                 |     |
|    | URL -> QR SVG                                 |     |
|    | HTML -> accessibility facts                   |     |
|    +-----------------------------------------------+     |
|       |                                                  |
|       | explicit bytes                                   |
|       v                                                  |
|  app decides where the output goes                       |
+----------------------------------------------------------+

The application can weave normal code between QIP stages. For example, it can log a metric, run a database query, choose a component, pass bytes into QIP, then store or render the output. QIP should not become the whole app.

QIP Router #

QIP Router uses the same boundary, but applies it to routed content.

site/
  index.md
  docs/security.md
  _recipes/text/markdown/10-render.wasm
  _recipes/text/markdown/20-wrap.wasm

Request: /docs/security
  |
  v
+----------------------------------------------------------+
| [trusted] qip router                                     |
|                                                          |
|  resolve path -> read source bytes -> choose recipes      |
+----------------------------------------------------------+
  |
  | docs/security.md bytes
  v
+------------------+      +------------------+
| [sandboxed]      |      | [sandboxed]      |
| 10-render.wasm   | ---> | 20-wrap.wasm     |
| Markdown -> HTML |      | HTML -> page     |
+------------------+      +------------------+
  |
  | response bytes
  v
Browser

The router can read files because it is the host. Recipe components cannot read the site tree directly. They only see the current response bytes unless the host passes a larger format, such as application/warc, into an archive-level recipe.

Whole-Site Recipes #

Some work needs the whole routed site, not one page at a time. QIP uses WARC as the explicit boundary format for that.

[trusted] qip router
  |
  | routed site as application/warc
  v
+-----------------------------------------------+
| [sandboxed] application/warc recipe            |
|                                               |
| check links, add sitemap, add redirects,       |
| rewrite archive records                        |
+-----------------------------------------------+
  |
  | updated application/warc
  v
[trusted] host writes tar, deploys, or inspects output

The component still does not get filesystem or network access. The host chooses to provide a whole archive as input bytes.

Security Boundary View #

QIP separates three questions that are often blended together:

In QIP, the answer to the third question should be boring: it can compute over its linear memory and exported functions. It cannot open a socket, read ~/.ssh, inspect process.env, install a package, or call a database unless the host gives it a custom capability. The current qip runtime does not provide WASI or custom host imports to content components.

That boundary is useful for unreviewed or AI-generated code. The code may still be wrong. It may still produce unsafe HTML, invalid JSON, or a bad image. But it should not be able to escape the input/output contract and rummage through the host.

For browser security, keep the distinction sharp:

What Still Needs Review #

The boundary is not a substitute for all review.

The important shift is scope. A bad component should be a bad transform, not a bad transform with filesystem, network, package, and secret access.

Short Comparison #

ModelMain unitGood atSecurity shape
Traditional web appapp processsimple request -> response renderingserver code and dependencies share app authority
Next.js-style appframework-coordinated approuting, caching, server/client rendering, hydrationbrowser code is sandboxed; server/build code is trusted app code
QIPsmall component or recipe stagedeterministic transforms that run inside many appscomponent sees explicit bytes and runs without ambient host access

QIP is not a better Next.js or a smaller traditional web framework. It is a different boundary. Use it when the valuable part is a portable, deterministic piece of work that should not inherit the full authority of the app around it.