# How to Make “No Upload” Enforceable in the Browser

- Canonical URL: https://oka-project.com/en/blog/browser-only-tools-csp
- Published: 2026-09-12
- Author: oka (https://oka-project.com/en/about)
- Summary: A privacy promise is easier to trust when the architecture supports it. Here is how browser-side processing, CSP, and Network inspection fit together.
- Category: build / Tags: CSP, browser security, privacy, local processing

---
Saying "we do not upload your document" is a policy statement. It is more useful when the product architecture makes that behavior easier to verify.

For oka-project's browser tools, the goal is to keep Markdown preview, editing, and conversion on the device, then use Content Security Policy (CSP) to narrow unnecessary outbound paths. I also use the browser Network panel to check what actually happens during document operations.

The important caveat is that **CSP is not a universal proof that data can never leave a page.** Different directives control different paths, and some directives do not fall back to `default-src`.

## Start closed, then open only what the page needs

A restrictive policy can start from:

```http:csp-example.txt
default-src 'none';
script-src 'self';
style-src 'self';
font-src 'self';
img-src 'self' data:;
connect-src 'none';
form-action 'none';
base-uri 'none';
```

This is an example, not a drop-in policy for every site. Real applications may need additional sources.

The design principle is what matters: deny broadly, then explicitly allow the resources and destinations required by the application.

## What `default-src 'none'` actually does

`default-src` provides a fallback for many CSP fetch directives.

If `connect-src` is not separately specified, for example, it can fall back to `default-src`. That affects common script-initiated connections such as `fetch()`, `XMLHttpRequest`, WebSocket, EventSource, and `sendBeacon()`.

This is useful because the restriction is enforced by the browser, not by application code that can simply choose to ignore its own policy.

But it is inaccurate to say that `default-src 'none'` automatically closes every possible path.

## `form-action` does not fall back to `default-src`

HTML form submissions are controlled by `form-action`.

If the application should not submit forms to external destinations, that intent needs to be expressed directly:

```http
form-action 'none';
```

This distinction matters when making strong claims about "no upload." A policy that blocks fetch/XHR but leaves form submission unrestricted is not equivalent to a policy that explicitly closes both paths.

## `base-uri` is another separate control

`base-uri` controls which URLs may be used by the document's `<base>` element.

Like `form-action`, it is not simply inherited from `default-src`.

For a page that does not need `<base>`, a restrictive policy can make that explicit:

```http
base-uri 'none';
```

The broader lesson is to inspect the individual directives rather than treating "CSP enabled" as a binary property.

## The external-image path I found during auditing

One useful finding came from image handling.

If Markdown contains:

```markdown:example.md
![](https://attacker.example/pixel.png)
```

and the preview renders that URL directly, the browser will make a request to the remote host. The Markdown body may never be POSTed anywhere, but an outbound connection still occurs.

Earlier, the tool policy allowed HTTPS images too broadly. I changed the approach so external Markdown images are not fetched automatically during preview, and the CSP image policy is narrowed to the sources the application actually needs.

The general pattern is defense in depth:

1. restrict image destinations with CSP
2. avoid blindly turning untrusted Markdown image URLs into network requests in the renderer

Neither layer needs to carry the full burden alone.

## Network inspection should focus on document operations

A browser app still has normal web traffic when it loads HTML, JavaScript, CSS, fonts, and other application resources.

So "there was network traffic" is not evidence that a document was uploaded.

A better test is:

1. Open developer tools and the Network panel.
2. Let the page finish loading.
3. Clear the existing Network log.
4. Enter a harmless unique test string.
5. Run the preview, conversion, or export action you care about.
6. Inspect new requests, destinations, methods, query strings, and payloads.

If no document-bearing request appears during that operation, you can reasonably say that **you did not observe the document being transmitted in that tested workflow at that point in time**.

That is useful evidence, but it is not proof about untested features or future versions.

## Four layers are better than one privacy claim

I find it more accurate to separate the problem into four layers:

- **Policy** — state that document content is not sent to a conversion server for the local workflow.
- **Implementation** — perform Markdown parsing, preview, and conversion in the browser.
- **CSP** — restrict unnecessary connection, image, form, and resource destinations.
- **Measurement** — inspect actual network behavior during the operations users care about.

CSP is an important layer, but it does not control the operating system, every browser extension, or every user action.

## What this means for Markdown Viewer and Yomu

[Markdown Viewer](https://oka-project.com/en/tools/markdown-viewer) is designed for opening and previewing Markdown in the browser.

[Yomu](https://oka-project.com/en/tools/markdown-editor) adds editing and export workflows.

For their core document workflows, the design is to process Markdown in the browser rather than send the body to a conversion API. The outbound policy is then narrowed so that the privacy statement is supported by browser-enforced restrictions rather than by copy alone.

That still does not justify an absolute statement such as "nothing can ever leak." A browser extension with broad permissions, a compromised device, or a feature that intentionally sends data to another service is a different trust boundary.

## What I changed after finding the image issue

The practical lesson was that "local processing" and "no unnecessary outbound communication" are related but not identical.

A document can be parsed locally while a renderer still fetches an external image.

After finding that path, I tightened both the application behavior and the CSP restrictions. That is the standard I want for browser-only tools: a privacy claim should survive inspection of the implementation and the browser's actual network behavior.

## Summary

A credible "no upload" design needs more than a sentence in a privacy policy.

Browser-side processing keeps the core document workflow local. CSP can restrict many outbound paths. Specific directives such as `connect-src`, `img-src`, `form-action`, and `base-uri` need to be considered separately. And the Network panel provides a practical way to verify what happens during a real operation.

The goal is not to claim perfect security. It is to make the intended data path narrow, explicit, and inspectable.

### References

- [MDN: Content-Security-Policy default-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/default-src)
- [MDN: Content-Security-Policy connect-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/connect-src)
- [MDN: Content-Security-Policy form-action](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/form-action)
- [MDN: Content-Security-Policy base-uri](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/base-uri)
- [W3C: Content Security Policy Level 3](https://www.w3.org/TR/CSP/)