[Security] Unhandled exception in .metadata.json parsing terminates ranger · Issue #3232 · ranger/ranger · GitHub
Skip to content

[Security] Unhandled exception in .metadata.json parsing terminates ranger #3232

Open

Description

@sectroyer

Summary

Title Unhandled exception in .metadata.json parsing terminates ranger
CWE CWE-248 (Uncaught Exception)
Affected ranger 1.9.4 (git 51e19b8) and likely all prior versions
Tested ranger 1.9.4, Python 3.13.12, x86_64 Linux

When ranger browses a directory containing a malformed .metadata.json file, the JSON parser raises a ValueError that is not caught anywhere in the draw call chain. The exception propagates to the outermost handler in main.py, which prints a traceback and exits. The user loses their ranger session without any warning or recovery prompt.


Description

_get_metafile_content() in ranger/core/metadata.py reads and parses .metadata.json files. When json.load() fails, it re-raises as a ValueError (line 126):

except ValueError:
    raise ValueError("Failed decoding JSON file %s" % metafile)

The public API get_metadata() only guards against KeyError. ValueError is not caught at any level of the call chain:

def get_metadata(self, filename):
    try:
        return ostruct(copy.deepcopy(self.metadata_cache[filename]))
    except KeyError:
        try:
            return ostruct(copy.deepcopy(self._get_entry(filename)))
        except KeyError:       # ValueError not caught here
            return ostruct()

The exception propagates upward through the draw stack:

_get_metafile_content()
-> _get_entry()
-> get_metadata()
-> browsercolumn._draw_directory()
-> DisplayableContainer.draw()
-> ui.draw()
-> ui.redraw()
-> fm.loop()
-> main.main()  [outer except Exception: prints traceback, exits]

A malformed .metadata.json can be placed in any shared or world-writable directory (e.g. /tmp, a project repository, a downloaded archive), requiring no elevated privileges. Accidentally malformed files from third-party tools that write a .metadata.json with non-standard content trigger the same crash.


Steps to Reproduce

  1. Create a test directory with a regular file:

    mkdir /tmp/testdir
    echo "example file" > /tmp/testdir/file.txt
  2. Write a malformed .metadata.json in the same directory:

    echo 'INVALID JSON <<<' > /tmp/testdir/.metadata.json
  3. Open ranger and navigate into /tmp/testdir/.

    To reproduce without the ranger TUI, run directly via Python:

    import sys; sys.path.insert(0, '/path/to/ranger')
    from ranger.core.metadata import MetadataManager
    MetadataManager().get_metadata('/tmp/testdir/file.txt')
  4. Observe that an unhandled ValueError is raised. In a live ranger session this exception terminates the process.


Error Output

Traceback (most recent call last):
  File "ranger/core/metadata.py", line 124, in _get_metafile_content
    entries = json.load(fobj)
  ...
  File "ranger/core/metadata.py", line 45, in get_metadata
    return ostruct(copy.deepcopy(self._get_entry(filename)))
  File "ranger/core/metadata.py", line 99, in _get_entry
    entries = self._get_metafile_content(metafile)
  File "ranger/core/metadata.py", line 126, in _get_metafile_content
    raise ValueError("Failed decoding JSON file %s" % metafile)
ValueError: Failed decoding JSON file /tmp/testdir/.metadata.json

Suggested Fix

Add ValueError to the exception tuple in the inner except clause of get_metadata() so that a JSON parse failure is handled gracefully and returns an empty metadata struct instead of propagating:

--- ranger/core/metadata.py (original)
+++ ranger/core/metadata.py (fixed)

     def get_metadata(self, filename):
         try:
             return ostruct(copy.deepcopy(self.metadata_cache[filename]))
         except KeyError:
             try:
                 return ostruct(copy.deepcopy(self._get_entry(filename)))
-            except KeyError:
+            except (KeyError, ValueError):
                 return ostruct()

This fix was applied and confirmed: get_metadata() now returns an empty DefaultOpenStruct without raising when .metadata.json contains invalid JSON. ranger continues operating normally and the file's entry is displayed without metadata.


Credits

This issue was identified by Michał Majchrowicz and Marcin Wyczechowski, members of the AFINE Team.


Disclosure Policy

We follow a 90-day coordinated disclosure policy. We are committed to working with the ranger maintainers to resolve this issue before any public disclosure. If we do not receive a response within 90 days of the initial report, we reserve the right to publicly disclose the details of this vulnerability.

Activity

  1. added this to the v1.9.6 milestone on Jul 5, 2026
  2. toonn commented on Jul 5, 2026

    @toonn
    Member

    The suggested fix just silences the error. A proper fix should notify the user of the problem.

  3. sectroyer commented on Jul 5, 2026

    @sectroyer
    Author

    Yes I agree. Maybe a better fix is to treat malformed .metadata.json as a
    recoverable error: notify the user, skip the broken metadata, and continue
    browsing instead of terminating ranger.

    Something along these lines should address that:

    diff --git a/ranger/core/fm.py b/ranger/core/fm.py
    --- a/ranger/core/fm.py
    +++ b/ranger/core/fm.py
    @@ -102,7 +102,7 @@ class FM(Actions,  # pylint: disable=too-many-instance-attributes
             self.loader = Loader()
             self.copy_buffer = set()
             self.do_cut = False
    -        self.metadata = MetadataManager()
    +        self.metadata = MetadataManager(notifier=self.notify)
             self.image_displayer = None
             self.run = None
             self.rifle = None
    
    diff --git a/ranger/core/metadata.py b/ranger/core/metadata.py
    --- a/ranger/core/metadata.py
    +++ b/ranger/core/metadata.py
    @@ -24,12 +24,15 @@ DEEP_SEARCH_DEFAULT = False
    
     class MetadataManager(object):
    
    -    def __init__(self):
    +    def __init__(self, notifier=None):
             self.metadata_cache = {}
             self.metafile_cache = {}
             self.deep_search = DEEP_SEARCH_DEFAULT
    +        self.notifier = notifier
    +        self._warned_metafiles = set()
    
         def reset(self):
             self.metadata_cache.clear()
             self.metafile_cache.clear()
    +        self._warned_metafiles.clear()
    
         def get_metadata(self, filename):
             try:
    @@ -115,12 +118,19 @@ class MetadataManager(object):
             if exists(metafile):
                 with open(metafile, "r", encoding="utf-8") as fobj:
                     try:
                         entries = json.load(fobj)
                     except ValueError:
    -                    raise ValueError("Failed decoding JSON file %s" % metafile)
    +                    if self.notifier and metafile not in self._warned_metafiles:
    +                        self.notifier(
    +                            "Failed decoding metadata file: {0}".format(metafile),
    +                            bad=True,
    +                        )
    +                        self._warned_metafiles.add(metafile)
    +                    return {}
                 self.metafile_cache[metafile] = entries
                 return entries
    
             return {}

    This should keep ranger usable, make the problem visible to the user, and avoid
    repeated redraw spam from the same malformed metadata file.

  4. toonn commented on Aug 15, 2026

    @toonn
    Member

    That seems like an acceptable patch to me. How often is the metadata reread, to see if keeping problematic entries in memory is worth it?

  5. sectroyer commented on Aug 24, 2026

    @sectroyer
    Author

    From what I can tell, metadata is normally cached for the lifetime of the session, so it is not reread very often unless the cache is reset.

    The important part here is the broken-file case: if we do not cache that state, ranger will keep reopening and reparsing the same malformed .metadata.json during redraws, because metadata lookup happens in the draw path.

    So I think it is worth keeping problematic metafiles in memory for the current session as well, and treating them as empty metadata until :reset or restart. That avoids repeated parse attempts and repeated noise for the same broken file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions