vim-patch:9.2.0597: [security]: possible code execution with python complete (#40117)

Problem:  [security]: another possible code execution with python complete
          (David Carliez)
Solution: Strip default expressions and annotations from generated
          source for pythoncomplete and python3complete.

Github Security Advisory:
https://github.com/vim/vim/security/advisories/GHSA-65p9-mwwx-7468

c8c63673bc

Co-authored-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
zeertzjq
2026-06-05 04:59:44 +02:00
committed by GitHub
parent 9ffa2069c1
commit e887cfb3b5
3 changed files with 302 additions and 8 deletions

View File

@@ -1,8 +1,8 @@
"pythoncomplete.vim - Omni Completion for python
" Maintainer: <vacancy>
" Previous Maintainer: Aaron Griffin <aaronmgriffin@gmail.com>
" Version: 0.9
" Last Updated: 2020 Oct 9
" Version: 0.10
" Last Updated: 2026 Jun 04
"
" Changes
" TODO:
@@ -15,6 +15,11 @@
" v 0.10 by Vim project
" * disables importing local modules, unless the global Vim variable
" g:pythoncomplete_allow_import is set to non-zero
" * strip default values and annotations from function parameter lists
" before exec(), and whitelist class base lists to dotted names: the
" previous code passed buffer-supplied expressions to exec() which
" Python evaluates at definition time, allowing arbitrary code
" execution via crafted def/class headers
"
" v 0.9
" * Fixed docstring parsing for classes and functions
@@ -95,6 +100,24 @@ function! s:DefPython()
python << PYTHONEOF
import sys, tokenize, cStringIO, types
from token import NAME, DEDENT, NEWLINE, STRING
import re
# Used by Class.get_code(): a base class expression is only included in the
# code passed to exec() if it is a pure dotted name (e.g. "Base", "mod.Base",
# "pkg.sub.Cls"). Anything containing calls, subscripts, "=", ":" or other
# operators is dropped, since exec()-ing it would evaluate buffer-supplied
# expressions. See the security note in the file header.
_DOTTED_NAME_RE = re.compile(r'^[A-Za-z_]\w*(\s*\.\s*[A-Za-z_]\w*)*$')
def _strip_param(p):
# Return the bare parameter name from a parameter spec harvested by
# _parenparse(), discarding any default value or annotation. Default
# values and annotations would otherwise be evaluated by exec() at
# function-definition time. Star prefixes ("*args", "**kw") and bare
# "*" / "/" are preserved as written.
p = p.split('=', 1)[0]
p = p.split(':', 1)[0]
return p.strip()
debugstmts=[]
def dbg(s): debugstmts.append(s)
@@ -365,7 +388,13 @@ class Class(Scope):
return c
def get_code(self):
str = '%sclass %s' % (self.currentindent(),self.name)
if len(self.supers) > 0: str += '(%s)' % ','.join(self.supers)
# Only include base class expressions that are pure dotted names.
# Anything else (calls, subscripts, conditionals, ...) is dropped
# because exec() would evaluate it at class-definition time. See
# the security note in the file header.
safe_supers = [s.strip() for s in self.supers
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.subscopes) > 0:
@@ -382,8 +411,14 @@ class Function(Scope):
def copy_decl(self,indent=0):
return Function(self.name,self.params,indent, self.docstr)
def get_code(self):
# Strip default values and annotations from each parameter before
# joining: exec() evaluates these at definition time and a hostile
# buffer could otherwise execute arbitrary code via crafted def
# headers. See file header for details.
safe_params = [_strip_param(p) for p in self.params]
safe_params = [p for p in safe_params if p]
str = "%sdef %s(%s):\n" % \
(self.currentindent(),self.name,','.join(self.params))
(self.currentindent(),self.name,','.join(safe_params))
if len(self.docstr) > 0: str += self.childindent()+'"""'+self.docstr+'"""\n'
str += "%spass\n" % self.childindent()
return str