{"record":{"id":"755699259fe132ab","repo":"Leantime/leantime","slug":"notification-plugin-zip-invalid-arg","errorCode":null,"errorMessage":"notification.plugin_zip_invalid_arg","messagePattern":"notification\\.plugin_zip_invalid_arg","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"app/Domain/Plugins/Services/Plugins.php","lineNumber":737,"sourceCode":"        }\n\n        if (\n            is_dir($pluginDir = \"{$this->pluginDirectory}{$foldername}\")\n            && ! File::deleteDirectory($pluginDir)\n        ) {\n            throw new \\Exception(__('notification.plugin_cant_remove'));\n        }\n\n        if (! mkdir($pluginDir) && ! is_dir($pluginDir)) {\n            throw new \\RuntimeException(sprintf('Directory \"%s\" was not created', $pluginDir));\n        }\n\n        $zip = new \\ZipArchive;\n\n        match ($zip->open($temporaryFile)) {\n            \\ZipArchive::ER_EXISTS => throw new \\Exception(__('notification.plugin_zip_exists')),\n            \\ZipArchive::ER_INCONS => throw new \\Exception(__('notification.plugin_zip_inconsistent')),\n            \\ZipArchive::ER_INVAL => throw new \\Exception(__('notification.plugin_zip_invalid_arg')),\n            \\ZipArchive::ER_MEMORY => throw new \\Exception(__('notification.plugin_zip_malloc')),\n            \\ZipArchive::ER_NOENT => throw new \\Exception(__('notification.plugin_zip_no_file')),\n            \\ZipArchive::ER_NOZIP => throw new \\Exception(__('notification.plugin_zip_not_zip')),\n            \\ZipArchive::ER_OPEN => throw new \\Exception(__('notification.plugin_zip_cant_open')),\n            \\ZipArchive::ER_READ => throw new \\Exception(__('notification.plugin_zip_read_err')),\n            \\ZipArchive::ER_SEEK => throw new \\Exception(__('notification.plugin_zip_seek_err')),\n            default => throw new \\Exception(__('notification.plugin_zip_unknown_err')),\n            true => null,\n        };\n\n        if (! $zip->extractTo($pluginDir)) {\n            throw new \\Exception(__('notification.plugin_zip_cant_extract'));\n        }\n\n        $zip->close();\n\n        unlink($temporaryFile);\n","sourceCodeStart":719,"sourceCodeEnd":755,"githubUrl":"https://github.com/Leantime/leantime/blob/9a9f49f1008f4782b30f6723c54228f4f992e636/app/Domain/Plugins/Services/Plugins.php#L719-L755","documentation":"The match maps ZipArchive::ER_INVAL to 'Zip: Invalid argument'. libzip returns ER_INVAL for an invalid filename/flags combination or when the archive contains invalid entries. Here the filename handed to open() is derived by naive substr() parsing of the Content-Disposition header (everything after 'filename='), so a missing, empty, or malformed header produces an empty or bogus path like '/tmp/' and open() fails with ER_INVAL.","triggerScenarios":"installMarketplacePlugin() where the marketplace response's Content-Disposition header lacks 'filename=' (substr yields '' -> path '/tmp/'), contains quotes/semicolons the substr() logic does not strip, or carries path separators; alternatively the zip contains entries with invalid names.","commonSituations":"Marketplace API or CDN changing its Content-Disposition format; a proxy stripping the header; header value like 'attachment; filename=\"Name.zip\"; size=123' leaving trailing quotes/params in the parsed name.","solutions":["curl -sD - -o /dev/null the download endpoint (with license headers) and inspect the exact Content-Disposition value.","Patch the parsing to a regex that handles quotes and trailing parameters (see exampleFix) and fails loudly on absence.","If the header is genuinely absent server-side, report it - the plugin cannot be installed under this Leantime version otherwise.","Clear any stale /tmp file created from a previous malformed attempt."],"exampleFix":"// before\n$filename = $response->header('Content-Disposition');\n$filename = substr($filename, strpos($filename, 'filename=') + 9);\n\n// after: regex that handles quotes/parameters and fails loudly when absent\nif (! preg_match('/filename=\"?([^\";]+)\\.zip\"?/i', (string) $response->header('Content-Disposition'), $m)) {\n    throw new \\Exception('Malformed Content-Disposition header from marketplace');\n}\n$filename = $m[1];","handlingStrategy":"validation","validationCode":"if (! preg_match('/filename=\"?([^\";]+\\.zip)\"?/i', (string) $response->header('Content-Disposition'), $m)) {\n    throw new RuntimeException('Marketplace response lacks a parsable filename - cannot install');\n}\n$foldername = Str::studly(basename($m[1], '.zip'));","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Validate Content-Disposition with a regex rather than strpos/substr before using it as a filename.","Fail loudly when the header is missing instead of deriving an empty path.","Watch marketplace API changes when upgrading Leantime versions."],"tags":["plugins","ziparchive","http-headers","content-disposition"],"backgroundTag":"malformed-content-disposition-header","analyzedSha":"9a9f49f1008f4782b30f6723c54228f4f992e636","analyzedAt":"2026-08-21T02:37:38.966Z","schemaVersion":2},"datasetVersion":"2026-08-21T03:17:12.404Z"}