Security

Redactor takes security seriously. The editor includes client-side cleanup — paste sanitization, URL filtering, and an htmlSanitizer service — but a JavaScript editor cannot protect your application by itself.

A user can open DevTools, edit the DOM, bypass editor rules, or post crafted HTML directly to your API. Treat everything you receive from the browser as untrusted input. Always sanitize and validate content on the server before storing or rendering it.

What Redactor does in the browser#

These features reduce accidental XSS and messy markup, but they are not a security boundary:

Layer What it does
Paste sanitization (paste.sanitize) Strips risky attributes from pasted HTML
htmlSanitizer Removes denied tags, event handlers, dangerous URLs and styles
Upload flow Sends files to your endpoint; validation is your job

Use them to keep the editing experience clean. Do not rely on them to keep your site safe.

Server-side checklist#

Before you save or display HTML from Redactor, run it through your own pipeline:

1. Strip unwanted tags

Allow only tags your product needs (p, a, img, table, …). Remove everything else — especially <script>, <iframe> (unless you explicitly allow embeds), <object>, <form>, and inline <svg> with scripts.

2. Strip unwanted attributes

Remove event handlers (onclick, onerror, …), javascript: URLs, and attributes you do not use. Pay extra attention to:

  • Images — src, srcset, onerror, usemap
  • Links — href, target, rel
  • Embeds — src, poster, data-* you did not whitelist

3. Validate URLs

Check that href, src, and embed URLs use allowed schemes (https:, mailto:, relative paths) and allowed hosts. Block javascript:, data: (except where you explicitly need it), and unknown domains.

4. Validate POST requests

  • Check Referer / Origin matches your site when content is submitted.
  • Require authentication for save and upload endpoints.
  • Use CSRF tokens on state-changing requests (see below).

5. Validate uploaded files

Image and file uploads are a common attack vector:

  • Verify the file is a real image (magic bytes / MIME sniffing), not PHP or JS disguised with an image extension.
  • Re-encode or strip EXIF metadata if needed.
  • Store files outside the web root or serve them with safe Content-Type and Content-Disposition.
  • Generate filenames server-side; never trust the client-provided name.

Never execute uploaded files. Never serve user uploads from the same origin as your app without strict type checks.

Send a CSRF token with autosave#

When autosave posts content to your server, include a CSRF token in the body or headers.

Option A — autosave.data (static fields)

If the token is known at init time:

const app = Redactor('#entry', {
    autosave: {
        url: '/api/save',
        name: 'content',
        data: {
            _csrf: document.querySelector('meta[name="csrf-token"]').content
        }
    }
});

Extra fields are merged into the request body (content=<html>&_csrf=… for default POST encoding).

Option B — autosave:before:request (headers)

Add headers to the underlying fetch request:

const app = Redactor('#entry', {
    autosave: {
        url: '/api/save',
        name: 'content'
    },
    events: {
        'autosave:before:request'({ url, method, init }) {
            return {
                url,
                init: {
                    ...init,
                    headers: {
                        ...init.headers,
                        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
                    }
                }
            };
        }
    }
});

Subscribe after init with app.on('autosave:before:request', …) if you prefer.

Option C — autosave.handler (full control)

Use a custom handler when you need JSON, cookies, or your own fetch logic:

const app = Redactor('#entry', {
    autosave: {
        name: 'content',
        debounce: 1500,
        handler({ name, data }) {
            return fetch('/api/save', {
                method: 'POST',
                credentials: 'same-origin',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': getCsrfToken()
                },
                body: JSON.stringify(data)
            });
        }
    }
});

Cancel or modify before send

autosave:before:send runs earlier — you can append fields or cancel the save:

app.on('autosave:before:send', ({ data, stop }) => {
    if (isReadOnly()) {
        stop();
        return;
    }
    return {
        data: {
            ...data,
            _csrf: getCsrfToken()
        }
    };
});

See Autosave events and autosave settings.

Send a CSRF token with upload#

Image and file uploads use FormData. Add the token as an extra field or send it in a header.

Option A — image.data

const app = Redactor('#entry', {
    image: {
        upload: '/upload/image',
        data: {
            _csrf: getCsrfToken()
        }
    }
});

Fields are appended to the multipart body before the request is sent.

Option B — upload:before:send event

Use this when the token changes or you need logic per upload:

const app = Redactor('#entry', {
    image: {
        upload: '/upload/image'
    },
    events: {
        'upload:before:send'({ data, files }) {
            data.append('_csrf', getCsrfToken());
            return { data, files };
        }
    }
});

Handlers can also cancel an upload by returning { cancel: true }.

Option C — custom image.upload function

For full control over headers and the request:

const app = Redactor('#entry', {
    image: {
        upload(uploader, { data, files, e }) {
            data.append('_csrf', getCsrfToken());

            fetch('/upload/image', {
                method: 'POST',
                credentials: 'same-origin',
                headers: {
                    'X-CSRF-Token': getCsrfToken()
                },
                body: data
            })
                .then((res) => res.json())
                .then((json) => uploader.complete(json, e))
                .catch((err) => uploader.complete({ error: true, message: String(err) }, e));
        }
    }
});

See Upload events and Image settings.

Subscribe at init vs after init#

You can wire security hooks in editor options:

const app = Redactor('#entry', {
    autosave: { url: '/api/save', name: 'content' },
    image: { upload: '/upload/image' },
    events: {
        'autosave:before:request'({ init }) {
            return {
                init: {
                    ...init,
                    headers: {
                        ...init.headers,
                        'X-CSRF-Token': getCsrfToken()
                    }
                }
            };
        },
        'upload:before:send'({ data, files }) {
            data.append('_csrf', getCsrfToken());
            return { data, files };
        }
    }
});

See Events overview.

Content Security Policy (CSP)#

If you use a strict CSP on pages with Redactor:

  • Allow inline styles the editor needs, or scope the editor in an iframe with a relaxed policy.
  • Restrict script-src — user HTML must never be able to run scripts in your page context.
  • Even with CSP, server-side sanitization is still required. CSP is defense in depth, not a substitute for cleaning stored HTML.

Quick reference#

Risk Client (Redactor) Server (your app)
XSS in saved HTML Paste sanitize, htmlSanitizer Strip tags/attrs, allowlist URLs
Malicious upload Sends file to your URL MIME check, re-encode, safe storage
Forged save/upload CSRF token in body/headers Validate token, auth, Referer/Origin
Direct API abuse — Auth, rate limits, input validation