{"record":{"id":"5a14db9b9dbef7e9","repo":"argoproj/argo-workflows","slug":"artifact-key-q-is-not-in-canonical-form","errorCode":null,"errorMessage":"artifact key %q is not in canonical form","messagePattern":"artifact key %q is not in canonical form","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/utils/artifactkey.go","lineNumber":32,"sourceCode":"// 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}\n\n\tuuidSegment := parts[2]\n\tif _, err := uuid.Parse(uuidSegment); err != nil {\n\t\treturn fmt.Errorf(\"artifact key %q must have a valid UUID segment: %w\", key, err)\n\t}\n\n\tfilename := parts[3]\n\tif path.Base(filename) != filename {\n\t\treturn fmt.Errorf(\"artifact key %q must have a bare filename segment\", key)","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/argoproj/argo-workflows/blob/35bff19146f5a6ada77468c431f2624bd577e373/server/utils/artifactkey.go#L14-L50","documentation":"The key must be in canonical path form: path.Clean(key) must equal key. Non-canonical forms like 'uploads/ns/uuid/./file' or double slashes reveal manipulative or sloppy construction and are rejected to keep keys unambiguous.","triggerScenarios":"Keys containing './' segments, redundant slashes ('uploads//ns/...'), or trailing slashes that path.Clean would normalize differently.","commonSituations":"String concatenation producing duplicate slashes; inserting current-directory segments; building keys from URL paths that were not normalized.","solutions":["Run path.Clean(key) before use and confirm it equals the key; if not, rebuild the key.","Construct keys programmatically from the 4 exact segments rather than concatenating raw strings.","Collapse duplicate separators with path.Join of the individual segments."],"exampleFix":"// before\nkey := \"uploads/my-ns//\" + id + \"/./file.bin\"\n// after\nkey := path.Join(\"uploads\", \"my-ns\", id, \"file.bin\")","handlingStrategy":"validation","validationCode":"if path.Clean(key) != key {\n\treturn fmt.Errorf(\"key %q is not canonical\", key)\n}","typeGuard":"func isCanonicalKey(key string) bool { return path.Clean(key) == key }","tryCatchPattern":"if err := utils.ValidateUploadedArtifactKey(ns, key); err != nil {\n\tif strings.Contains(err.Error(), \"canonical form\") {\n\t\tkey = path.Clean(key)\n\t\t// re-validate before proceeding\n\t}\n}","preventionTips":["Build keys with path.Join over the exact 4 segments instead of string concatenation.","Run path.Clean on keys derived from external input.","Reject keys with './' or duplicate slashes at construction time."],"tags":["artifacts","validation","path"],"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"}