]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0699: [security]: possible code execution with python complete v9.2.0699
authorChristian Brabandt <cb@256bit.org>
Sun, 21 Jun 2026 19:50:56 +0000 (19:50 +0000)
committerChristian Brabandt <cb@256bit.org>
Sun, 21 Jun 2026 20:03:26 +0000 (20:03 +0000)
Problem:  [security]: possible code execution with python complete
          (morningbread)
Solution: Use repr() to quote the doc strings correctly

Github Security Advisory:
https://github.com/vim/vim/security/advisories/GHSA-ppj8-wqjf-6fp3

Supported by AI

Signed-off-by: Christian Brabandt <cb@256bit.org>
runtime/autoload/python3complete.vim
runtime/autoload/pythoncomplete.vim
src/testdir/test_plugin_python3complete.vim
src/version.c

index 4b6f5d2ced139d52dfc13ffff17953feaebe6dc9..88e3466515f581b6d0a8f750b83dce8193469474 100644 (file)
@@ -2,7 +2,7 @@
 " Maintainer: <vacancy>
 " Previous Maintainer: Aaron Griffin <aaronmgriffin@gmail.com>
 " Version: 0.10
-" Last Updated: 2026 Jun 04
+" Last Updated: 2026 Jun 21
 "
 " Roland Puntaier: this file contains adaptations for python3 and is parallel to pythoncomplete.vim
 "
@@ -22,6 +22,7 @@
 "     previous code passed buffer-supplied expressions to exec() which
 "     Python evaluates at definition time, allowing arbitrary code
 "     execution via crafted def/class headers
+"   * use repr() on doc strings to prevent code execution
 "
 " v 0.9
 "   * Fixed docstring parsing for classes and functions
@@ -338,7 +339,7 @@ class Scope(object):
 
     def get_code(self):
         str = ""
-        if len(self.docstr) > 0: str += '"""'+self.docstr+'"""\n'
+        if len(self.docstr) > 0: str += repr(self.docstr)+'\n'
         str += 'class _PyCmplNoType:\n    def __getattr__(self,name):\n        return None\n'
         for sub in self.subscopes:
             str += sub.get_code()
@@ -381,7 +382,7 @@ class Class(Scope):
                        if _DOTTED_NAME_RE.match(s.strip())]
         if len(safe_supers) > 0: str += '(%s)' % ','.join(safe_supers)
         str += ':\n'
-        if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n'
+        if len(self.docstr) > 0: str += self.childindent()+repr(self.docstr)+'\n'
         if len(self.subscopes) > 0:
             for s in self.subscopes: str += s.get_code()
         else:
@@ -404,7 +405,7 @@ class Function(Scope):
         safe_params = [p for p in safe_params if p]
         str = "%sdef %s(%s):\n" % \
             (self.currentindent(),self.name,','.join(safe_params))
-        if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n'
+        if len(self.docstr) > 0: str += self.childindent()+repr(self.docstr)+'\n'
         str += "%spass\n" % self.childindent()
         return str
 
index 7614882446de32f274439f2239750d3b5380bbb3..5dd6c9462dab7ce6bc60f573ab24ad083717d2c7 100644 (file)
@@ -2,7 +2,7 @@
 " Maintainer: <vacancy>
 " Previous Maintainer: Aaron Griffin <aaronmgriffin@gmail.com>
 " Version: 0.10
-" Last Updated: 2026 Jun 04
+" Last Updated: 2026 Jun 21
 "
 " Changes
 " TODO:
@@ -20,6 +20,7 @@
 "     previous code passed buffer-supplied expressions to exec() which
 "     Python evaluates at definition time, allowing arbitrary code
 "     execution via crafted def/class headers
+"   * use repr() on doc strings to prevent code execution
 "
 " v 0.9
 "   * Fixed docstring parsing for classes and functions
@@ -353,7 +354,7 @@ class Scope(object):
 
     def get_code(self):
         str = ""
-        if len(self.docstr) > 0: str += '"""'+self.docstr+'"""\n'
+        if len(self.docstr) > 0: str += repr(self.docstr)+'\n'
         str += 'class _PyCmplNoType:\n    def __getattr__(self,name):\n        return None\n'
         for sub in self.subscopes:
             str += sub.get_code()
@@ -396,7 +397,7 @@ class Class(Scope):
                        if _DOTTED_NAME_RE.match(s.strip())]
         if len(safe_supers) > 0: str += '(%s)' % ','.join(safe_supers)
         str += ':\n'
-        if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n'
+        if len(self.docstr) > 0: str += self.childindent()+repr(self.docstr)+'\n'
         if len(self.subscopes) > 0:
             for s in self.subscopes: str += s.get_code()
         else:
@@ -419,7 +420,7 @@ class Function(Scope):
         safe_params = [p for p in safe_params if p]
         str = "%sdef %s(%s):\n" % \
             (self.currentindent(),self.name,','.join(safe_params))
-        if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n'
+        if len(self.docstr) > 0: str += self.childindent()+repr(self.docstr)+'\n'
         str += "%spass\n" % self.childindent()
         return str
 
index e2b0c6616d860eead4b82fb6c5265340f3a82bb5..590348ee4a6f47499fdb859e5c7af04c99eddeca 100644 (file)
@@ -221,4 +221,19 @@ func Test_python3complete_allow_import_on_runs_imports()
         \ 'g:pythoncomplete_allow_import=1 did not run the buffer import')
 endfunc
 
+func Test_python3complete_no_exec_via_class_docstring()
+  " A class-body docstring is emitted verbatim between triple quotes by
+  " get_code() and runs at class-definition time during exec().  A single-
+  " quoted source docstring lets an embedded """ survive doc()'s leading/
+  " trailing quote strip and break out of the generated literal.
+  let marker = tempname()
+  call s:CompleteAndExpectNoMarker([
+        \ 'class Foo:',
+        \ '    ''x"""+open("' . marker . '", "w").close()+"""y''',
+        \ '    pass',
+        \ 'Foo.',
+        \ ], marker,
+        \ 'class docstring expression was evaluated during omni-completion')
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
index 336707f7f770c9d13f21370142d48637e9e7b1d1..b93bdd9e1c01460b700075780fba8bd12f315d79 100644 (file)
@@ -759,6 +759,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    699,
 /**/
     698,
 /**/