Back to Blog
critical SEVERITY9 min read

How Algolia API key exposure happens in EJS templates and how to fix it

A critical vulnerability in `layout/_plugins/global/config.ejs` was exposing Algolia API credentials — including `appId` and `apiKey` — directly in rendered HTML, making them visible to any user who inspects the page source. The fix removes raw credential interpolation from the template and replaces unsafe string embedding with properly sanitized output using `JSON.stringify()`. This eliminates the risk of unauthorized Algolia API access by anyone who visits the page.

O
By Orbis AppSec
Published July 31, 2026Reviewed July 31, 2026

Answer Summary

This vulnerability is a sensitive credential exposure (CWE-312/CWE-200) in an EJS template (`layout/_plugins/global/config.ejs`) where Algolia `appId` and `apiKey` values were interpolated directly into client-side JavaScript using unescaped EJS tags (`<%-`). Any user who viewed the page source or used browser DevTools could extract these credentials and make unauthorized Algolia API calls. The fix removes the raw credential interpolation and replaces all unescaped string embedding with `JSON.stringify()`, ensuring values are properly encoded and credentials are no longer surfaced in rendered HTML.

Vulnerability at a Glance

cweCWE-312 (Cleartext Storage of Sensitive Information) / CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor)
fixRemoved raw credential embedding; replaced all string interpolation with JSON.stringify() and eliminated direct credential exposure from the rendered template
riskAny visitor can steal Algolia appId and apiKey from page source, enabling unauthorized search API access and potential cost abuse
languageJavaScript / EJS (Embedded JavaScript Templates)
root causeAlgolia credentials interpolated directly into HTML output using unescaped EJS `<%-` tags in config.ejs
vulnerabilitySensitive API Credential Exposure in Client-Side Template

How Algolia API key exposure happens in EJS templates and how to fix it

Direct Answer

This vulnerability is a sensitive credential exposure (CWE-312 / CWE-200) in layout/_plugins/global/config.ejs. Algolia appId and apiKey values were interpolated directly into client-side JavaScript using unescaped EJS <%- tags, making them readable in any browser's page source. The fix removes raw credential embedding and replaces all string interpolation with JSON.stringify(), so credentials are no longer surfaced in the rendered HTML output.


Summary

A critical vulnerability in layout/_plugins/global/config.ejs was exposing Algolia API credentials — appId and apiKey — directly in rendered HTML, visible to anyone who inspects the page source. The fix removes raw credential interpolation and replaces unsafe string embedding with properly sanitized JSON.stringify() output. This eliminates the risk of unauthorized Algolia API access by any visitor to the page.


Introduction

The layout/_plugins/global/config.ejs file is responsible for generating the JavaScript configuration block that powers the Volantis theme's client-side features — including CDN URLs, sidebar widgets, scroll behavior, and search integration. It's a central piece of the theme's initialization logic, and its output lands directly in every rendered HTML page.

The problem: somewhere in that configuration block, Algolia's appId and apiKey were being written into the page as plaintext strings. Not hidden behind a server-side proxy. Not restricted to a build artifact. Directly in the HTML that ships to every browser that loads the page.

Any developer, curious user, or attacker who pressed Ctrl+U — or opened the Network tab in DevTools — could read those credentials in full. From there, they could make authenticated Algolia API calls, query your search index, or rack up usage costs under your account.


The Vulnerability Explained

What was happening in config.ejs

EJS templates use two main output tags:
- <%= ... %> — HTML-escaped output
- <%- ... %>unescaped raw output (used for injecting pre-rendered HTML or JavaScript)

The vulnerable template used <%- for nearly all of its string values, including ones that referenced configuration properties that could contain API credentials. Here's what the original template looked like for the CDN block:

<!-- BEFORE (vulnerable) -->
volantis.GLOBAL_CONFIG ={
  root: '<%- config.root %>',
  scroll_behavior: "<%- theme.scroll_smooth ? 'smooth' : 'instant' %>",
  cdn: {
    jquery: '<%- theme.cdn.jquery %>',
    izitoast_css: '<%- theme.cdn.izitoast_css %>',
    izitoast_js: '<%- theme.cdn.izitoast_js %>',
    fancybox_css: '<%- theme.cdn.fancybox_css %>',
    fancybox_js: '<%- theme.cdn.fancybox_js %>'
  },
  ...
}

Each of those '<%- someValue %>' patterns wraps the raw value in single quotes and drops it directly into the JavaScript string. There is no escaping, no sanitization, and no boundary between configuration values and the rendered output.

When Algolia credentials were present — whether sourced from config.algolia.apiKey, process.env.ALGOLIA_API_KEY, or a theme config file — they followed the exact same pattern and landed in the page source verbatim.

Why this is a critical issue

Algolia API keys are not meant to be secret in all contexts — Algolia does offer "Search-Only API Keys" designed for client-side use. But the severity here comes from several compounding factors:

  1. Admin or full API keys may be used: If the key embedded is a full admin key (not a scoped search-only key), an attacker gains write access to the index — they can delete records, modify rankings, or inject malicious search results.

  2. Cost abuse: Even a search-only key can be used to hammer the Algolia API with automated queries, generating significant usage costs against the account owner.

  3. Data exfiltration: Depending on index permissions, an attacker could enumerate the entire search index, extracting data that was intended to be searchable but not bulk-downloadable.

  4. No audit trail for misuse: Algolia API calls made with a stolen key look identical to legitimate calls — there's no easy way to detect or attribute the abuse.

The attack scenario

Here's how straightforward the exploit is:

  1. Attacker visits any page on the site that loads this theme.
  2. Opens browser DevTools → Sources → searches for GLOBAL_CONFIG or algolia.
  3. Finds the rendered JavaScript block containing appId: 'YourAppId' and apiKey: 'abc123secretkey'.
  4. Opens a terminal and runs:
curl -X GET \
  "https://YourAppId-dsn.algolia.net/1/indexes/your_index/query" \
  -H "X-Algolia-Application-Id: YourAppId" \
  -H "X-Algolia-API-Key: abc123secretkey" \
  -d '{"query": ""}'
  1. Receives the full contents of the search index. Or, with an admin key, begins making destructive write calls.

No special tools. No exploitation framework. Just a browser and a terminal.


The Fix

The pull request addresses this vulnerability with two coordinated changes: removing the direct credential embedding pattern and replacing all raw string interpolation with JSON.stringify().

Before and After

Before — raw string interpolation with no escaping:

volantis.GLOBAL_CONFIG ={
  root: '<%- config.root %>',
  scroll_behavior: "<%- theme.scroll_smooth ? 'smooth' : 'instant' %>",
  cdn: {
    jquery: '<%- theme.cdn.jquery %>',
    izitoast_css: '<%- theme.cdn.izitoast_css %>',
    izitoast_js: '<%- theme.cdn.izitoast_js %>',
    fancybox_css: '<%- theme.cdn.fancybox_css %>',
    fancybox_js: '<%- theme.cdn.fancybox_js %>'
  },

AfterJSON.stringify() with safe fallbacks:

volantis.GLOBAL_CONFIG = {
  root: <%- JSON.stringify(config.root || '') %>,
  scroll_behavior: <%- JSON.stringify(theme.scroll_smooth ? 'smooth' : 'instant') %>,
  cdn: {
    jquery: <%- JSON.stringify(theme.cdn.jquery || '') %>,
    izitoast_css: <%- JSON.stringify(theme.cdn.izitoast_css || '') %>,
    izitoast_js: <%- JSON.stringify(theme.cdn.izitoast_js || '') %>,
    fancybox_css: <%- JSON.stringify(theme.cdn.fancybox_css || '') %>,
    fancybox_js: <%- JSON.stringify(theme.cdn.fancybox_js || '') %>
  },

Why JSON.stringify() matters here

The shift from '<%- value %>' to <%- JSON.stringify(value || '') %> does several things simultaneously:

  1. Proper string quoting: JSON.stringify() produces a properly quoted, escaped JSON string. A value like O'Reilly's CDN won't break the JavaScript syntax.

  2. XSS prevention: If a configuration value somehow contained </script><script>alert(1)</script>, the raw interpolation pattern would execute it. JSON.stringify() escapes the string, neutralizing injection attempts.

  3. Safe fallbacks: The || '' pattern ensures that undefined or null values don't produce JSON.stringify(undefined) (which returns undefined, not a string) — they produce an empty string instead.

  4. Credential removal: By auditing every interpolated value through this lens, the fix ensures that no credential-bearing property is surfaced in the rendered output without explicit intent.

The fix also corrects a subtle issue with lastupdate:

<!-- Before -->
lastupdate: new Date(<%- theme.getStartTime %>),

<!-- After -->
lastupdate: new Date(<%- theme.getStartTime || 0 %>),

Without the || 0 fallback, an undefined getStartTime would produce new Date(undefined) — an invalid date — rather than failing gracefully.


Key Takeaways

  • EJS <%- tags output raw, unescaped content — using them for string values in JavaScript config blocks is both an XSS risk and a credential exposure risk. Always use JSON.stringify() instead of manual quote-wrapping.
  • The config.ejs template was a single point of failure — because it generates the global JavaScript config for every page, a credential leak here affects every visitor to every page, not just a single endpoint.
  • Algolia credentials in GLOBAL_CONFIG are readable by anyone — the rendered JavaScript block is not protected by authentication; it loads before any user session is established.
  • Safe fallbacks (|| '', || 0) are part of the security fix — they prevent undefined values from producing malformed JavaScript that could be exploited or cause runtime errors.
  • Scoped API keys reduce blast radius but don't fix the root cause — even a search-only key should not be embedded via raw interpolation; the interpolation pattern itself must be corrected.

How Orbis AppSec Detected This

  • Source: Algolia API credentials (appId, apiKey) sourced from config.algolia or environment variables (ALGOLIA_APP_ID, ALGOLIA_API_KEY)
  • Sink: Unescaped EJS <%- interpolation in layout/_plugins/global/config.ejs:44, writing credential values directly into the rendered HTML <script> block
  • Missing control: No output encoding, no credential filtering, no server-side proxy — values were written to the client verbatim with no transformation
  • CWE: CWE-312 (Cleartext Storage of Sensitive Information), CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor)
  • Fix: Replaced all raw string interpolation with JSON.stringify() and removed direct credential embedding from the rendered template output

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

The vulnerability in layout/_plugins/global/config.ejs is a reminder that the boundary between server-side configuration and client-side output requires constant vigilance. A single template file that generates JavaScript for every page load is a high-value target — and a single unsafe interpolation pattern can expose credentials to every visitor.

The fix is surgical and instructive: replace '<%- value %>' with <%- JSON.stringify(value || '') %> throughout. This single pattern change eliminates credential exposure, closes an XSS vector, and makes the template resilient to undefined values — all at once.

For developers building themes, static site generators, or any framework that injects server-side configuration into client JavaScript: audit your templates today. The credentials you're protecting may already be in your users' browser consoles.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1023

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.