CVE-2024-29073
Description
Anki 24.04 fails to block the verbatim LaTeX package, allowing arbitrary file read via a crafted flashcard.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Anki 24.04 fails to block the verbatim LaTeX package, allowing arbitrary file read via a crafted flashcard.
Root
Cause Anki uses LaTeX to render mathematical and scientific content on flashcards. To prevent malicious commands, Anki maintains a blocklist of dangerous TeX commands. However, the verbatim package, which is included by default in most LaTeX distributions (e.g., MiKTeX), was not blocked [1][3][4]. This oversight allows an attacker to bypass the sanitization.
Exploitation
An attacker creates a shared deck containing a flashcard with specially crafted LaTeX code that uses the verbatim package to read arbitrary files (e.g., /etc/passwd). When a user imports and views the flashcard, Anki invokes LaTeX, which processes the malicious code and reads the specified file [1][4]. No authentication or special privileges are needed beyond the user importing the deck.
Impact
Successful exploitation results in arbitrary file read, potentially exposing sensitive information such as configuration files, private keys, or user data. This attack is particularly dangerous given the trust users place in shared decks [1].
Mitigation
The vendor has acknowledged the issue and added a preference to disable LaTeX generation in Anki 24.07+ (see pull request #3218) [2]. Users are advised to update Anki or disable LaTeX processing. Additionally, caution is warranted when importing decks from untrusted sources [1][4].
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-x3r6-ccvq-cf5vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-29073ghsaADVISORY
- 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-1992ghsaWEB
News mentions
0No linked articles in our index yet.