Skip to main content

Security

Two things decide whether a WF-Algolia site is safe: which key you publish, and what happens to record content on its way into the page. The script handles the second. The first is on you.

Keys​

Everything in a script tag is public. View source is enough.

KeySafe in a script tagCan do
Search-OnlyYesQuery indices
AdminNeverAdd, overwrite and delete records and indices

Use a Search-Only key with the search ACL and nothing more. If an Admin key has ever been pasted into custom code, treat it as compromised and rotate it in the Algolia dashboard — republishing the site does not undo it having been public.

For the same reason, WF-Algolia's own sync stores your Admin key server-side and never sends it to the browser.

Rendering untrusted content​

Anything in your index is untrusted as far as the script is concerned — CMS content passes through editors, imports and APIs, and any of those can carry markup.

Plain text bindings​

wf-algolia-text writes through textContent. Markup in the field is displayed as characters, never parsed. This is the default and the safe one.

HTML bindings​

wf-algolia-html exists for fields that legitimately contain rich text. It is sanitized before insertion, using the browser's own parser plus a strict allow-list.

Only these tags survive:

p · br · em · strong · b · i · u · mark · ul · ol · li · h1 · h2 · h3 · h4 · blockquote · code · pre · a · img

Any other element is unwrapped: the tag is dropped and its text is kept. A <script> in a field becomes its own text content, not an executing script.

Attributes work the same way — an allow-list, not a blocklist:

TagAttributes kept
ahref, title, target, rel
imgsrc, alt, title, width, height
everything elsenone

This is why onclick, onerror and style cannot get through. They are not blocked by name — they are simply not on the list, so nothing has to anticipate the next event-handler attribute someone invents.

URLs​

Every URL-bearing attribute — a href, img src — is additionally checked. The value is parsed with the browser's own URL parser and its scheme is matched against an allow-list:

AttributeSchemes allowed
a hrefhttps mailto tel
img srchttps

Anything else is replaced with # (or an empty string, for src) and logged:

[wf-algolia] Blocked unsafe URL scheme: javascript: javascript:alert(1)

http addresses are refused. A cleartext destination is a downgrade your visitor never sees, so a record field holding http://example.com/page renders as a dead link and logs:

[wf-algolia] Blocked a cleartext destination: http://example.com/page — destinations must be https. Update the record field to an https address.

The fix is in the record, not in your markup: change the stored address to https. A Webflow published site is always served over https, so this only affects addresses your own content points at.

One narrow exception exists for local development. If the page itself is served over http (a local server, for example), an http address on that same origin is allowed, because there is no downgrade to protect against on a page that is already cleartext. This can never apply on a published Webflow site.

An allow-list rather than a blocklist, because a blocklist of javascript:, data: and vbscript: prefixes was bypassable: the URL spec strips tab, newline and carriage return from inside a scheme, so java\nscript:alert(1) resolved as javascript: while no prefix comparison could see it. Parsing first makes that entire class of trick structurally impossible instead of merely enumerable.

Relative URLs keep working and are never rewritten — the original value is returned, not the resolved absolute one, so /blog/post stays /blog/post.

Seeing that warning means a record in your index contains an attack payload, not that your markup is wrong. It is worth finding out how it got there.

Forms the script takes over​

A search element often sits inside a Webflow Form Block. Left alone, pressing Enter submits the form, reloads the page and throws the query away. The script can prevent that: it stops the submit, hides the submit button, and hides the success and error messages.

It does this only when you ask. Mark the form:

<form wf-algolia-form="search"> ... </form>

Only that exact value works. Anything else, including true, leaves the form alone.

An unmarked form is never touched, and says so once in the browser console:

[wf-algolia] Not taking over this form: it has no wf-algolia-form="search". The form will submit normally, which reloads the page and discards the query. If it is a search form, add wf-algolia-form="search" to it.

Upgrading from 1.0.8 or earlier​

Older versions took over any form containing a search element, and refused only forms carrying a password or payment field. That was wrong for the forms in between: a contact or newsletter form carries nothing sensitive, so it was taken over and silently stopped submitting.

If you built a search form inside a Form Block by hand, add wf-algolia-form="search" to it and republish. Until you do, it will submit and reload the page.

Anything the app inserted for you is unaffected: inserted experiences are not built inside a <form>.

Privacy​

Insights tracking is off unless you set data-insights="true".

Turning it on is not enough to start it. The script binds its listeners and then sends nothing until your consent surface calls WfAlgolia.setInsightsConsent(true), and anything the visitor did before that is discarded rather than sent afterwards. Add data-insights-consent="granted" if your site already has a legal basis to track. See the script tag reference for both attributes.

When on, it is cookieless by default — the user token lives only for the page session. Setting data-insights-cookie="true" persists it across visits, which is what makes personalization work and what may put it in scope for your cookie consent flow. That cookie is written SameSite=Lax, and Secure on https.

wf-algolia-user-token="cookie:NAME" reads only _ALGOLIA or a cookie named wf-algolia-* / wf_algolia_*. Any other name is refused and logged, so the attribute cannot be used to read an unrelated cookie.

Personalized recommendations​

The recommended-for-you model is the one feature that reads a visitor identifier in order to work. It waits for consent exactly like event tracking does.

What is collected, and why:

QuestionAnswer
What is readAn Algolia visitor identifier, from the _ALGOLIA cookie or a wf-algolia-* cookie you set yourself
Who reads itThe script, in the visitor's browser
Where it is sentYour own Algolia application, in the Recommend request
What it is used forChoosing which of your records to recommend to that visitor
Who else receives itNobody. It is not sent to us, and we never see it
Before consentNothing is read and no request is made

Until consent is given, the section stays hidden and logs a note naming the two ways to grant it. The moment consent arrives the section loads, so a cookie banner that grants consent after the page has loaded works without a reload.

This applies whether or not you enabled data-insights. Personalization and event tracking are separate features and either can be used without the other, but both wait for the same consent.

Content Security Policy​

The script needs very little relaxation of its own.

  • It runs no eval and builds no code from strings, so 'unsafe-eval' is not required.
  • It assigns no HTML strings and writes no style attributes, so 'unsafe-inline' is not required — for scripts or for styles.

The minimum policy is the CDN it loads from, the Algolia endpoints it calls, and wherever your record images live:

script-src https://cdn.jsdelivr.net;
connect-src https://*.algolia.net https://*.algolianet.com;
img-src https://cdn.prod.website-files.com;
DirectiveWhy
script-srcWhere the script itself is served. Change it if you host the file yourself.
connect-srcAlgolia resolves queries across *.algolia.net and its *.algolianet.com retry hosts. Both are needed. Add https://insights.algolia.io only if you have enabled Insights.
img-srcWherever the images in your records are hosted. For images uploaded to Webflow that is https://cdn.prod.website-files.com; if your records point at another CDN, list that instead.
style-srcNothing to add. The script's own rules are installed as a constructable stylesheet adopted into the document, which style-src does not govern.

How results are rendered​

Search results are built as DOM nodes and inserted with replaceChildren — no HTML string is ever assigned to innerHTML. Highlighted matches and snippets are assembled from text nodes plus one element per match, so record content can only ever become text, never a tag name and never an attribute.

Visibility is driven by two classes rather than by writing element.style. These are CSS classes, not attributes, which is why they are not in the attribute reference:

ClassMeaning
.wf-algolia-hiddenHidden. display: none !important.
.wf-algolia-display-<kind>Shown, with the display your wf-algolia-display asked for — block, flex, grid, inline, inline-block, inline-flex, inline-grid, contents, flow-root.

You can style against these, but you do not have to define them: the script installs the rules itself.

Trusted Types​

If you send:

Content-Security-Policy: require-trusted-types-for 'script'

everything works with no further action. An absent trusted-types directive leaves policy creation unrestricted, and the only sink the script touches is the HTML parser used for wf-algolia-html rich-text fields.

If you enumerate the policies your page allows, add ours:

Content-Security-Policy: require-trusted-types-for 'script';
trusted-types wf-algolia

If you do not — or you send trusted-types 'none' — the script keeps working and only rich-text fields are affected: wf-algolia-html renders its field as plain text instead of formatted markup, and logs one line saying so. Search results, highlighting and snippets are unaffected, because none of them go near an HTML parser.

One caveat​

On browsers without constructable stylesheets — before Chrome 73, Safari 16.4 or Firefox 101 — the script falls back to injecting a <style> element, which style-src does govern. On such a browser under style-src 'self', results would render but would not hide correctly. In practice those browsers predate strict-CSP adoption and the two do not overlap, but we would rather state the limit than imply there isn't one.

Reporting a vulnerability​

Please report security issues privately rather than in a public issue.