CVE-2024-32152
Description
A blocklist bypass in Anki's LaTeX processing allows attackers to create an arbitrary file at a fixed path by sharing a malicious flashcard.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A blocklist bypass in Anki's LaTeX processing allows attackers to create an arbitrary file at a fixed path by sharing a malicious flashcard.
Vulnerability
Overview
CVE-2024-32152 is a blocklist bypass vulnerability in the LaTeX functionality of Ankitects Anki 24.04. The application uses a blocklist to prevent dangerous TeX commands (such as \write, \input, \include) from being executed. A specially crafted malicious flashcard can bypass this blacklist by encoding blocked command strings with hex characters, leading to arbitrary file creation at a fixed path. [1][4]
Exploitation
An attacker can exploit this vulnerability by creating a shared deck containing a malicious flashcard and distributing it to other users, for example via AnkiWeb or by directly sharing the deck file. The victim must import and then generate the LaTeX content in the flashcard. The attack requires no authentication and can be performed over the network, though it relies on user interaction (the victim must study the flashcard). [1][2][4]
Impact
Successful exploitation allows the attacker to create a file at a fixed, predetermined path on the victim's system. While the file content and exact location are constrained by the vulnerability, this arbitrary file creation could be used to write configuration files, scripts, or other data that may lead to further compromise. The official CVSS v3.1 score is 3.1 (Low), reflecting high attack complexity and the need for user interaction. [3][4]
Mitigation
The Anki project has addressed this vulnerability by adding a user preference to toggle LaTeX generation, which can be used as a workaround to prevent exploitation. There is no mention of a full patch in the available references; administrators and users are advised to disable LaTeX generation if not required. [2]
AI Insight generated on May 20, 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 |
|---|---|---|
ankiPyPI | < 24.6 | 24.6 |
Affected products
2Patches
106f7aa393d21Add a preference to toggle LaTeX generation (#3218)
9 files changed · +35 −66
ftl/core/preferences.ftl+2 −0 modified@@ -13,6 +13,8 @@ preferences-media-is-not-backed-up = Media is not backed up. Please create a per preferences-on-next-sync-force-changes-in = On next sync, force changes in one direction preferences-paste-clipboard-images-as-png = Paste clipboard images as PNG preferences-paste-without-shift-key-strips-formatting = Paste without shift key strips formatting +preferences-generate-latex-images-automatically = Generate LaTeX images (security risk) +preferences-latex-generation-disabled = LaTeX image generation is disabled in the preferences. preferences-periodically-sync-media = Periodically sync media preferences-please-restart-anki-to-complete-language = Please restart Anki to complete language change. preferences-preferences = Preferences
proto/anki/config.proto+2 −0 modified@@ -53,6 +53,7 @@ message ConfigKey { RESET_COUNTS_REVIEWER = 22; RANDOM_ORDER_REPOSITION = 23; SHIFT_POSITION_OF_EXISTING_CARDS = 24; + RENDER_LATEX = 25; } enum String { SET_DUE_BROWSER = 0; @@ -121,6 +122,7 @@ message Preferences { bool paste_strips_formatting = 3; string default_search_text = 4; bool ignore_accents_in_search = 5; + bool render_latex = 6; } message BackupLimits { uint32 daily = 1;
pylib/anki/latex.py+6 −23 modified@@ -5,12 +5,12 @@ import html import os -import re from dataclasses import dataclass import anki import anki.collection from anki import card_rendering_pb2, hooks +from anki.config import Config from anki.models import NotetypeDict from anki.template import TemplateRenderContext, TemplateRenderOutput from anki.utils import call, is_mac, namedtmp, tmpdir @@ -36,9 +36,6 @@ ["dvisvgm", "--no-fonts", "--exact", "-Z", "2", "tmp.dvi", "-o", "tmp.svg"], ] -# if off, use existing media but don't create new -build = True # pylint: disable=invalid-name - # add standard tex install location to osx if is_mac: os.environ["PATH"] += ":/usr/texbin:/Library/TeX/texbin" @@ -104,11 +101,15 @@ def render_latex_returning_errors( out = ExtractedLatexOutput.from_proto(proto) errors = [] html = out.html + render_latex = col.get_config_bool(Config.Bool.RENDER_LATEX) for latex in out.latex: # don't need to render? - if not build or col.media.have(latex.filename): + if col.media.have(latex.filename): continue + if not render_latex: + errors.append(col.tr.preferences_latex_generation_disabled()) + return html, errors err = _save_latex_image(col, latex, header, footer, svg) if err is not None: @@ -126,24 +127,6 @@ def _save_latex_image( ) -> str | None: # add header/footer latex = f"{header}\n{extracted.latex_body}\n{footer}" - # it's only really secure if run in a jail, but these are the most common - tmplatex = latex.replace("\\includegraphics", "") - for bad in ( - "\\write18", - "\\readline", - "\\input", - "\\include", - "\\catcode", - "\\openout", - "\\write", - "\\loop", - "\\def", - "\\shipout", - ): - # don't mind if the sequence is only part of a command - bad_re = f"\\{bad}[^a-zA-Z]" - if re.search(bad_re, tmplatex): - return col.tr.media_for_security_reasons_is_not(val=bad) # commands to use if svg:
pylib/tests/test_latex.py+3 −41 modified@@ -6,12 +6,14 @@ import os import shutil +from anki.config import Config from anki.lang import without_unicode_isolation from tests.shared import getEmptyCol def test_latex(): col = getEmptyCol() + col.set_config_bool(Config.Bool.RENDER_LATEX, True) # change latex cmd to simulate broken build import anki.latex @@ -51,49 +53,9 @@ def test_latex(): assert ".png" in oldcard.question() # if we turn off building, then previous cards should work, but cards with # missing media will show a broken image - anki.latex.build = False + col.set_config_bool(Config.Bool.RENDER_LATEX, False) note = col.newNote() note["Front"] = "[latex]foo[/latex]" col.addNote(note) assert len(os.listdir(col.media.dir())) == 2 assert ".png" in oldcard.question() - # turn it on again so other test don't suffer - anki.latex.build = True - - # bad commands - (result, msg) = _test_includes_bad_command("\\write18") - assert result, msg - (result, msg) = _test_includes_bad_command("\\readline") - assert result, msg - (result, msg) = _test_includes_bad_command("\\input") - assert result, msg - (result, msg) = _test_includes_bad_command("\\include") - assert result, msg - (result, msg) = _test_includes_bad_command("\\catcode") - assert result, msg - (result, msg) = _test_includes_bad_command("\\openout") - assert result, msg - (result, msg) = _test_includes_bad_command("\\write") - assert result, msg - (result, msg) = _test_includes_bad_command("\\loop") - assert result, msg - (result, msg) = _test_includes_bad_command("\\def") - assert result, msg - (result, msg) = _test_includes_bad_command("\\shipout") - assert result, msg - - # inserting commands beginning with a bad name should not raise an error - (result, msg) = _test_includes_bad_command("\\defeq") - assert not result, msg - # normal commands should not either - (result, msg) = _test_includes_bad_command("\\emph") - assert not result, msg - - -def _test_includes_bad_command(bad): - col = getEmptyCol() - note = col.newNote() - note["Front"] = f"[latex]{bad}[/latex]" - col.addNote(note) - q = without_unicode_isolation(note.cards()[0].question()) - return (f"'{bad}' is not allowed on cards" in q, f"Card content: {q}")
qt/aqt/forms/preferences.ui+16 −2 modified@@ -6,7 +6,7 @@ <rect> <x>0</x> <y>0</y> - <width>606</width> + <width>636</width> <height>638</height> </rect> </property> @@ -347,7 +347,7 @@ <property name="title"> <string>preferences_review</string> </property> - <layout class="QVBoxLayout" name="verticalLayout_16"> + <layout class="QVBoxLayout" name="verticalLayout_5"> <item> <widget class="QCheckBox" name="showPlayButtons"> <property name="sizePolicy"> @@ -413,6 +413,19 @@ </property> </widget> </item> + <item> + <widget class="QCheckBox" name="render_latex"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>preferences_generate_latex_images_automatically</string> + </property> + </widget> + </item> </layout> </widget> </item> @@ -1098,6 +1111,7 @@ <tabstop>showProgress</tabstop> <tabstop>showEstimates</tabstop> <tabstop>spacebar_rates_card</tabstop> + <tabstop>render_latex</tabstop> <tabstop>pastePNG</tabstop> <tabstop>paste_strips_formatting</tabstop> <tabstop>useCurrent</tabstop>
qt/aqt/preferences.py+2 −0 modified@@ -126,6 +126,7 @@ def setup_collection(self) -> None: form.paste_strips_formatting.setChecked(editing.paste_strips_formatting) form.ignore_accents_in_search.setChecked(editing.ignore_accents_in_search) form.pastePNG.setChecked(editing.paste_images_as_png) + form.render_latex.setChecked(editing.render_latex) form.default_search_text.setText(editing.default_search_text) form.backup_explanation.setText( @@ -154,6 +155,7 @@ def update_collection(self, on_done: Callable[[], None]) -> None: editing.adding_defaults_to_current_deck = not form.useCurrent.currentIndex() editing.paste_images_as_png = self.form.pastePNG.isChecked() editing.paste_strips_formatting = self.form.paste_strips_formatting.isChecked() + editing.render_latex = self.form.render_latex.isChecked() editing.default_search_text = self.form.default_search_text.text() editing.ignore_accents_in_search = ( self.form.ignore_accents_in_search.isChecked()
rslib/src/backend/config.rs+1 −0 modified@@ -36,6 +36,7 @@ impl From<BoolKeyProto> for BoolKey { BoolKeyProto::ResetCountsReviewer => BoolKey::ResetCountsReviewer, BoolKeyProto::RandomOrderReposition => BoolKey::RandomOrderReposition, BoolKeyProto::ShiftPositionOfExistingCards => BoolKey::ShiftPositionOfExistingCards, + BoolKeyProto::RenderLatex => BoolKey::RenderLatex, } } }
rslib/src/config/bool.rs+1 −0 modified@@ -27,6 +27,7 @@ pub enum BoolKey { NewCardsIgnoreReviewLimit, PasteImagesAsPng, PasteStripsFormatting, + RenderLatex, PreviewBothSides, RestorePositionBrowser, RestorePositionReviewer,
rslib/src/preferences.rs+2 −0 modified@@ -128,6 +128,7 @@ impl Collection { paste_strips_formatting: self.get_config_bool(BoolKey::PasteStripsFormatting), default_search_text: self.get_config_string(StringKey::DefaultSearchText), ignore_accents_in_search: self.get_config_bool(BoolKey::IgnoreAccentsInSearch), + render_latex: self.get_config_bool(BoolKey::RenderLatex), }) } @@ -141,6 +142,7 @@ impl Collection { self.set_config_bool_inner(BoolKey::PasteStripsFormatting, s.paste_strips_formatting)?; self.set_config_string_inner(StringKey::DefaultSearchText, &s.default_search_text)?; self.set_config_bool_inner(BoolKey::IgnoreAccentsInSearch, s.ignore_accents_in_search)?; + self.set_config_bool_inner(BoolKey::RenderLatex, s.render_latex)?; Ok(()) } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-q47p-v5rw-v574ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-32152ghsaADVISORY
- github.com/ankitects/anki/commit/06f7aa393d21d7d5dd8039e15d543b73c3346932ghsaWEB
- github.com/ankitects/anki/pull/3218ghsaWEB
- skerritt.blog/anki-0dayghsaWEB
- skii.dev/anki-0dayghsaWEB
- talosintelligence.com/vulnerability_reports/TALOS-2024-1994ghsaWEB
News mentions
0No linked articles in our index yet.