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.


Prevention & Best Practices

1. Never embed secret API keys in client-side templates

If a credential must be used for a client-side feature (like Algolia search), use a scoped, read-only key with the minimum permissions required. Algolia explicitly provides "Search-Only API Keys" for this purpose. Document which key type is expected in your configuration schema.

2. Use JSON.stringify() for all values in JavaScript config blocks

Any EJS template that generates a JavaScript object should use JSON.stringify() for every string value — not manual quote wrapping. This prevents both XSS injection and syntax errors from special characters.

<!-- Don't do this -->
var config = { name: '<%- user.name %>' };

<!-- Do this -->
var config = { name: <%- JSON.stringify(user.name || '') %> };

3. Audit environment variable usage in templates

Search your templates for patterns like <%- process.env. or <%- config.algolia — these are high-risk patterns that may surface secrets. Add a pre-commit hook or CI check using grep or Semgrep to flag them.

# Quick grep to find potential credential exposure in EJS
grep -rn "<%[-=].*[Kk]ey\|[Ss]ecret\|[Pp]assword\|[Tt]oken" layout/

4. Implement Content Security Policy (CSP)

A strong CSP won't prevent credential exposure, but it limits what an attacker can do with injected scripts — reducing the blast radius of any XSS vulnerabilities that might exist alongside credential exposure.

5. Use secret scanning in CI/CD

Tools like truffleHog, gitleaks, and GitHub's built-in secret scanning can catch API keys committed to source or present in build artifacts before they reach production.

Relevant Standards

  • OWASP Top 10 A02:2021 — Cryptographic Failures (which includes cleartext transmission and storage of sensitive data)
  • CWE-312: Cleartext Storage of Sensitive Information
  • CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
  • CWE-116: Improper Encoding or Escaping of Output (the secondary issue fixed by JSON.stringify)

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.


References

Frequently Asked Questions

What is sensitive credential exposure in EJS templates?

It occurs when secret values like API keys are interpolated directly into server-rendered HTML using EJS tags, making them readable in the page source by any browser user.

How do you prevent API key exposure in EJS/Node.js templates?

Never embed secret credentials in client-side templates. Use server-side proxies for API calls, restrict keys with server-side environment variables, and use JSON.stringify() for any values that must appear in JS config blocks.

What CWE is API key exposure in rendered HTML?

CWE-312 (Cleartext Storage of Sensitive Information) and CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor) both apply when credentials are surfaced in client-accessible output.

Is restricting the Algolia key's permissions enough to prevent this vulnerability?

Partially — scoping key permissions reduces blast radius, but the key is still exposed and can be abused within those permissions. The root cause (embedding the key in HTML) must also be fixed.

Can static analysis detect API key exposure in EJS templates?

Yes. Tools like Semgrep can flag patterns where environment variables containing "KEY" or "SECRET" are interpolated into EJS output blocks. Orbis AppSec's multi-agent AI scanner detected exactly this pattern in config.ejs.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1023

Related Articles

high

How Quadratic CPU Consumption Vulnerabilities Happen in JavaScript YAML Parsers and How to Fix Them

A high-severity denial-of-service vulnerability in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through specially crafted YAML documents using the !!omap tag. This fix upgrades js-yaml from 4.1.1 to 4.3.1 and from 3.14.2 to 3.15.1, eliminating the algorithmic complexity attack vector that could freeze Node.js applications processing untrusted YAML input.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `scripts/build.js` where `execSync` was called with string-interpolated arguments (`sourceDir` and `outputPath`) inside a shell command. By replacing `execSync` with `spawnSync` using an argument array (no shell), the fix eliminates the possibility of shell metacharacter injection while preserving identical build behavior.

high

How Command Injection happens in Node.js child_process and how to fix it

A command injection vulnerability in nix.js's Release class allowed potentially malicious input through the `arch` parameter to be executed via shell commands. The fix replaced `execSync()` with `execFileSync()`, eliminating shell interpretation and preventing command injection by passing arguments as an array instead of a concatenated string.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.

critical

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.

high

How Path Traversal and Security Policy Bypass Happens in Node.js Dependencies and How to Fix It

A high-severity vulnerability in the fast-uri package (CVE-2026-6321) allowed attackers to bypass security policies through improper Unicode hostname canonicalization and path traversal. This issue affected the @apralabs/apra-fleet project through its dependency tree, and was resolved by upgrading fast-uri from version 3.1.0 to 4.1.2 using npm overrides.