Root cause
Path & link handling
refactor: remove mock data and enhance Rive runtime integration
How AI contributed: New attack surfacePublished Aug 8, 2026 in
astralisone/rive-mcp-server-core
importRiveFile.ts (packages/mcp-server/src/tools/importRiveFile.ts) is an MCP tool (wired into CallToolRequest at index.ts:224) that takes a libraryId argument and passes it unvalidated into addComponentToLibrary()/saveLibrary(). saveLibrary() in packages/mcp-server/src/utils/storage.ts builds the manifest write path as path.join(config.manifestsPath, ${library.id}.library.json) and fs.writeFile()s it. path.join does not collapse '..' segments, so a libraryId like '../../etc/x' writes arbitrary JSON files anywhere the server process can write (CWE-22). No sanitization of libraryId exists anywhere in the flow; the default 'imported-components' is only a fallback when the argument is omitted.
Root cause
refactor: remove mock data and enhance Rive runtime integration
Fix status
No minimum fix commit has been established for this finding.
Fix status unresolved
Case facts
@@ -0,0 +1,180 @@+import { parseRiveFile } from '../utils/riveParser';+import { saveLibrary, addComponentToLibrary } from '../utils/storage';+import { MCPToolResponse, RiveComponent, RiveLibrary } from '../types';+import { logger } from '../utils/logger';+import * as path from 'path';+import * as fs from 'fs/promises';++export interface ImportRiveFileParams {+ filePath: string;+ libraryId?: string;+ componentName?: string;+ componentId?: string;+}++export interface ImportRiveFileResponse {+ component: RiveComponent;+ library: RiveLibrary;+ manifestCreated: boolean;+}++/**+ * Import a .riv file and auto-generate manifest+ */+export async function importRiveFile(+ params: ImportRiveFileParams+): Promise<MCPToolResponse<ImportRiveFileResponse>> {+ logger.info('importRiveFile called', {+ filePath: params.filePath,+ libraryId: params.libraryId+ });++ try {+ // Validate file path+ if (!params.filePath) {+ logger.warn('importRiveFile called without file path');+ return {+ status: 'error',+ tool: 'importRiveFile',+ error: {+ code: 'MISSING_PARAMETER',+ message: 'File path is required',+ },+ timestamp: new Date().toISOString(),+ };+ }++ // Check if file exists+ logger.debug('Checking if file exists', { filePath: params.filePath });+ try {+ await fs.access(params.filePath);+ } catch {+ logger.warn('File not found', { filePath: params.filePath });+ return {+ status: 'error',+ tool: 'importRiveFile',+ error: {+ code: 'FILE_NOT_FOUND',+ message: `File not found: ${params.filePath}`,+ },+ timestamp: new Date().toISOString(),+ };+ }++ // Validate it's a .riv file+ if (!params.filePath.endsWith('.riv')) {+ logger.warn('File is not a .riv file', { filePath: params.filePath });+ return {+ status: 'error',+ tool: 'importRiveFile',+ error: {+ code: 'INVALID_FILE_TYPE',+ message: 'File must be a .riv file',+ },+ timestamp: new Date().toISOString(),+ };+ }++ // Parse the .riv file to extract runtime surface+ logger.debug('Parsing .riv file', { filePath: params.filePath });+ const runtimeSurface = await parseRiveFile(params.filePath);++ // Generate component ID from filename if not provided+ const fileName = path.basename(params.filePath, '.riv');+ const componentId = params.componentId || fileName.toLowerCase().replace(/[^a-z0-9]+/g, '-');+ const componentName = params.componentName || fileName;++ logger.debug('Generated component metadata', {+ componentId,+ componentName,@@ -7,6 +7,8 @@ import { getComponentDetail } from "./tools/getComponentDetail.js"; import { getRuntimeSurface } from "./tools/getRuntimeSurface.js"; import { generateWrapper } from "./tools/generateWrapper.js"; import { composeScene } from "./tools/composeScene.js";+import { importRiveFile } from "./tools/importRiveFile.js";+import { logger } from "./utils/logger.js"; const server = new Server( {@@ -22,6 +24,7 @@ const server = new Server( // Register tool list handler server.setRequestHandler(ListToolsRequestSchema, async () => {+ logger.debug('ListTools request received'); return { tools: [ {@@ -155,6 +158,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { }, required: ["name", "components"] }+ },+ {+ name: "import_rive_file",+ description: "Import a .riv file and auto-generate manifest from it. Accepts file path and extracts all metadata automatically.",+ inputSchema: {+ type: "object",+ properties: {+ filePath: {+ type: "string",+ description: "Absolute path to the .riv file"+ },+ libraryId: {+ type: "string",+ description: "Optional library ID (defaults to 'imported-components')"+ },+ componentName: {+ type: "string",+ description: "Optional component name (defaults to filename)"+ },+ componentId: {+ type: "string",+ description: "Optional component ID (defaults to filename)"+ }+ },+ required: ["filePath"]+ } } ] };@@ -164,6 +193,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; + logger.info('Tool call received', {+ tool: name,+ hasArguments: Boolean(args && Object.keys(args).length > 0)+ });+ try { let result; @@ -186,10 +220,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { case "compose_scene": result = await composeScene(args as any); break;+ case "import_rive_file":+ result = await importRiveFile(args as any);+ break; default:+ logger.warn('Unknown tool requested', { tool: name }); throw new Error(`Unknown tool: ${name}`); } + logger.debug('Tool call completed', {+ tool: name,+ status: result.status || 'unknown'+ });+ return { content: [ {@@ -199,6 +242,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { ] }; } catch (error) {+ logger.error('Tool call failed', {+ tool: name,+ error: error instanceof Error ? error.message : String(error),+ stack: error instanceof Error ? error.stack : undefined@@ -20,8 +20,8 @@ interface StorageConfig { } const defaultConfig: StorageConfig = {- manifestsPath: process.env.MANIFESTS_PATH || path.join(process.cwd(), 'libs', 'rive-manifests'),- assetsPath: process.env.ASSETS_PATH || path.join(process.cwd(), 'assets', 'rive'),+ manifestsPath: process.env.MANIFESTS_PATH || path.join(process.cwd(), 'manifests'),+ assetsPath: process.env.ASSETS_PATH || path.join(process.cwd(), 'assets'), }; /**