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 unauthenticated endpoint exposure happens in Node.js Express and how to fix it

A high-severity vulnerability in the AgenticATODetectionService allowed unauthenticated users to access sensitive agent status data, trigger detection scans, and view security alerts. The fix adds authentication middleware to four critical API endpoints, ensuring only authorized users can access these sensitive operations.

critical

How unsigned auto-update code execution happens in Node.js Neutralinojs and how to fix it

A critical vulnerability in the WeekBox application's self-update mechanism allowed attackers to serve malicious binaries through man-in-the-middle attacks or repository compromise. The `app-updater.service.js` file downloaded and installed updates from GitHub Releases without enforcing cryptographic hash verification before proceeding with the update. The fix adds mandatory SHA-256 digest validation that halts the update process if a valid hash is not present in the release metadata.

critical

How command injection via shell metacharacter escaping happens in Node.js and how to fix it

A critical command injection vulnerability was discovered in the GameBanana provider module where the `quoteCommandArgument()` function only escaped double quotes, leaving shell metacharacters like `$()`, backticks, and other dangerous patterns exploitable. Attackers could craft malicious mod URLs on GameBanana containing shell commands that would execute when users viewed the content. The fix switches from double-quote to single-quote escaping, which prevents shell interpretation of metacharact

high

How Denial of Service via malformed HTTP header decoding happens in Node.js @opentelemetry/propagator-jaeger and how to fix it

CVE-2026-59892 is a high-severity Denial of Service vulnerability in `@opentelemetry/propagator-jaeger` versions prior to 2.9.0, where malformed HTTP trace-context headers could cause the propagator's decoding logic to crash a Node.js application. The fix upgrades the package from 2.8.0 to 2.9.0, patching the unsafe header parsing behavior and eliminating the attack surface for any service that propagates distributed tracing headers.

critical

How server-side template injection happens in Node.js EJS and how to fix it

A critical server-side template injection vulnerability (CVE-2022-29078) was discovered in EJS version 3.1.6, allowing attackers to execute arbitrary code on the server through the `outputFunctionName` option. This vulnerability was fixed by upgrading EJS from 3.1.6 to 3.1.7 in the react-redux-bad-algo project, eliminating a dangerous remote code execution vector.

critical

How hardcoded API keys happen in Node.js client-side JavaScript and how to fix it

A critical hardcoded API key for ipregistry.co was discovered embedded as a fallback value in `src/utils.js` at line 65 of a Node.js library. This key (`f8n4kqe8pv4kii`) was visible to anyone inspecting the JavaScript bundle, allowing unauthorized use of the service. The fix removes the hardcoded fallback, requiring callers to explicitly provide their own API key.