Introduction
In the Actor Studio application, we discovered a critical hardcoded API key vulnerability in src/plugins/llm/index.js at line 5. The LLM class—responsible for powering name and biography generation via an external AI service—had the API key actor-studio-gpt-beta baked directly into the class property definition:
apiKey = 'actor-studio-gpt-beta';
This wasn't an isolated oversight. The same key appeared as a default value in src/settings/llmNameGeneration.js, meaning it was doubly embedded in the codebase and distributed to every user of the application. For any developer building plugin systems that interact with external APIs, this is a textbook example of how a seemingly convenient default can become a critical security liability.
The Vulnerability Explained
The LLM class in src/plugins/llm/index.js serves as the client for Actor Studio's AI-powered features. It connects to https://actor-studio-llm.vercel.app/api to call endpoints like generateName and generateBiography. Here's the vulnerable code:
class LLM {
apiKey = 'actor-studio-gpt-beta';
baseUrl = 'https://actor-studio-llm.vercel.app/api';
// ...
}
And in the settings file src/settings/llmNameGeneration.js, the same key was registered as the default setting value:
{
scope: 'world',
config: true,
type: String,
default: 'actor-studio-gpt-beta',
}
Why This Is Dangerous
This creates a two-pronged exposure:
-
Source code exposure: Anyone who clones the repository, inspects the npm package, or views the distributed JavaScript bundle can extract the string
actor-studio-gpt-beta. -
Settings system exposure: Because the key is the default in the settings registration, even users who never explicitly configure an API key are silently using this shared credential. The key also appears in language/localization files referenced by the settings system, widening the attack surface further.
Concrete Attack Scenario
An attacker discovers the Actor Studio repository on GitHub (or decompiles the distributed package). They search for strings like apiKey, Bearer, or Authorization and immediately find actor-studio-gpt-beta. With this key, they craft HTTP requests:
curl -X POST https://actor-studio-llm.vercel.app/api/generateName \
-H "Authorization: Bearer actor-studio-gpt-beta" \
-H "Content-Type: application/json" \
-d '{"prompt": "Generate a fantasy character name"}'
This grants the attacker:
- Unauthorized API access: Free use of the LLM service endpoints without any legitimate Actor Studio installation.
- Cost escalation: If the LLM backend charges per request (common with GPT-based services), the attacker can rack up costs against the service operator's account.
- Abuse and data exfiltration: The attacker could probe the API for additional endpoints, attempt prompt injection attacks against the backend LLM, or use the service as a proxy for their own AI workloads.
- Credential stuffing: The key pattern
actor-studio-gpt-betamight hint at naming conventions for other internal keys, enabling further enumeration.
Because this is a 2-step exploitation chain (extract key → craft requests), the barrier to exploitation is extremely low—no sophisticated tooling or deep application knowledge is required.
The Fix
The fix is surgically scoped to exactly two files, removing the hardcoded credential from both locations where it appeared.
Change 1: src/plugins/llm/index.js
Before:
class LLM {
apiKey = 'actor-studio-gpt-beta';
baseUrl = 'https://actor-studio-llm.vercel.app/api';
After:
class LLM {
// No default API key is hardcoded here; it must be supplied via module settings.
apiKey = '';
baseUrl = 'https://actor-studio-llm.vercel.app/api';
The apiKey class property is now initialized to an empty string. This means the LLM plugin cannot function until a legitimate key is explicitly configured through the module's settings system. The comment makes the intent crystal clear to future contributors: this is a deliberate security decision, not a missing default.
Change 2: src/settings/llmNameGeneration.js
Before:
{
scope: 'world',
config: true,
type: String,
default: 'actor-studio-gpt-beta',
}
After:
{
scope: 'world',
config: true,
type: String,
default: '',
}
The settings registration now defaults to an empty string as well. This is critical because even if someone had already patched index.js, the settings system would have continued injecting the hardcoded key as the default value for any user who hadn't explicitly changed their configuration.
Why Both Changes Are Necessary
Fixing only index.js would leave the key exposed in the settings definition, where it would still be readable in the source and would be auto-populated into new installations. Fixing only the settings file would leave the class-level default intact, meaning the key would still be used if settings retrieval failed or returned undefined. Both files had to be patched to fully eliminate the credential from the codebase.
Important Post-Fix Action
The key actor-studio-gpt-beta must be considered compromised and should be rotated on the backend service (actor-studio-llm.vercel.app). Since it existed in the git history, simply removing it from the current code doesn't prevent extraction from previous commits.
Prevention & Best Practices
1. Never Hardcode Secrets—Even "Temporary" Ones
It's tempting to set a default API key during development for convenience. But "temporary" defaults have a way of shipping to production. Use environment variables or secure configuration from the start:
class LLM {
apiKey = process.env.LLM_API_KEY || '';
// ...
}
2. Implement Secret Scanning in CI/CD
Tools like truffleHog, GitLeaks, or GitHub's native secret scanning can catch hardcoded credentials before they reach the main branch. Add them as pre-commit hooks or CI pipeline steps.
3. Use a Secrets Manager
For production applications, use dedicated secrets management:
- Environment variables (minimum viable approach)
- HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault (enterprise-grade)
- Foundry VTT module settings with user-supplied keys (as this fix now requires)
4. Audit Settings Defaults
Settings systems often have default values that are overlooked during security reviews. Treat settings defaults with the same scrutiny as code-level constants—they're equally visible and equally dangerous.
5. Rotate Compromised Keys Immediately
Any key that has ever appeared in source code, logs, or configuration files should be rotated. Git history is permanent (without force-push and rewriting), so removal from HEAD alone is insufficient.
Relevant Standards
- OWASP: Cryptographic Failures — hardcoded credentials fall under this category
- CWE-798: Use of Hard-coded Credentials
- OWASP Cheat Sheet: Secrets Management
Key Takeaways
- The
actor-studio-gpt-betakey was embedded in two separate files—both theLLMclass definition and the settings registration—demonstrating how hardcoded secrets can propagate across a codebase. - Settings system defaults are a hidden attack surface: the key in
llmNameGeneration.jswould have persisted even if the class property was fixed, silently distributing the credential to every new installation. - A shared API key across all installations means a single compromise affects everyone: any user extracting the key could abuse the service on behalf of all Actor Studio users.
- The fix correctly initializes
apiKeyto an empty string in both locations, enforcing that the LLM plugin is non-functional until a legitimate key is explicitly provided—a secure-by-default posture. - Post-fix key rotation is mandatory: the
actor-studio-gpt-betakey remains in git history and must be revoked on theactor-studio-llm.vercel.appbackend.
How Orbis AppSec Detected This
- Source: The hardcoded string literal
'actor-studio-gpt-beta'insrc/plugins/llm/index.js:5andsrc/settings/llmNameGeneration.js:34, distributed with the application package. - Sink: The
Authorizationheader construction in theLLMclass methods (generateName,generateBiography) that sendBearer actor-studio-gpt-betatohttps://actor-studio-llm.vercel.app/api/endpoints. - Missing control: No environment variable lookup, no secrets manager integration, and no mechanism requiring user-supplied keys before API calls could be made.
- CWE: CWE-798 — Use of Hard-coded Credentials
- Fix: Replaced the hardcoded API key default
'actor-studio-gpt-beta'with an empty string''in bothsrc/plugins/llm/index.jsandsrc/settings/llmNameGeneration.js, requiring explicit key configuration via module settings.
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
Hardcoded credentials are one of the most common and most preventable security vulnerabilities in modern software. In this case, the API key actor-studio-gpt-beta was embedded directly in the LLM plugin class and its settings registration, making it trivially extractable by anyone with access to the source code or distributed package. The fix is straightforward—replace hardcoded defaults with empty strings and require explicit configuration—but the downstream action of rotating the compromised key is equally critical. If you're building plugin systems or any code that interacts with external APIs, treat every credential as sensitive from day one and never let convenience defaults ship to production.