T09 · Insecure Skill Coding Practices
Warning
- Location
- references/src-gateway-server-methods-agents-ts.txt:440
- Finding
- Avatar uploads trust client-controlled file type metadata without validating image content<![CDATA[ ## Vulnerability Details **File Location**: `references/src-gateway-server-methods-agents-ts.txt:440-464`, with the upload handler at lines `649-675` **Vulnerability Type**: Unrestricted or insufficiently validated file upload **Risk Level**: Medium ### Vulnerable Code ```ts function avatarExtensionForUpload(filename: string, contentType?: string | null): string | null { const lower = filename.toLowerCase(); if (lower.endsWith(".png") || contentType === "image/png") return ".png"; if (lower.endsWith(".jpg") || lower.endsWith(".jpeg") || contentType === "image/jpeg") { return ".jpg"; } if (lower.endsWith(".webp") || contentType === "image/webp") return ".webp"; if (lower.endsWith(".gif") || contentType === "image/gif") return ".gif"; return null; } async function writeAgentAvatarFile(params: { cfg: ReturnType<typeof loadConfig>; agentId: string; bytes: Buffer; extension: string; }): Promise<{ avatar: string; avatarUrl: string }> { const workspace = resolveAgentWorkspaceDir(params.cfg, params.agentId); const avatarDir = path.join(workspace, "avatars"); await fs.mkdir(avatarDir, { recursive: true }); const filename = `profile${params.extension}`; const absolutePath = path.join(avatarDir, filename); await fs.writeFile(absolutePath, params.bytes); const identityPath = path.join(workspace, DEFAULT_IDENTITY_FILENAME); await fs.appendFile(identityPath, `\n- Avatar: avatars/${filename}\n`, "utf-8"); return { avatar: `avatars/${filename}`, avatarUrl: `/avatar/${params.agentId}`, }; } ``` The corresponding upload handler derives the extension entirely from request metadata: ```ts const decoded = decodeAvatarUploadData(String(params.data ?? "")); if (!decoded || decoded.bytes.length === 0) { respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid avatar image data")); return; } if (decoded.bytes.length > 2 * 1024 * 1024) { respond(false, undefined, errorShape(ErrorCodes.INVALID_REQ ...[truncated 2946 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Validate decoded content using trusted image parsing rather than relying on filename or MIME metadata. 2. Check format-specific magic bytes before invoking an image decoder, but do not treat signature checks alone as sufficient. 3. Fully decode and re-encode accepted images into a canonical server-selected format, such as PNG or WEBP. This removes trailing payloads, metadata, and most polyglot structures. 4. Reject files with inconsistent filename, declared MIME type, detected format, or decoding results. 5. Enforce maximum pixel dimensions, frame counts, decompressed size, and animation duration to prevent image decompression bombs. 6. Serve avatar files with a fixed validated `Content-Type`, `X-Content-Type-Options: nosniff`, and a restrictive Content Security Policy where applicable. 7. Add tests for fake extensions, fake MIME types, malformed files, polyglot files, oversized dimensions, animated-image abuse, and valid supported formats. ]]>
