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

CVE-2024-32152

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.

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.