CVE-2022-23057 (Python, published 2022-06-22T08:15Z)
Description: value: In ERPNext, versions v12.0.9--v13.0.3 are vulnerable to Stored Cross-Site-Scripting (XSS), due to user input not being validated properly. A low privileged attacker could inject arbitrary code into input fields when editing his profile.
Severity: LOW (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/frappe/frappe
Files changed:
frappe/__init__.py
frappe/automation/doctype/auto_repeat/auto_repeat.py
frappe/core/doctype/report/report.py
frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py
frappe/custom/doctype/customize_form/customize_form.py
frappe/data_migration/doctype/data_migration_run/data_migration_run.py
frappe/desk/search.py
frappe/email/doctype/newsletter/newsletter.py
frappe/handler.py
frappe/model/document.py
frappe/public/js/frappe/form/controls/button.js
frappe/public/js/frappe/request.js
frappe/social/doctype/energy_point_log/energy_point_log.py
frappe/website/doctype/blog_post/blog_post.py
frappe/website/doctype/portal_settings/portal_settings.py
frappe/website/doctype/website_theme/website_theme.py
Commit message: feat: frappe.whitelist for class methods


Vulnerable code:
		whitelisted.append(fn)
	@Document.whitelist
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals
import json, inspect
import frappe
from frappe import _
from frappe.utils import cint
from six import text_type, string_types

@frappe.whitelist()
def runserverobj(method, docs=None, dt=None, dn=None, arg=None, args=None):
	"""run controller method - old style"""
	if not args: args = arg or ""

	if dt: # not called from a doctype (from a page)
		if not dn: dn = dt # single
		doc = frappe.get_doc(dt, dn)

	else:
		doc = frappe.get_doc(json.loads(docs))
		doc._original_modified = doc.modified
		doc.check_if_latest()

	if not doc.has_permission("read"):
		frappe.msgprint(_("Not permitted"), raise_exception = True)

	if doc:
		try:
			args = json.loads(args)
		except ValueError:
			args = args

		try:
			fnargs, varargs, varkw, defaults = inspect.getargspec(getattr(doc, method))
		except ValueError:
			fnargs = inspect.getfullargspec(getattr(doc, method)).args
			varargs = inspect.getfullargspec(getattr(doc, method)).varargs
			varkw = inspect.getfullargspec(getattr(doc, method)).varkw
			defaults = inspect.getfullargspec(getattr(doc, method)).defaults

		if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"):
			r = doc.run_method(method)

		elif "args" in fnargs or not isinstance(args, dict):
			r = doc.run_method(method, args)

		else:
			r = doc.run_method(method, **args)

		if r:
			#build output as csv
			if cint(frappe.form_dict.get('as_csv')):
				make_csv_output(r, doc.doctype)
			else:
				frappe.response['message'] = r

		frappe.response.docs.append(doc)

def make_csv_output(res, dt):
	"""send method response as downloadable CSV file"""
	import frappe

	from six import StringIO
	import csv

	f = StringIO()
	writer = csv.writer(f)
	for r in res:
		row = []
		for v in r:
			if isinstance(v, string_types):
				v = v.encode("utf-8")
			row.append(v)
		writer.writerow(row)

	f.seek(0)

	frappe.response['result'] = text_type(f.read(), 'utf-8')
	frappe.response['type'] = 'csv'
	frappe.response['doctype'] = dt.replace(' ','')
from frappe.handler import is_whitelisted
from frappe import _
	return fn(**kwargs)
from frappe import _
from frappe.utils.response import build_response
from frappe.api import validate_auth
from werkzeug.wrappers import Response
from six import string_types
	is_whitelisted(method)
	is_valid_http_method(method)
def is_whitelisted(method):
	# check if whitelisted
	if frappe.session['user'] == 'Guest':
		if (method not in frappe.guest_methods):
			frappe.throw(_("Not permitted"), frappe.PermissionError)

		if method not in frappe.xss_safe_methods:
			# strictly sanitize form_dict
			# escapes html characters like <> except for predefined tags like a, b, ul etc.
			for key, value in frappe.form_dict.items():
				if isinstance(value, string_types):
					frappe.form_dict[key] = frappe.utils.sanitize_html(value)

	else:
		if not method in frappe.whitelisted:
			frappe.throw(_("Not permitted"), frappe.PermissionError)

@frappe.whitelist()
def runserverobj(method, docs=None, dt=None, dn=None, arg=None, args=None):
	frappe.desk.form.run_method.runserverobj(method, docs=docs, dt=dt, dn=dn, arg=arg, args=args)

@frappe.whitelist(allow_guest=True)
def run_custom_method(doctype, name, custom_method):
	"""cmd=run_custom_method&doctype={doctype}&name={name}&custom_method={custom_method}"""
	doc = frappe.get_doc(doctype, name)
	if getattr(doc, custom_method, frappe._dict()).is_whitelisted:
		frappe.call(getattr(doc, custom_method), **frappe.local.form_dict)
	else:
		frappe.throw(_("Not permitted"), frappe.PermissionError)

@frappe.whitelist(allow_guest = True)
from frappe import _, msgprint
	def whitelist(f):
		f.whitelisted = True
		return f
	def is_whitelisted(self, method):
		fn = getattr(self, method, None)
			raise NotFound("Method {0} not found".format(method))
		elif not getattr(fn, "whitelisted", False):
			raise Forbidden("Method {0} not whitelisted".format(method))
				method: "runserverobj",
			cmd: "runserverobj",

Fixed code:
		# get function from the unbound / bound method
		# this is needed because functions can be compared, but not methods
		if hasattr(fn, '__func__'):
			fn = fn.__func__

		whitelisted.append(fn)
def is_whitelisted(method):
	from frappe.utils import sanitize_html

	is_guest = session['user'] == 'Guest'
	if method not in whitelisted or is_guest and method not in guest_methods:
		throw(_("Not permitted"), PermissionError)

	if is_guest and method not in xss_safe_methods:
		# strictly sanitize form_dict
		# escapes html characters like <> except for predefined tags like a, b, ul etc.
		for key, value in form_dict.items():
			if isinstance(value, string_types):
				form_dict[key] = sanitize_html(value)

	@frappe.whitelist()
	@frappe.whitelist()
	@frappe.whitelist()
	@frappe.whitelist()
	@frappe.whitelist()
	@frappe.whitelist()
	@frappe.whitelist()
	@frappe.whitelist()
	@frappe.whitelist()
	@frappe.whitelist()
	@frappe.whitelist()
from frappe import _, is_whitelisted
	return fn(**kwargs)
	@frappe.whitelist()

from werkzeug.wrappers import Response
from six import text_type, string_types, StringIO


from frappe.api import validate_auth
from frappe import _, is_whitelisted
from frappe.utils.response import build_response
from frappe.utils.csvutils import build_csv_response

	if method != run_doc_method:
		is_whitelisted(method)
		is_valid_http_method(method)
@frappe.whitelist(allow_guest=True)

@frappe.whitelist()
def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
	"""run controller method - old style"""
	import json, inspect

	if not args: args = arg or ""

	if dt: # not called from a doctype (from a page)
		if not dn: dn = dt # single
		doc = frappe.get_doc(dt, dn)

	else:
		doc = frappe.get_doc(json.loads(docs))
		doc._original_modified = doc.modified
		doc.check_if_latest()

	if not doc.has_permission("read"):
		frappe.msgprint(_("Not permitted"), raise_exception = True)

	if not doc:
		return

	try:
		args = json.loads(args)
	except ValueError:
		args = args

	method_obj = getattr(doc, method)
	is_whitelisted(getattr(method_obj, '__func__', method_obj))

	try:
		fnargs = inspect.getargspec(method_obj)[0]
	except ValueError:
		fnargs = inspect.getfullargspec(method_obj).args

	if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"):
		r = doc.run_method(method)

	elif "args" in fnargs or not isinstance(args, dict):
		r = doc.run_method(method, args)

	else:
		r = doc.run_method(method, **args)

	frappe.response.docs.append(doc)

	if not r:
		return

	# build output as csv
	if cint(frappe.form_dict.get('as_csv')):
		build_csv_response(r, doc.doctype.replace(' ', ''))
		return

	frappe.response['message'] = r

# for backwards compatibility
runserverobj = run_doc_method
from frappe import _, msgprint, is_whitelisted
	def whitelist(fn):
		frappe.whitelist()(fn)
		return fn
	def is_whitelisted(self, method_name):
		method = getattr(self, method_name, None)
			raise NotFound("Method {0} not found".format(method_name))

		is_whitelisted(getattr(method, '__func__', method))
				method: "run_doc_method",
			cmd: "run_doc_method",
	@frappe.whitelist()
	@frappe.whitelist()
	@frappe.whitelist()
	@frappe.whitelist()
	@frappe.whitelist()