CVE-2020-27544 (Python, published 2023-08-11T14:15Z)
Description: value: An issue was discovered in FoldingAtHome Client Advanced Control GUI before commit 9b619ae64443997948a36dda01b420578de1af77, allows remote attackers to execute arbitrary code via crafted payload to function parse_message in file Connection.py.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/FoldingAtHome/fah-control
Files changed:
fah/Connection.py
fah/util/PYONDecoder.py
fah/util/__init__.py
Commit message: Use PYONDecoder instead of eval()


Vulnerable code:
            msg = eval(data, {}, {})

Fixed code:
import json
from fah.util import PYONDecoder
            msg = json.loads(data, cls = PYONDecoder)
################################################################################
#                                                                              #
#                    Folding@Home Client Control (FAHControl)                  #
#                   Copyright (C) 2016-2020 foldingathome.org                  #
#                  Copyright (C) 2010-2016 Stanford University                 #
#                                                                              #
#      This program is free software: you can redistribute it and/or modify    #
#      it under the terms of the GNU General Public License as published by    #
#       the Free Software Foundation, either version 3 of the License, or      #
#                      (at your option) any later version.                     #
#                                                                              #
#        This program is distributed in the hope that it will be useful,       #
#         but WITHOUT ANY WARRANTY; without even the implied warranty of       #
#         MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the        #
#                  GNU General Public License for more details.                #
#                                                                              #
#       You should have received a copy of the GNU General Public License      #
#     along with this program.  If not, see <http://www.gnu.org/licenses/>.    #
#                                                                              #
################################################################################

import re
import json
import sys


NUMBER_RE = re.compile(r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?',
                       (re.VERBOSE | re.MULTILINE | re.DOTALL))
FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL
STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS)
BACKSLASH = {
  '"': u'"', '\\': u'\\', '/': u'/',
  'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t',
}

DEFAULT_ENCODING = "utf-8"


def linecol(doc, pos):
  lineno = doc.count('\n', 0, pos) + 1
  if lineno == 1:
    colno = pos + 1
  else:
    colno = pos - doc.rindex('\n', 0, pos)
  return lineno, colno


def errmsg(msg, doc, pos, end=None):
  # Note that this function is called from _json
  lineno, colno = linecol(doc, pos)
  if end is None:
    fmt = '{0}: line {1} column {2} (char {3})'
    return fmt.format(msg, lineno, colno, pos)

  endlineno, endcolno = linecol(doc, end)
  fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})'
  return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end)


def _decode_uXXXX(s, pos):
  esc = s[pos + 1:pos + 5]

  if len(esc) == 4 and esc[1] not in 'xX':
    try:
      return int(esc, 16)
    except ValueError: pass

  msg = "Invalid \\uXXXX escape"
  raise ValueError(errmsg(msg, s, pos))


def pyon_scanstring(s, end, encoding = None, strict = True,
                    _b = BACKSLASH, _m = STRINGCHUNK.match):
  """Scan the string s for a JSON string. End is the index of the
  character in s after the quote that started the JSON string.
  Unescapes all valid JSON string escape sequences and raises ValueError
  on attempt to decode an invalid string. If strict is False then literal
  control characters are allowed in the string.
  Returns a tuple of the decoded string and the index of the character in s
  after the end quote."""
  if encoding is None: encoding = DEFAULT_ENCODING
  chunks = []
  _append = chunks.append
  begin = end - 1

  while True:
    chunk = _m(s, end)
    if chunk is None:
      raise ValueError(
        errmsg("Unterminated string starting at", s, begin))

    end = chunk.end()
    content, terminator = chunk.groups()

    # Content is contains zero or more unescaped string characters
    if content:
      if not isinstance(content, unicode):
        content = unicode(content, encoding)
      _append(content)

    # Terminator is the end of string, a literal control character,
    # or a backslash denoting that an escape sequence follows
    if terminator == '"': break
    elif terminator != '\\':
      if strict:
        msg = "Invalid control character {0!r} at".format(terminator)
        raise ValueError(errmsg(msg, s, end))
      else:
        _append(terminator)
        continue

    try:
      esc = s[end]
    except IndexError:
      raise ValueError(errmsg("Unterminated string starting at", s, begin))

    # If not a unicode escape sequence, must be in the lookup table
    if esc != 'u' and esc != 'x':
      try:
        char = _b[esc]
      except KeyError:
        msg = "Invalid \\escape: " + repr(esc)
        raise ValueError(errmsg(msg, s, end))
      end += 1

    elif esc == 'x':
      # Hex escape sequence
      try:
        code = s[end + 1: end + 3]
        char = code.decode('hex')
      except:
        raise ValueError(errmsg('Invalid \\escape: ' + repr(code), s, end))

      end += 3

    else:
      # Unicode escape sequence
      uni = _decode_uXXXX(s, end)
      end += 5
      # Check for surrogate pair on UCS-4 systems
      if sys.maxunicode > 65535 and \
         0xd800 <= uni <= 0xdbff and s[end:end + 2] == '\\u':
        uni2 = _decode_uXXXX(s, end + 1)
        if 0xdc00 <= uni2 <= 0xdfff:
          uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00))
          end += 6
      char = unichr(uni)

    # Append the unescaped character
    _append(char)

  return u''.join(chunks), end



def make_pyon_scanner(context):
  parse_object = context.parse_object
  parse_array = context.parse_array
  parse_string = context.parse_string
  match_number = NUMBER_RE.match
  strict = context.strict
  parse_float = context.parse_float
  parse_int = context.parse_int
  parse_constant = context.parse_constant
  object_hook = context.object_hook
  object_pairs_hook = context.object_pairs_hook


  def scan_once(string, idx):
    try:
        nextchar = string[idx]
    except IndexError:
      raise StopIteration(idx)

    if nextchar == '"': return parse_string(string, idx + 1, 'utf-8', strict)
    elif nextchar == '{':
      return parse_object((string, idx + 1), 'utf-8', strict, scan_once,
                          object_hook, object_pairs_hook)
    elif nextchar == '[':
      return parse_array((string, idx + 1), scan_once)
    elif nextchar == 'N' and string[idx:idx + 4] == 'None':
      return None, idx + 4
    elif nextchar == 'T' and string[idx:idx + 4] == 'True':
      return True, idx + 4
    elif nextchar == 'F' and string[idx:idx + 5] == 'False':
      return False, idx + 5

    m = match_number(string, idx)
    if m is not None:
      integer, frac, exp = m.groups()
      if frac or exp:
        res = parse_float(integer + (frac or '') + (exp or ''))
      else: res = parse_int(integer)

      return res, m.end()

    elif nextchar == 'N' and string[idx:idx + 3] == 'NaN':
      return parse_constant('NaN'), idx + 3

    elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity':
      return parse_constant('Infinity'), idx + 8

    elif nextchar == '-' and string[idx:idx + 9] == '-Infinity':
      return parse_constant('-Infinity'), idx + 9

    else: raise StopIteration(idx)


  return scan_once


class PYONDecoder(json.JSONDecoder):
  def __init__(self, *args, **kwargs):
    json.JSONDecoder.__init__(self, *args, **kwargs)
    self.parse_string = pyon_scanstring
    self.scan_once = make_pyon_scanner(self)
from PYONDecoder import *