VYPR
Critical severityNVD Advisory· Published Oct 26, 2020· Updated Aug 4, 2024

Shell Command Execution in lookatme

CVE-2020-15271

Description

In lookatme (python/pypi package) versions prior to 2.3.0, the package automatically loaded the built-in "terminal" and "file_loader" extensions. Users that use lookatme to render untrusted markdown may have malicious shell commands automatically run on their system. This is fixed in version 2.3.0. As a workaround, the lookatme/contrib/terminal.py and lookatme/contrib/file_loader.py files may be manually deleted. Additionally, it is always recommended to be aware of what is being rendered with lookatme.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

lookatme prior to 2.3.0 auto-loads terminal and file_loader extensions, enabling remote code execution via untrusted markdown.

Vulnerability

In the Python package lookatme (an interactive, terminal-based markdown presentation tool) versions before 2.3.0, the built-in "terminal" and "file_loader" extensions are automatically loaded. This design flaw means that when a user renders untrusted markdown content, malicious shell commands embedded via these extensions may be executed without warning or user interaction [1].

Exploitation

An attacker can craft a markdown file that makes use of the terminal extension to execute arbitrary shell commands, or the file_loader extension to read/write files from the filesystem. No special privileges are required beyond the ability to present the markdown (such as displaying a presentation file). The attack surface is any scenario where lookatme is used to render untrusted input, which could occur in shared slide decks, automated rendering pipelines, or when viewing externally sourced documents [1].

Impact

Successful exploitation allows an attacker to execute arbitrary shell commands on the victim's system with the same privileges as the lookatme process, leading to a full compromise of the user's session. This is considered a critical remote code execution vulnerability [1].

Mitigation

The issue is fixed in lookatme version 2.3.0. Users are strongly advised to upgrade immediately. For those unable to upgrade, a workaround is to manually delete the files lookatme/contrib/terminal.py and lookatme/contrib/file_loader.py from the installation to disable the dangerous extensions [1].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
lookatmePyPI
< 2.3.02.3.0

Affected products

3

Patches

1
72fe36b784b2

Merge pull request #110 from d0c-s4vage/feature/109-extension_warnings

https://github.com/d0c-s4vage/lookatmeJames JohnsonOct 23, 2020via ghsa
12 files changed · +232 19
  • docs/source/getting_started.rst+31 1 modified
    @@ -24,6 +24,8 @@ The ``lookatme`` CLI has a few options to control it's behavior:
     
           lookatme - An interactive, terminal-based markdown presentation tool.
     
    +      See https://lookatme.readthedocs.io/en/v{{VERSION}} for documentation
    +
         Options:
           --debug
           -l, --log PATH
    @@ -35,6 +37,14 @@ The ``lookatme`` CLI has a few options to control it's behavior:
           --live, --live-reload           Watch the input filename for modifications
                                           and automatically reload
     
    +      -s, --safe                      Do not load any new extensions specified in
    +                                      the source markdown. Extensions specified
    +                                      via env var or -e are still loaded
    +
    +      --no-ext-warn                   Load new extensions specified in the source
    +                                      markdown without warning
    +
    +      -i, --ignore-ext-failure        Ignore load failures of extensions
           -e, --exts TEXT                 A comma-separated list of extension names to
                                           automatically load (LOOKATME_EXTS)
     
    @@ -58,11 +68,31 @@ are possible:
       :alt: Live Updates
     
     ``-e EXT_NAME1,EXT_NAME2`` / ``--exts EXT_NAME1,EXT_NAME2``
    -^^^^^^^^^^^^^^^^^^^
    +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     
     Allows a comma-separated list of extension names to be pre-loaded into lookatme
     without requring them to be declared in the Markdown source.
     
    +``-s`` / ``--safe``
    +^^^^^^^^^^^^^^^^^^^
    +
    +Do **NOT** load any new extensions specified in the markdown (ignore them). New
    +extensions are extensions that have not manually been allowed via the ``-e``
    +argument or the ``LOOKATME_EXTS`` environment variable.
    +
    +``--no-ext-warn``
    +^^^^^^^^^^^^^^^^^
    +
    +Do not warn about new extensions that are to-be-loaded that are specified in
    +the source markdown. New extensions are extensions that have not manually been
    +allowed via the ``-e`` argument or the ``LOOKATME_EXTS`` environment variable.
    +
    +``-i``
    +^^^^^^
    +
    +Ignore failure loading extensions. This does not ignore warnings, but ignores
    +any hard-errors during import, such as ``ImportError``.
    +
     
     ``--single`` / ``--one``
     ^^^^^^^^^^^^^^^^^^^^^^^^
    
  • docs/source/_static/lookatme_tour.gif+0 0 modified
  • examples/calendar_contrib/lookatme/contrib/calendar.py+8 0 modified
    @@ -12,6 +12,14 @@
     from lookatme.exceptions import IgnoredByContrib
     
     
    +def user_warnings():
    +    """No warnings exist for this extension. Anything you want to warn the
    +    user about, such as security risks in processing untrusted markdown, should
    +    go here.
    +    """
    +    return []
    +
    +
     def render_code(token, body, stack, loop):
         lang = token["lang"] or ""
         if lang != "calendar":
    
  • examples/tour.md+8 7 modified
    @@ -3,18 +3,19 @@ title: lookatme Tour
     date: 2020-10-09
     author: James Johnson
     extensions:
    +  - terminal
       - qrcode
       - image_ueberzug
     styles:
    -  style: solarized-dark
    +  style: monokai
       table:
    -    column_spacing: 5
    +    column_spacing: 15
       margin:
         top: 3
         bottom: 0
       padding:
    -    top: 1
    -    bottom: 1
    +    top: 3
    +    bottom: 3
     ---
     
     # Markdown Support: Inline
    @@ -170,16 +171,16 @@ docker run --rm -it ubuntu:18.04
     
     # Live Editing
     
    -When run with the `--live` option, lookatme watches for file changes and
    -auto-reloads the source markdown 
    +Hello from vim! The `--live` flag makes lookatme watch the source input
    +for file changes and auto-reloads the slides.
     
     ---
     
     # Live Editing: Including Styles!
     
     ```python
     def a_function(test):
    -    print "Hello again from vim"
    +    print "Hello again from vim again"
     ```
     
     | h1     | h2     | h3    |
    
  • lookatme/ascii_art.py+22 0 added
    @@ -0,0 +1,22 @@
    +"""
    +Misc ASCII art
    +"""
    +
    +WARNING = r"""
    +               _mBma
    +              sQf "QL
    +             jW(   -$g.
    +            jW'     -$m,
    +          .y@'  _aa.  4m,
    +         .mD`  ]QQWQ.  4Q,
    +        _mP`   ]QQQQ    ?Q/
    +       _QF     )WQQ@     ?Qc
    +      <QF       QQQF      )Qa
    +     jW(        QQQf       "QL
    +    jW'         ]H8'        -Q6.
    +  .y@'          _as.         -$m.
    + .m@`          ]QQWQ.         -4m,
    +_mP`           -?$8!            4Q,
    +mE                               $m
    +?$gyygggggggggwywgyygggggggygggggD(
    +"""
    
  • lookatme/contrib/file_loader.py+13 0 modified
    @@ -16,6 +16,19 @@
     from lookatme.exceptions import IgnoredByContrib
     
     
    +def user_warnings():
    +    """Provide warnings to the user that loading this extension may cause
    +    shell commands specified in the markdown to be run.
    +    """
    +    return [
    +        "Code-blocks with a language starting with 'file' may cause shell",
    +        "  commands from the source markdown to be run if the 'transform'",
    +        "  field is set",
    +        "See https://lookatme.readthedocs.io/en/latest/builtin_extensions/file_loader.html",
    +        "  for more details",
    +    ]
    +
    +
     class YamlRender:
         loads = lambda data: yaml.safe_load(data)
         dumps = lambda data: yaml.safe_dump(data)
    
  • lookatme/contrib/__init__.py+43 6 modified
    @@ -8,41 +8,78 @@
     import contextlib
     
     
    +import lookatme.ascii_art
     from lookatme.exceptions import IgnoredByContrib
    +import lookatme.prompt
     from . import terminal
     from . import file_loader
     
     
    -CONTRIB_MODULES = [
    -    terminal,
    -    file_loader,
    -]
    +CONTRIB_MODULES = []
     
     
    -def load_contribs(contrib_names):
    +def validate_extension_mod(ext_name, ext_mod):
    +    """Validate the extension, returns an array of warnings associated with the
    +    module
    +    """
    +    res = []
    +    if not hasattr(ext_mod, "user_warnings"):
    +        res.append("'user_warnings' is missing. Extension is not able to "
    +                   "provide user warnings.")
    +    else:
    +        res += ext_mod.user_warnings()
    +
    +    return res
    +
    +
    +def load_contribs(contrib_names, safe_contribs, ignore_load_failure=False):
         """Load all contrib modules specified by ``contrib_names``. These should
         all be namespaced packages under the ``lookatmecontrib`` namespace. E.g.
         ``lookatmecontrib.calendar`` would be an extension provided by a
         contrib module, and would be added to an ``extensions`` list in a slide's
         YAML header as ``calendar``.
    +
    +    ``safe_contribs`` is a set of contrib names that are manually provided
    +    by the user by the ``-e`` flag or env variable of extensions to auto-load.
         """
         if contrib_names is None:
             return
     
         errors = []
    +    all_warnings = []
         for contrib_name in contrib_names:
             module_name = f"lookatme.contrib.{contrib_name}"
             try:
                 mod = __import__(module_name, fromlist=[contrib_name])
    -            CONTRIB_MODULES.append(mod)
             except Exception as e:
    +            if ignore_load_failure:
    +                continue
                 errors.append(str(e))
    +        else:
    +            if contrib_name not in safe_contribs:
    +                ext_warnings = validate_extension_mod(contrib_name, mod)
    +                if len(ext_warnings) > 0:
    +                    all_warnings.append((contrib_name, ext_warnings))
    +            CONTRIB_MODULES.append(mod)
     
         if len(errors) > 0:
             raise Exception(
                 "Error loading one or more extensions:\n\n" + "\n".join(errors),
             )
     
    +    if len(all_warnings) == 0:
    +        return
    +
    +    print("\nExtension-provided user warnings:")
    +    for ext_name, ext_warnings in all_warnings:
    +        print("\n  {!r}:\n".format(ext_name))
    +        for ext_warning in ext_warnings:
    +            print("    * {}".format(ext_warning))
    +    print("")
    +
    +    if not lookatme.prompt.yes("Continue anyways?"):
    +        exit(1)
    +
     
     def contrib_first(fn):
         """A decorator that allows contrib modules to override default behavior
    
  • lookatme/contrib/terminal.py+12 0 modified
    @@ -18,6 +18,18 @@
     import lookatme.config
     
     
    +def user_warnings():
    +    """Provide warnings to the user that loading this extension may cause
    +    shell commands specified in the markdown to be run.
    +    """
    +    return [
    +        "Code-blocks with a language starting with 'terminal' will cause shell",
    +        "  commands from the source markdown to be run",
    +        "See https://lookatme.readthedocs.io/en/latest/builtin_extensions/terminal.html",
    +        "  for more details",
    +    ]
    +
    +
     class YamlRender:
         loads = lambda data: yaml.safe_load(data)
         dumps = lambda data: yaml.safe_dump(data)
    
  • lookatme/__main__.py+28 1 modified
    @@ -58,6 +58,27 @@
         is_flag=True,
         default=False,
     )
    +@click.option(
    +    "-s",
    +    "--safe",
    +    help="Do not load any new extensions specified in the source markdown. "
    +         "Extensions specified via env var or -e are still loaded",
    +    is_flag=True,
    +    default=False,
    +)
    +@click.option(
    +    "--no-ext-warn",
    +    help="Load new extensions specified in the source markdown without warning",
    +    is_flag=True,
    +    default=False,
    +)
    +@click.option(
    +    "-i",
    +    "--ignore-ext-failure",
    +    help="Ignore load failures of extensions",
    +    is_flag=True,
    +    default=False,
    +)
     @click.option(
         "-e",
         "--exts",
    @@ -82,8 +103,11 @@
         nargs=-1,
     )
     def main(debug, log_path, theme, code_style, dump_styles,
    -         input_files, live_reload, extensions, single_slide):
    +         input_files, live_reload, extensions, single_slide, safe, no_ext_warn,
    +         ignore_ext_failure):
         """lookatme - An interactive, terminal-based markdown presentation tool.
    +    
    +    See https://lookatme.readthedocs.io/en/v{{VERSION}} for documentation
         """
         if debug:
             lookatme.config.LOG = lookatme.log.create_log(log_path)
    @@ -102,6 +126,9 @@ def main(debug, log_path, theme, code_style, dump_styles,
             live_reload=live_reload,
             single_slide=single_slide,
             preload_extensions=preload_exts,
    +        safe=safe,
    +        no_ext_warn=no_ext_warn,
    +        ignore_ext_failure=ignore_ext_failure,
         )
     
         if dump_styles:
    
  • lookatme/pres.py+47 4 modified
    @@ -12,6 +12,7 @@
     import lookatme.config
     import lookatme.contrib
     from lookatme.parser import Parser
    +import lookatme.prompt
     import lookatme.themes
     import lookatme.tui
     from lookatme.utils import dict_deep_update
    @@ -21,7 +22,8 @@ class Presentation(object):
         """Defines a presentation
         """
         def __init__(self, input_stream, theme, style_override=None, live_reload=False,
    -                 single_slide=False, preload_extensions=None):
    +                 single_slide=False, preload_extensions=None, safe=False,
    +                 no_ext_warn=False, ignore_ext_failure=False):
             """Creates a new Presentation
     
             :param stream input_stream: An input stream from which to read the
    @@ -37,6 +39,10 @@ def __init__(self, input_stream, theme, style_override=None, live_reload=False,
             self.live_reload = live_reload
             self.tui = None
             self.single_slide = single_slide
    +        self.safe = safe
    +        self.no_ext_warn = no_ext_warn
    +        self.ignore_ext_failure = ignore_ext_failure
    +        self.initial_load_complete = False
     
             self.theme_mod = __import__("lookatme.themes." + theme, fromlist=[theme])
     
    @@ -46,6 +52,7 @@ def __init__(self, input_stream, theme, style_override=None, live_reload=False,
                 self.reload_thread.start()
     
             self.reload(data=input_stream.read())
    +        self.initial_load_complete = True
     
         def reload_watcher(self):
             """Watch for changes to the input filename, automatically reloading
    @@ -79,9 +86,24 @@ def reload(self, data=None):
             parser = Parser(single_slide=self.single_slide)
             self.meta, self.slides = parser.parse(data)
     
    -        all_exts = set(self.preload_extensions)
    -        all_exts |= set(self.meta.get("extensions", []))
    -        lookatme.contrib.load_contribs(all_exts)
    +        # only load extensions once! Live editing does not support
    +        # auto-extension reloading
    +        if not self.initial_load_complete:
    +            safe_exts = set(self.preload_extensions)
    +            new_exts = set()
    +            # only load if running with safe=False
    +            if not self.safe:
    +                source_exts = set(self.meta.get("extensions", []))
    +                new_exts = source_exts - safe_exts
    +                self.warn_exts(new_exts)
    +
    +            all_exts = safe_exts | new_exts
    +
    +            lookatme.contrib.load_contribs(
    +                all_exts,
    +                safe_exts,
    +                self.ignore_ext_failure,
    +            )
     
             self.styles = lookatme.themes.ensure_defaults(self.theme_mod)
             dict_deep_update(self.styles, self.meta.get("styles", {}))
    @@ -91,6 +113,27 @@ def reload(self, data=None):
                 self.styles["style"] = self.style_override
     
             lookatme.config.STYLE = self.styles
    +        self.initial_load_complete = True
    +
    +    def warn_exts(self, exts):
    +        """Warn about source-provided extensions that are to-be-loaded
    +        """
    +        if len(exts) == 0 or self.no_ext_warn:
    +            return
    +
    +        warning = lookatme.ascii_art.WARNING
    +        print("\n".join(["    " + x for x in warning.split("\n")]))
    +
    +        print("New extensions required by {!r} are about to be loaded:\n".format(
    +            self.input_filename
    +        ))
    +        for ext in exts:
    +            print("  - {!r}".format("lookatme.contrib." + ext))
    +        print("")
    +
    +        if not lookatme.prompt.yes("Are you ok with attempting to load them?"):
    +            print("Bailing due to unacceptance of source-required extensions")
    +            exit(1)
     
         def run(self, start_slide=0):
             """Run the presentation!
    
  • lookatme/prompt.py+10 0 added
    @@ -0,0 +1,10 @@
    +"""
    +Basic user-prompting helper functions
    +"""
    +
    +
    +def yes(msg):
    +    """Prompt the user for a yes/no answer. Returns bool
    +    """
    +    answer = input("{} (Y/N) ".format(msg))
    +    return answer.strip().lower() in ["y", "yes"]
    
  • README.md+10 0 modified
    @@ -63,6 +63,8 @@ Usage: lookatme [OPTIONS] [INPUT_FILES]...
     
       lookatme - An interactive, terminal-based markdown presentation tool.
     
    +  See https://lookatme.readthedocs.io/en/v{{VERSION}} for documentation
    +
     Options:
       --debug
       -l, --log PATH
    @@ -74,6 +76,14 @@ Options:
       --live, --live-reload           Watch the input filename for modifications
                                       and automatically reload
     
    +  -s, --safe                      Do not load any new extensions specified in
    +                                  the source markdown. Extensions specified
    +                                  via env var or -e are still loaded
    +
    +  --no-ext-warn                   Load new extensions specified in the source
    +                                  markdown without warning
    +
    +  -i, --ignore-ext-failure        Ignore load failures of extensions
       -e, --exts TEXT                 A comma-separated list of extension names to
                                       automatically load (LOOKATME_EXTS)
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.