Workspace File Copy API
API reference for the workspace file copy and write operations, including CWE-22 path traversal protection.
Workspace File Copy API
Source:
workspace-server/internal/handlers/container_files.go+templates.goHandler:TemplatesHandler.WriteFile→copyFilesToContainerSecurity: CWE-22 path traversal protection (PRs #1267, #1270, #1271)
copyFilesToContainer is the internal Go implementation that powers workspace file write operations. It is not called directly by API clients — clients reach it through the HTTP handler PUT /workspaces/:id/files/*path.
Endpoint Overview
PUT /workspaces/:id/files/*path writes a single file to a workspace container or its config volume.
PUT /workspaces/:id/files/*path
Authorization: Bearer <workspace-token>
Content-Type: application/json
{
"content": "string"
}The handler (TemplatesHandler.WriteFile) validates the path, then routes to one of two backends:
| Workspace state | Backend | Method |
|---|---|---|
| Container running | Docker CopyToContainer (tar) | copyFilesToContainer |
| Container offline | Ephemeral Alpine container | writeViaEphemeral → copyFilesToContainer |
Both paths use copyFilesToContainer internally. The ephemeral container path mounts the config volume as /configs and calls the same function, so CWE-22 protection applies regardless of container state.
Function Signature
func (h *TemplatesHandler) copyFilesToContainer(
ctx context.Context,
containerName string,
destPath string,
files map[string]string, // filename → content
) error| Parameter | Type | Description |
|---|---|---|
ctx | context.Context | Request-scoped context |
containerName | string | Docker container name or ID |
destPath | string | Target directory inside the container (typically /configs) |
files | map[string]string | Map of relative filenames to file contents |
Parameters
containerName
The running container for the workspace. Resolved by TemplatesHandler.findContainer, which checks three candidates in order:
- Platform provisioner naming convention (
ws-<uuid>) - The full workspace container ID
- The workspace name from the database (spaces replaced with dashes)
If the container is not running, findContainer returns "" and the handler falls back to writeViaEphemeral.
destPath
The directory inside the container where files are written. In normal operation this is /configs, which is mounted from the platform-managed config volume. All file operations are constrained to this volume.
files (map[string]string)
A map of relative filenames to their string content. File names are relative paths only — absolute paths and .. traversal sequences are rejected before the tar header is written.
Security Notes
CWE-22 Path Traversal Protection
PRs #1267, #1270, #1271 added path traversal protection at the tar-archive-write boundary.
Before these PRs, copyFilesToContainer used raw map keys as tar header names without validation:
// Before — UNSAFE
header := &tar.Header{
Name: name, // name came directly from map key
Mode: 0644,
Size: int64(len(data)),
}A malicious caller embedding ../ in a file name could write outside the volume mount. Now:
// After — SAFE (PRs #1267 / #1270)
clean := filepath.Clean(name)
if filepath.IsAbs(clean) || strings.HasPrefix(clean, "..") {
return fmt.Errorf("unsafe file path in archive: %s", name)
}
archiveName := filepath.Join(destPath, name)
header := &tar.Header{
Name: archiveName, // always inside destPath
Mode: 0644,
Size: int64(len(data)),
}The validation works in three stages:
filepath.Cleannormalizes the path (removes redundant separators, resolves.).- Absolute path check (
filepath.IsAbs) rejects any path that resolves to an absolute OS path. ..prefix check (strings.HasPrefix) rejects paths that would escape the destination via parent-directory traversal.
The resulting archiveName is always inside destPath, so the tar header can never write outside the mounted volume regardless of input.
Defense in depth:
WriteFile(the HTTP handler) also callsvalidateRelPath(filePath)before passing the path tocopyFilesToContainer. This closes the gap for any future caller that bypasses the handler-level check. Do not remove handler-levelvalidateRelPathwhen modifying this code.
Handler-Level Validation (validateRelPath)
func validateRelPath(relPath string) error {
clean := filepath.Clean(relPath)
if filepath.IsAbs(clean) || strings.HasPrefix(clean, "..") {
return fmt.Errorf("path traversal blocked: %s", relPath)
}
return nil
}validateRelPath is called at the start of every file operation handler (WriteFile, ReadFile, DeleteFile, ListFiles). Invalid paths return 400 Bad Request with {"error": "invalid path"}.
Allowed root paths are also allow-listed: root must be one of /configs, /workspace, /home, or /plugins. Other values return 400 Bad Request.
Error Codes
copyFilesToContainer returns errors directly. The WriteFile HTTP handler wraps them:
| HTTP status | Condition | Response body |
|---|---|---|
400 Bad Request | validateRelPath rejects the path (traversal attempt) | {"error": "invalid path"} |
400 Bad Request | Malformed JSON body | {"error": "invalid request body"} |
404 Not Found | Workspace not found in database | {"error": "workspace not found"} |
500 Internal Server Error | Docker unavailable | {"error": "failed to write file: docker not available"} |
500 Internal Server Error | Tar header write failure | {"error": "failed to write file: failed to write tar header for <name>: ..."} |
500 Internal Server Error | Docker CopyToContainer failure | {"error": "failed to write file: <docker error>"} |
Example
Write a file to a workspace
curl -X PUT https://platform.example.com/workspaces/ws-abc123/files/claude.md \
-H "Authorization: Bearer <workspace-token>" \
-H "Content-Type: application/json" \
-d '{
"content": "# My Agent\n\nThis agent specializes in code review.\n"
}'Success response (200 OK):
{
"status": "saved",
"path": "claude.md"
}Path traversal rejected
curl -X PUT https://platform.example.com/workspaces/ws-abc123/files/../../etc/passwd \
-H "Authorization: Bearer <workspace-token>" \
-H "Content-Type: application/json" \
-d '{"content": "hacked"}'Rejection response (400 Bad Request):
{
"error": "invalid path"
}Related
- Platform API Reference — full API endpoint table
- Workspace Runtime — runtime environment model
workspace-server/internal/handlers/templates.go—WriteFile,validateRelPathworkspace-server/internal/handlers/container_files.go—copyFilesToContainer,writeViaEphemeral