Skip to main content
Glama
REMnux

REMnux MCP Server

Official
by REMnux

extract_archive

Extract files from compressed archives (.zip, .7z, .rar) for malware analysis, automatically trying common passwords if protected.

Instructions

Extract files from a compressed archive (.zip, .7z, .rar). Automatically tries common malware passwords if the archive is password-protected. Returns list of extracted files.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
archive_fileYesPath to archive file relative to samples directory (e.g., 'sample.zip')
passwordNoOptional password to try first. If not provided, tries common passwords from built-in list.
output_subdirNoOptional subdirectory name for extracted files. Defaults to archive filename without extension.

Implementation Reference

  • Main handler function handleExtractArchive that validates inputs, checks archive type, validates paths for security, calls extractArchive, and formats the response with extracted files list and password used
    export async function handleExtractArchive(
      deps: HandlerDeps,
      args: ExtractArchiveArgs
    ) {
      const startTime = Date.now();
      const { connector, config } = deps;
    
      // Validate archive file path (skip unless --sandbox)
      if (!config.noSandbox) {
        const validation = validateFilePath(args.archive_file, config.samplesDir);
        if (!validation.safe) {
          return formatError("extract_archive", new REMnuxError(
            validation.error || "Invalid archive file path",
            "INVALID_PATH",
            "validation",
            "Use a relative path within the samples directory",
          ), startTime);
        }
      }
    
      // Security: Validate output subdirectory if provided
      if (args.output_subdir && !config.noSandbox) {
        if (args.output_subdir.trim() === "") {
          return formatError("extract_archive", new REMnuxError(
            "output_subdir cannot be blank",
            "INVALID_SUBDIR",
            "validation",
            "Provide a non-empty subdirectory name without special characters",
          ), startTime);
        }
        if (args.output_subdir.includes("..") || args.output_subdir.includes("/") || args.output_subdir.includes("\\")) {
          return formatError("extract_archive", new REMnuxError(
            "Invalid output subdirectory name",
            "INVALID_SUBDIR",
            "validation",
            "Subdirectory must not contain path separators or '..'",
          ), startTime);
        }
        if (/[;&|`$\n\r'"\\]/.test(args.output_subdir)) {
          return formatError("extract_archive", new REMnuxError(
            "Output subdirectory contains invalid characters",
            "INVALID_SUBDIR",
            "validation",
            "Use only alphanumeric characters, hyphens, and underscores",
          ), startTime);
        }
      }
    
      // Verify archive type is supported
      const archiveType = detectArchiveType(args.archive_file);
      if (!archiveType) {
        return formatError("extract_archive", new REMnuxError(
          "Unsupported archive format. Supported: .zip, .7z, .rar",
          "UNSUPPORTED_FORMAT",
          "validation",
          "Rename the file with a supported extension or use a different tool",
        ), startTime);
      }
    
      // Build full archive path
      const archivePath = `${config.samplesDir}/${args.archive_file}`;
    
      try {
        const result = await extractArchive(
          connector,
          archivePath,
          config.samplesDir,
          args.password,
          args.output_subdir
        );
    
        if (result.success) {
          // Store archive metadata for download_file to reuse
          if (result.password && archiveType) {
            deps.sessionState.storeArchiveInfo(
              args.archive_file,
              result.files,
              archiveType,
              result.password
            );
          }
    
          return formatResponse("extract_archive", {
            extracted_to: result.outputDir,
            files: result.files,
            file_count: result.files.length,
            password_used: result.password || "(none - archive was not encrypted)",
          }, startTime);
        } else {
          return formatError("extract_archive", new REMnuxError(
            result.error || "Extraction failed",
            "EXTRACTION_FAILED",
            "tool_failure",
            "Check that the archive is valid and not corrupted",
          ), startTime);
        }
      } catch (error) {
        return formatError("extract_archive", toREMnuxError(error, deps.config.mode), startTime);
      }
    }
  • Input schema definition for extract_archive tool using Zod validation, defining archive_file, password (optional), and output_subdir (optional) parameters
    export const extractArchiveSchema = z.object({
      archive_file: z.string().describe("Path to archive file relative to samples directory (e.g., 'sample.zip')"),
      password: z.string().optional().describe("Optional password to try first. If not provided, tries common passwords from built-in list."),
      output_subdir: z.string().optional().describe("Optional subdirectory name for extracted files. Defaults to archive filename without extension."),
    });
    export type ExtractArchiveArgs = z.infer<typeof extractArchiveSchema>;
  • src/index.ts:119-125 (registration)
    Tool registration in MCP server, registering extract_archive with its description, schema, and handler function
    // Tool: extract_archive - Extract files from compressed archives
    server.tool(
      "extract_archive",
      "Extract files from a compressed archive (.zip, .7z, .rar). Automatically tries common malware passwords if the archive is password-protected. Returns list of extracted files.",
      extractArchiveSchema.shape,
      (args) => handleExtractArchive(deps, args)
    );
  • Core extraction logic in extractArchive function that supports .zip, .7z, and .rar formats with automatic password detection, zip-slip protection, and pre-extraction validation of archive entries
    export async function extractArchive(
      connector: Connector,
      archivePath: string,
      samplesDir: string,
      customPassword?: string,
      outputSubdir?: string
    ): Promise<ExtractionResult> {
      // Detect archive type
      const archiveType = detectArchiveType(archivePath);
      if (!archiveType) {
        return {
          success: false,
          files: [],
          error: `Unsupported archive format: ${extname(archivePath)}`,
          outputDir: "",
        };
      }
    
      // Determine output directory
      const archiveName = basename(archivePath, extname(archivePath));
      const subdir = outputSubdir || archiveName;
      const outputDir = join(samplesDir, subdir);
    
      // Create output directory
      const mkdirResult = await connector.execute(["mkdir", "-p", outputDir], {
        timeout: 10000,
      });
      if (mkdirResult.exitCode !== 0) {
        return {
          success: false,
          files: [],
          error: `Failed to create output directory: ${mkdirResult.stderr}`,
          outputDir,
        };
      }
    
      // Pre-extraction zip-slip check: inspect archive entries before extracting
      const entries = await listArchiveEntries(connector, archivePath, archiveType);
      if (entries.length > 0) {
        const traversalEntries = checkEntriesForTraversal(entries);
        if (traversalEntries.length > 0) {
          return {
            success: false,
            files: [],
            error: `Archive contains path traversal entries (zip-slip): ${traversalEntries.slice(0, 3).join(", ")}${traversalEntries.length > 3 ? ` and ${traversalEntries.length - 3} more` : ""}`,
            outputDir,
          };
        }
      }
    
      // Build password list to try
      const passwordsToTry: (string | undefined)[] = [];
    
      // 1. Custom password first if provided
      if (customPassword) {
        passwordsToTry.push(customPassword);
      }
    
      // 2. Try without password (archive might not be encrypted)
      passwordsToTry.push(undefined);
    
      // 3. Add passwords from config file
      const configPasswords = loadPasswordList();
      for (const pwd of configPasswords) {
        if (pwd !== customPassword) {
          // Avoid duplicates
          passwordsToTry.push(pwd);
        }
      }
    
      // Try extraction with each password
      let lastError = "";
      for (const password of passwordsToTry) {
        try {
          const cmd = getExtractionCommand(archiveType, archivePath, outputDir, password);
          const result = await connector.execute(cmd, { timeout: 120000 });
    
          if (result.exitCode === 0) {
            // Success! List extracted files
            const files = await listExtractedFiles(connector, outputDir);
    
            // Validate for zip-slip attacks (path traversal in archive entries)
            const escapeAttempts = validateExtractedPaths(files, outputDir);
            if (escapeAttempts.length > 0) {
              // Clean up potentially malicious files
              await connector.execute(["rm", "-rf", outputDir], { timeout: 30000 });
              return {
                success: false,
                files: [],
                error: `Archive contains path escape attempts (zip-slip): ${escapeAttempts.slice(0, 3).join(", ")}${escapeAttempts.length > 3 ? ` and ${escapeAttempts.length - 3} more` : ""}`,
                outputDir,
              };
            }
    
            return {
              success: true,
              files,
              password: password, // undefined if no password was needed
              outputDir,
            };
          }
    
          // Check if this is a password error
          if (isWrongPasswordError(result, archiveType)) {
            lastError = "Incorrect password";
            continue; // Try next password
          }
    
          // Some other error - might still try other passwords for encrypted archives
          lastError = result.stderr || result.stdout || "Unknown extraction error";
        } catch (err) {
          lastError = err instanceof Error ? err.message : "Extraction failed";
        }
      }
    
      // All passwords failed
      return {
        success: false,
        files: [],
        error: `Extraction failed: ${lastError}. Tried ${passwordsToTry.length} password(s).`,
        outputDir,
      };
    }
  • Helper function detectArchiveType that identifies archive format from file extension (.zip, .7z, .rar)
    export function detectArchiveType(filename: string): ArchiveType {
      const ext = extname(filename).toLowerCase();
      switch (ext) {
        case ".zip":
          return "zip";
        case ".7z":
          return "7z";
        case ".rar":
          return "rar";
        default:
          return null;
      }
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/REMnux/remnux-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server