jumpserver/jumpserver · error · ValidationError

Invalid zip file: {}

Error message

Invalid zip file: {}

What it means

Thrown by VirtualApp extract_zip_pkg when safe_extract_zip fails while unpacking an uploaded .zip (bad archive, unsupported compression, or a path-traversal attempt caught by the safe-extract guard). The RuntimeError is converted into DRF ValidationError so the API returns 400 with 'Invalid zip file: <reason>`.

Source

Thrown at apps/terminal/api/virtualapp/virtualapp.py:49

        if extract_to and os.path.exists(extract_to):
            shutil.rmtree(extract_to)

    def extract_zip_pkg(self):
        serializer = self.get_serializer(data=self.request.data)
        serializer.is_valid(raise_exception=True)
        file = serializer.validated_data['file']
        save_to = 'virtual_apps/{}'.format(file.name + '.tmp.zip')
        if default_storage.exists(save_to):
            default_storage.delete(save_to)
        rel_path = default_storage.save(save_to, file)
        path = default_storage.path(rel_path)
        extract_to = default_storage.path('virtual_apps/{}.tmp'.format(file.name))
        if os.path.exists(extract_to):
            shutil.rmtree(extract_to)
        try:
            safe_extract_zip(path, extract_to)
        except RuntimeError as e:
            raise ValidationError({'error': _('Invalid zip file') + ': {}'.format(e)})
        tmp_dir = VirtualApp.locate_pkg_root(extract_to, file.name)
        return tmp_dir, rel_path, extract_to

    @action(detail=False, methods=['post'], serializer_class=FileSerializer)
    def upload(self, request, *args, **kwargs):
        rel_path = None
        extract_to = None
        try:
            tmp_dir, rel_path, extract_to = self.extract_zip_pkg()
            manifest = VirtualApp.validate_pkg(tmp_dir)
            name = manifest['name']
            instance = VirtualApp.objects.filter(name=name).first()
            if instance:
                return Response({'error': 'virtual app already exists: {}'.format(name)}, status=400)

            app, serializer = VirtualApp.install_from_dir(tmp_dir)
            return Response(serializer.data, status=201)
        finally:

View on GitHub (pinned to 6ec464fabd)

Solutions

  1. Re-create the zip with standard tooling (zip -r app.zip . from inside the package dir) and re-upload
  2. Inspect the archive for absolute paths or .. entries and repack with relative paths
  3. Verify integrity locally first: unzip -t app.zip
  4. If it persists, check server storage permissions/paths used by default_storage

Example fix

# before: zipping including parent dirs / absolute entries
zip -r /tmp/myapp.zip /home/user/myapp/

# after: build the archive from inside the package root
cd myapp && zip -r ../myapp.zip . && curl -F 'file=@../myapp.zip' .../virtual-apps/upload/
Defensive patterns

Strategy: validation

Validate before calling

import zipfile
with zipfile.ZipFile(path) as z:
    assert z.testzip() is None, 'corrupt zip'
    bad = [n for n in z.namelist() if n.startswith('/') or '..' in n.split('/')]
    assert not bad, f'unsafe entries: {bad}'

Type guard

def is_safe_zip(path: str) -> bool:
    import zipfile
    try:
        with zipfile.ZipFile(path) as z:
            return z.testzip() is None and not any(
                n.startswith('/') or '..' in n.split('/') for n in z.namelist())
    except zipfile.BadZipFile:
        return False

Try / catch

try:
    resp = client.post('.../virtual-apps/upload/', files={'file': f})
except HTTPError as e:
    detail = e.response.json().get('error', '')
    if detail.startswith('Invalid zip file'):
        repack_archive_and_retry()
    raise

Prevention

When it happens

Trigger: POST upload to the virtual-app upload action with a corrupted zip, a zip containing absolute paths or '../' entries (rejected by safe_extract_zip), or a file that is not actually a zip. Also triggered by disk/permission errors surfaced as RuntimeError during extraction.

Common situations: Repacking an applet on Windows producing archives with backslash entries; truncated uploads; zips built with unusual tools; deliberately malicious archives blocked by the safe-extraction check.

Related errors


AI-assisted analysis of jumpserver/jumpserver@6ec464fabd (2026-08-28). Data as JSON: /api/errors/f5a19b02fc197c01. Report an issue: GitHub.