argoproj/argo-workflows · error

Invalid filename

Error message

Invalid filename

What it means

The uploaded filename is sanitized with path.Base (after normalizing Windows backslashes) to prevent path traversal. If the resulting base name is ".", "/", or empty, the server refuses the upload with HTTP 400 "Invalid filename". This blocks filenames like "..", "./", or a filename consisting only of separators.

Source

Thrown at server/artifacts/artifact_server.go:235

				"artifactName": artifactName,
			}).Info(ctx, "Resolved artifact location from default repository")
		}
	}

	// Check if the artifact has a location configured (S3, GCS, etc.)
	if !artifactCopy.HasLocation() {
		http.Error(w, fmt.Sprintf("Artifact '%s' does not have a storage location configured (s3, gcs, azure, oss). Please configure a storage location in the WorkflowTemplate or set up a default artifact repository.", artifactName), http.StatusBadRequest)
		return
	}

	// Generate unique key for the artifact
	uploadUUID := uuid.NewString()
	originalKey, _ := artifactCopy.GetKey()
	// Sanitize filename to prevent path traversal attacks. path.Base only
	// recognises '/' as a separator, so normalise Windows-style '\' first.
	sanitizedFilename := path.Base(strings.ReplaceAll(header.Filename, "\\", "/"))
	if sanitizedFilename == "." || sanitizedFilename == "/" || sanitizedFilename == "" {
		http.Error(w, "Invalid filename", http.StatusBadRequest)
		return
	}
	// Replace the key with uploaded file path under uploads/
	newKey := fmt.Sprintf("uploads/%s/%s/%s", namespace, uploadUUID, sanitizedFilename)
	if validateErr := sutils.ValidateUploadedArtifactKey(namespace, newKey); validateErr != nil {
		a.serverInternalError(ctx, fmt.Errorf("generated artifact key failed self-validation: %w", validateErr), w)
		return
	}

	// Create a copy of the artifact for uploading (using artifactCopy which has resolved location)
	outputArtifact := artifactCopy.DeepCopy()
	if setErr := outputArtifact.SetKey(newKey); setErr != nil {
		http.Error(w, fmt.Sprintf("Failed to set artifact key: %v", setErr), http.StatusInternalServerError)
		return
	}

	a.logger.WithFields(logging.Fields{
		"originalKey": originalKey,

View on GitHub (pinned to 35bff19146)

Solutions

  1. Send a real, non-empty base filename in the multipart part (e.g. file=@data.bin, not a directory path).
  2. Strip trailing slashes and pass only the base name explicitly if your client builds the multipart part manually.
  3. If uploading a directory, iterate files and upload each with its own basename.

Example fix

// before (client sends a directory-like name)
writer.CreateFormFile("file", "mydir/")
// after
writer.CreateFormFile("file", "myfile.txt")
Defensive patterns

Strategy: validation

Validate before calling

const base = filename.replaceAll('\\', '/').split('/').pop() || '';
if (!base || base === '.' || base === '..') {
  throw new Error(`invalid upload filename: ${JSON.stringify(filename)}`);
}
form.append('file', file, base);

Type guard

function isValidUploadFilename(name) {
  const base = String(name).replaceAll('\\', '/').split('/').pop();
  return typeof base === 'string' && base.length > 0 && base !== '.' && base !== '..';
}

Try / catch

if (!isValidUploadFilename(file.name)) {
  return reject(new Error('client-side: filename normalizes to empty path, refusing upload'));
}

Prevention

When it happens

Trigger: Uploading a file part whose filename is ".." or "."; a client that sends a filename ending in a separator (e.g. "dir/"); a filename that reduces to empty after backslash-to-slash normalization and base extraction; maliciously crafted multipart parts attempting path traversal (../../etc/passwd).

Common situations: Directory uploads where directory entries are sent with trailing slashes; scripted uploads using an empty or placeholder filename; security scanners probing the endpoint with traversal payloads.

Related errors


AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03). Data as JSON: /api/errors/d9b7e57d452396f3. Report an issue: GitHub.