VYPR
Moderate severityNVD Advisory· Published Jul 22, 2024· Updated Nov 4, 2025

CVE-2024-29073

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.

PackageAffected versionsPatched versions
ankiPyPI
< 24.624.6

Affected products

2

Patches

1
06f7aa393d21

Add a preference to toggle LaTeX generation (#3218)

https://github.com/ankitects/ankiAbdoJun 1, 2024via ghsa
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

News mentions

0

No linked articles in our index yet.