Shell Command Execution in lookatme
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.
| Package | Affected versions | Patched versions |
|---|---|---|
lookatmePyPI | < 2.3.0 | 2.3.0 |
Affected products
3- Range: <2.3.0
- d0c-s4vage/lookatmev5Range: < 2.3.0
Patches
172fe36b784b2Merge pull request #110 from d0c-s4vage/feature/109-extension_warnings
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 modifiedexamples/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- github.com/advisories/GHSA-c84h-w6cr-5v8qghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-15271ghsaADVISORY
- github.com/d0c-s4vage/lookatme/commit/72fe36b784b234548d49dae60b840c37f0eb8d84ghsax_refsource_MISCWEB
- github.com/d0c-s4vage/lookatme/pull/110ghsax_refsource_MISCWEB
- github.com/d0c-s4vage/lookatme/releases/tag/v2.3.0ghsax_refsource_MISCWEB
- github.com/d0c-s4vage/lookatme/security/advisories/GHSA-c84h-w6cr-5v8qghsax_refsource_CONFIRMWEB
- github.com/pypa/advisory-database/tree/main/vulns/lookatme/PYSEC-2020-61.yamlghsaWEB
- pypi.org/project/lookatme/ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.