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:
-
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.
-
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.
-
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.
-
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:
- Attacker visits any page on the site that loads this theme.
- Opens browser DevTools → Sources → searches for
GLOBAL_CONFIGoralgolia. - Finds the rendered JavaScript block containing
appId: 'YourAppId'andapiKey: 'abc123secretkey'. - 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": ""}'
- 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 %>'
},
After — JSON.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:
-
Proper string quoting:
JSON.stringify()produces a properly quoted, escaped JSON string. A value likeO'Reilly's CDNwon't break the JavaScript syntax. -
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. -
Safe fallbacks: The
|| ''pattern ensures thatundefinedornullvalues don't produceJSON.stringify(undefined)(which returnsundefined, not a string) — they produce an empty string instead. -
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 useJSON.stringify()instead of manual quote-wrapping. - The
config.ejstemplate 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_CONFIGare 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 preventundefinedvalues 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 fromconfig.algoliaor environment variables (ALGOLIA_APP_ID,ALGOLIA_API_KEY) - Sink: Unescaped EJS
<%-interpolation inlayout/_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
- 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
- OWASP Cryptographic Failures (A02:2021)
- OWASP Secrets Management Cheat Sheet
- Algolia Security Best Practices — API Key Scoping
- EJS Documentation — Tags
- Semgrep rules for credential exposure
- fix: algolia api credentials (appid and apikey) are ... in config.ejs