{"record":{"id":"8e5b2dc08f146948","repo":"argoproj/argo-workflows","slug":"artifact-key-q-must-start-with-q","errorCode":null,"errorMessage":"artifact key %q must start with %q","messagePattern":"artifact key %q must start with %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/utils/artifactkey.go","lineNumber":23,"sourceCode":"\t\"path\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/google/uuid\"\n)\n\n// ValidateUploadedArtifactKey checks that key is exactly the format the upload\n// endpoint generates for namespace: uploads/{namespace}/{uuid}/{filename}. It\n// rejects path traversal, absolute paths, empty segments, and any key outside\n// the upload prefix, since a client-supplied key is otherwise applied to the\n// artifact location without further checks.\n//\n// This is defense-in-depth, not a proof of ownership: a valid-looking key\n// naming another user's upload under the same namespace still passes.\nfunc ValidateUploadedArtifactKey(namespace, key string) error {\n\tprefix := \"uploads/\" + namespace + \"/\"\n\tif !strings.HasPrefix(key, prefix) {\n\t\treturn fmt.Errorf(\"artifact key %q must start with %q\", key, prefix)\n\t}\n\tif strings.Contains(key, \"..\") {\n\t\treturn fmt.Errorf(\"artifact key %q must not contain '..'\", key)\n\t}\n\tif strings.HasPrefix(key, \"/\") {\n\t\treturn fmt.Errorf(\"artifact key %q must not be an absolute path\", key)\n\t}\n\tif path.Clean(key) != key {\n\t\treturn fmt.Errorf(\"artifact key %q is not in canonical form\", key)\n\t}\n\n\tparts := strings.Split(key, \"/\")\n\tif len(parts) != 4 {\n\t\treturn fmt.Errorf(\"artifact key %q must have exactly 4 segments: uploads/{namespace}/{uuid}/{filename}\", key)\n\t}\n\tif slices.Contains(parts, \"\") {\n\t\treturn fmt.Errorf(\"artifact key %q must not contain empty segments\", key)\n\t}","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/argoproj/argo-workflows/blob/35bff19146f5a6ada77468c431f2624bd577e373/server/utils/artifactkey.go#L5-L41","documentation":"ValidateUploadedArtifactKey enforces that an artifact key matches the exact format the upload endpoint generates for the caller's namespace: uploads/{namespace}/{uuid}/{filename}. A key that does not begin with the namespace-scoped 'uploads/<namespace>/' prefix is rejected, since client-supplied keys are applied to artifact locations without further ownership checks.","triggerScenarios":"Passing an artifact key that omits the uploads/<namespace>/ prefix (e.g. 'my-file.bin', 'uploads/other-ns/uuid/file' when namespace differs), a raw bucket key from another environment, or a key built by hand with the wrong namespace string.","commonSituations":"Hard-coded keys from another namespace; copying a key from logs of a different namespace; forgetting to prepend the prefix when constructing keys programmatically; case mismatch in the namespace.","solutions":["Prefix the key with 'uploads/<your-namespace>/': uploads/{namespace}/{uuid}/{filename}.","Re-generate the key by letting the upload endpoint produce it instead of constructing it manually.","Verify the namespace argument matches the namespace encoded in the key (exact, case-sensitive match)."],"exampleFix":"// before\nerr := utils.ValidateUploadedArtifactKey(\"my-ns\", \"abc123/results.tar.gz\")\n// after\nkey := fmt.Sprintf(\"uploads/%s/%s/%s\", \"my-ns\", uuid.NewString(), \"results.tar.gz\")\nerr := utils.ValidateUploadedArtifactKey(\"my-ns\", key)","handlingStrategy":"validation","validationCode":"func validUploadKey(namespace, key string) bool {\n\treturn strings.HasPrefix(key, \"uploads/\"+namespace+\"/\")\n}\nif !validUploadKey(ns, key) { /* rebuild key before calling */ }","typeGuard":"func isUploadKeyFor(key, namespace string) bool {\n\treturn strings.HasPrefix(key, \"uploads/\"+namespace+\"/\")\n}","tryCatchPattern":"if err := utils.ValidateUploadedArtifactKey(ns, key); err != nil {\n\tif strings.Contains(err.Error(), \"must start with\") {\n\t\tkey = path.Join(\"uploads\", ns, key) // or regenerate key\n\t}\n}","preventionTips":["Always build keys as uploads/{namespace}/{uuid}/{filename} using path.Join.","Never hand-copy keys across namespaces or environments.","Let the upload endpoint generate the key instead of constructing it manually."],"tags":["artifacts","validation","security"],"backgroundTag":"artifact-key-validation","analyzedSha":"35bff19146f5a6ada77468c431f2624bd577e373","analyzedAt":"2026-09-03T19:34:35.908Z","contentChangedAt":"2026-09-03T19:34:35.908Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}