Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ful14 security patches #48

Open
wants to merge 12 commits into
base: ful14
Choose a base branch
from
4 changes: 2 additions & 2 deletions addons/auth_signup/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from odoo import http, _
from odoo.addons.auth_signup.models.res_users import SignupError
from odoo.addons.web.controllers.main import ensure_db, Home
from odoo.addons.web.controllers.main import ensure_db, Home, SIGN_UP_REQUEST_PARAMS
from odoo.addons.base_setup.controllers.main import BaseSetup
from odoo.exceptions import UserError
from odoo.http import request
Expand Down Expand Up @@ -101,7 +101,7 @@ def get_auth_signup_config(self):

def get_auth_signup_qcontext(self):
""" Shared helper returning the rendering context for signup and reset password """
qcontext = request.params.copy()
qcontext = {k: v for (k, v) in request.params.items() if k in SIGN_UP_REQUEST_PARAMS}
qcontext.update(self.get_auth_signup_config())
if not qcontext.get('token') and request.session.get('auth_signup_token'):
qcontext['token'] = request.session.get('auth_signup_token')
Expand Down
7 changes: 3 additions & 4 deletions addons/http_routing/models/ir_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
slugify_lib = None

import odoo
from odoo import api, models, registry, exceptions, tools
from odoo import api, models, registry, exceptions, tools, http
from odoo.addons.base.models.ir_http import RequestUID, ModelConverter
from odoo.addons.base.models.qweb import QWebException
from odoo.http import request
Expand Down Expand Up @@ -653,8 +653,7 @@ def _handle_exception(cls, exception):
@tools.ormcache('path')
def url_rewrite(self, path):
new_url = False
req = request.httprequest
router = req.app.get_db_router(request.db).bind('')
router = http.root.get_db_router(request.db).bind('')
try:
_ = router.match(path, method='POST')
except werkzeug.exceptions.MethodNotAllowed:
Expand All @@ -672,7 +671,7 @@ def url_rewrite(self, path):
@api.model
@tools.cache('path', 'query_args')
def _get_endpoint_qargs(self, path, query_args=None):
router = request.httprequest.app.get_db_router(request.db).bind('')
router = http.root.get_db_router(request.db).bind('')
endpoint = False
try:
endpoint = router.match(path, method='POST', query_args=query_args)
Expand Down
8 changes: 5 additions & 3 deletions addons/l10n_fr_fec/wizard/account_fr_fec.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import io

from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.exceptions import UserError, AccessDenied
from odoo.tools import float_is_zero, pycompat


Expand All @@ -30,7 +30,7 @@ def _onchange_export_file(self):
if not self.test_file:
self.export_type = 'official'

def do_query_unaffected_earnings(self):
def _do_query_unaffected_earnings(self):
''' Compute the sum of ending balances for all accounts that are of a type that does not bring forward the balance in new fiscal years.
This is needed because we have to display only one line for the initial balance of all expense/revenue accounts in the FEC.
'''
Expand Down Expand Up @@ -103,6 +103,8 @@ def _get_company_legal_data(self, company):

def generate_fec(self):
self.ensure_one()
if not (self.env.is_admin() or self.env.user.has_group('account.group_account_user')):
raise AccessDenied()
# We choose to implement the flat file instead of the XML
# file for 2 reasons :
# 1) the XSD file impose to have the label on the account.move
Expand Down Expand Up @@ -147,7 +149,7 @@ def generate_fec(self):
unaffected_earnings_line = True # used to make sure that we add the unaffected earning initial balance only once
if unaffected_earnings_xml_ref:
#compute the benefit/loss of last year to add in the initial balance of the current year earnings account
unaffected_earnings_results = self.do_query_unaffected_earnings()
unaffected_earnings_results = self._do_query_unaffected_earnings()
unaffected_earnings_line = False

sql_query = '''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,9 @@ function factory(dependencies) {
notificationTitle = owl.utils.escape(authorName);
}
}
const notificationContent = htmlToTextContentInline(message.body).substr(0, PREVIEW_MSG_MAX_SIZE);
const notificationContent = owl.utils.escape(
htmlToTextContentInline(message.body).substr(0, PREVIEW_MSG_MAX_SIZE)
);
this.env.services['bus_service'].sendNotification(notificationTitle, notificationContent);
messaging.update({ outOfFocusUnreadMessageCounter: increment() });
const titlePattern = messaging.outOfFocusUnreadMessageCounter === 1
Expand Down
6 changes: 3 additions & 3 deletions addons/sale/models/sale.py
Original file line number Diff line number Diff line change
Expand Up @@ -1041,12 +1041,12 @@ def _create_payment_transaction(self, vals):
if payment_token and payment_token.acquirer_id != acquirer:
raise ValidationError(_('Invalid token found! Token acquirer %s != %s') % (
payment_token.acquirer_id.name, acquirer.name))
if payment_token and payment_token.partner_id != partner:
raise ValidationError(_('Invalid token found! Token partner %s != %s') % (
payment_token.partner.name, partner.name))
else:
acquirer = payment_token.acquirer_id

if payment_token and payment_token.partner_id != partner:
raise ValidationError(_('Invalid token found!'))

# Check an acquirer is there.
if not acquirer_id and not acquirer:
raise ValidationError(_('A payment acquirer is required to create a transaction.'))
Expand Down
11 changes: 7 additions & 4 deletions addons/web/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,8 @@ def redirect_with_hash(*args, **kw):
return http.redirect_with_hash(*args, **kw)

def abort_and_redirect(url):
r = request.httprequest
response = werkzeug.utils.redirect(url, 302)
response = r.app.get_response(r, response, explicit_session=False)
response = http.root.get_response(request.httprequest, response, explicit_session=False)
werkzeug.exceptions.abort(response)

def ensure_db(redirect='/web/database/selector'):
Expand Down Expand Up @@ -647,6 +646,10 @@ def get_qweb_templates_checksum(cls, addons, db=None, debug=False):
def get_qweb_templates(cls, addons, db=None, debug=False):
return cls(addons, db, debug=debug)._get_qweb_templates()[0]

# Shared parameters for all login/signup flows
SIGN_UP_REQUEST_PARAMS = {'db', 'login', 'debug', 'token', 'message', 'error', 'scope', 'mode',
'redirect', 'redirect_hostname', 'email', 'name', 'partner_id',
'password', 'confirm_password', 'city', 'country_id', 'lang'}

class GroupsTreeNode:
"""
Expand Down Expand Up @@ -934,7 +937,7 @@ def web_login(self, redirect=None, **kw):
if not request.uid:
request.uid = odoo.SUPERUSER_ID

values = request.params.copy()
values = {k: v for k, v in request.params.items() if k in SIGN_UP_REQUEST_PARAMS}
try:
values['databases'] = http.db_list()
except odoo.exceptions.AccessDenied:
Expand Down Expand Up @@ -1102,7 +1105,7 @@ def post(self, path):
from werkzeug.wrappers import BaseResponse
base_url = request.httprequest.base_url
query_string = request.httprequest.query_string
client = Client(request.httprequest.app, BaseResponse)
client = Client(http.root, BaseResponse)
headers = {'X-Openerp-Session-Id': request.session.sid}
return client.post('/' + path, base_url=base_url, query_string=query_string,
headers=headers, data=data)
Expand Down
23 changes: 7 additions & 16 deletions addons/web/static/src/js/fields/basic_fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -2088,22 +2088,13 @@ var FieldBinaryFile = AbstractFieldBinary.extend({
this.filename_value = this.recordData[this.attrs.filename];
},
_renderReadonly: function () {
this.do_toggle(!!this.value);
if (this.value) {
this.$el.empty().append($("<span/>").addClass('fa fa-download'));
if (this.recordData.id) {
this.$el.css('cursor', 'pointer');
} else {
this.$el.css('cursor', 'not-allowed');
}
if (this.filename_value) {
this.$el.append(" " + this.filename_value);
}
}
if (!this.res_id) {
this.$el.css('cursor', 'not-allowed');
} else {
this.$el.css('cursor', 'pointer');
var visible = !!(this.value && this.res_id);
this.$el.empty().css('cursor', 'not-allowed');
this.do_toggle(visible);
if (visible) {
this.$el.css('cursor', 'pointer')
.text(this.filename_value || '')
.prepend($('<span class="fa fa-download"/>'), ' ');
}
},
_renderEdit: function () {
Expand Down
13 changes: 10 additions & 3 deletions addons/web/static/tests/fields/basic_fields_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2441,7 +2441,7 @@ QUnit.module('basic_fields', {
});

QUnit.test('binary fields that are readonly in create mode do not download', async function (assert) {
assert.expect(2);
assert.expect(4);

// save the session function
var oldGetFile = session.get_file;
Expand Down Expand Up @@ -2473,10 +2473,17 @@ QUnit.module('basic_fields', {
await testUtils.fields.many2one.clickOpenDropdown('product_id');
await testUtils.fields.many2one.clickHighlightedItem('product_id');

assert.containsOnce(form, 'a.o_field_widget[name="document"] > .fa-download',
assert.containsOnce(form, 'a.o_field_widget[name="document"]',
'The link to download the binary should be present');
assert.containsNone(form, 'a.o_field_widget[name="document"] > .fa-download',
'The download icon should not be present');

var link = form.$('a.o_field_widget[name="document"]');
assert.ok(link.is(':hidden'), "the link element should not be visible");

testUtils.dom.click(form.$('a.o_field_widget[name="document"]'));
// force visibility to test that the clicking has also been disabled
link.removeClass('o_hidden');
testUtils.dom.click(link);

assert.verifySteps([]); // We shouldn't have passed through steps

Expand Down
6 changes: 3 additions & 3 deletions addons/website/models/website.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from werkzeug.datastructures import OrderedMultiDict
from werkzeug.exceptions import NotFound

from odoo import api, fields, models, tools
from odoo import api, fields, models, tools, http
from odoo.addons.http_routing.models.ir_http import slugify, _guess_mimetype, url_for
from odoo.addons.website.models.ir_http import sitemap_qs2dom
from odoo.addons.portal.controllers.portal import pager
Expand Down Expand Up @@ -794,7 +794,7 @@ def _enumerate_pages(self, query_string=None, force=False):
:rtype: list({name: str, url: str})
"""

router = request.httprequest.app.get_db_router(request.db)
router = http.root.get_db_router(request.db)
# Force enumeration to be performed as public user
url_set = set()

Expand Down Expand Up @@ -973,7 +973,7 @@ def _get_canonical_url_localized(self, lang, canonical_params):
"""
self.ensure_one()
if request.endpoint:
router = request.httprequest.app.get_db_router(request.db).bind('')
router = http.root.get_db_router(request.db).bind('')
arguments = dict(request.endpoint_arguments)
for key, val in list(arguments.items()):
if isinstance(val, models.BaseModel):
Expand Down
21 changes: 18 additions & 3 deletions addons/website/static/src/js/content/snippets.animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,10 @@ registry.mediaVideo = publicWidget.Widget.extend({

var def = this._super.apply(this, arguments);
if (this.$target.children('iframe').length) {
// There already is an <iframe/>, do nothing
// There already is an <iframe/>, do nothing. This is the normal
// case. The whole code that follows is only there to ensure
// compatibility with videos added before bug fixes or new Odoo
// versions where the <iframe/> element is properly saved.
return def;
}

Expand All @@ -626,11 +629,23 @@ registry.mediaVideo = publicWidget.Widget.extend({
// the src is saved in the 'data-src' attribute or the
// 'data-oe-expression' one (the latter is used as a workaround in 10.0
// system but should obviously be reviewed in master).
var src = _.escape(this.$target.data('oe-expression') || this.$target.data('src'));
// Validate the src to only accept supported domains we can trust
var m = src.match(/^(?:https?:)?\/\/([^/?#]+)/);
if (!m) {
// Unsupported protocol or wrong URL format, don't inject iframe
return def;
}
var domain = m[1].replace(/^www\./, '');
var supportedDomains = ['youtu.be', 'youtube.com', 'youtube-nocookie.com', 'instagram.com', 'vine.co', 'player.vimeo.com', 'vimeo.com', 'dailymotion.com', 'player.youku.com', 'youku.com'];
if (!_.contains(supportedDomains, domain)) {
// Unsupported domain, don't inject iframe
return def;
}
this.$target.append($('<iframe/>', {
src: _.escape(this.$target.data('oe-expression') || this.$target.data('src')),
src: src,
frameborder: '0',
allowfullscreen: 'allowfullscreen',
sandbox: 'allow-scripts allow-same-origin', // https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
}));

return def;
Expand Down
6 changes: 5 additions & 1 deletion odoo/addons/base/models/assetsbundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
# If the `sass` python library isn't found, we fallback on the
# `sassc` executable in the path.
libsass = None
from contextlib import closing
from datetime import datetime
from subprocess import Popen, PIPE
from collections import OrderedDict
from odoo import fields, tools, SUPERUSER_ID
from odoo.tools.pycompat import to_text
from odoo.tools.misc import file_open
from odoo.http import request
from odoo.modules.module import get_resource_path
from .qweb import escape
Expand All @@ -27,6 +29,8 @@
import logging
_logger = logging.getLogger(__name__)

EXTENSIONS = (".js", ".css", ".scss", ".sass", ".less")


class CompileError(RuntimeError): pass
def rjsmin(script):
Expand Down Expand Up @@ -676,7 +680,7 @@ def _fetch_content(self):
try:
self.stat()
if self._filename:
with open(self._filename, 'rb') as fp:
with closing(file_open(self._filename, 'rb', filter_ext=EXTENSIONS)) as fp:
return fp.read().decode('utf-8')
else:
return base64.b64decode(self._ir_attach['datas']).decode('utf-8')
Expand Down
20 changes: 10 additions & 10 deletions odoo/addons/base/models/ir_actions_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,13 +728,13 @@ def _render_qweb_pdf(self, res_ids=None, data=None):
data = {}
data.setdefault('report_type', 'pdf')

# access the report details with sudo() but evaluation context as current user
# access the report details with sudo() but evaluation context as sudo(False)
self_sudo = self.sudo()

# In case of test environment without enough workers to perform calls to wkhtmltopdf,
# fallback to render_html.
if (tools.config['test_enable'] or tools.config['test_file']) and not self.env.context.get('force_report_rendering'):
return self._render_qweb_html(res_ids, data=data)
return self_sudo._render_qweb_html(res_ids, data=data)

# As the assets are generated during the same transaction as the rendering of the
# templates calling them, there is a scenario where the assets are unreachable: when
Expand Down Expand Up @@ -831,7 +831,7 @@ def _render_qweb_text(self, docids, data=None):
data = {}
data.setdefault('report_type', 'text')
data = self._get_rendering_context(docids, data)
return self._render_template(self.sudo().report_name, data), 'text'
return self._render_template(self.report_name, data), 'text'

@api.model
def _render_qweb_html(self, docids, data=None):
Expand All @@ -841,29 +841,29 @@ def _render_qweb_html(self, docids, data=None):
data = {}
data.setdefault('report_type', 'html')
data = self._get_rendering_context(docids, data)
return self._render_template(self.sudo().report_name, data), 'html'
return self._render_template(self.report_name, data), 'html'

def _get_rendering_context_model(self):
report_model_name = 'report.%s' % self.report_name
return self.env.get(report_model_name)

def _get_rendering_context(self, docids, data):
# access the report details with sudo() but evaluation context as current user
self_sudo = self.sudo()

# If the report is using a custom model to render its html, we must use it.
# Otherwise, fallback on the generic html rendering.
report_model = self_sudo._get_rendering_context_model()
report_model = self._get_rendering_context_model()

data = data and dict(data) or {}

if report_model is not None:
# _render_ may be executed in sudo but evaluation context as real user
report_model = report_model.sudo(False)
data.update(report_model._get_report_values(docids, data=data))
else:
docs = self.env[self_sudo.model].browse(docids)
# _render_ may be executed in sudo but evaluation context as real user
docs = self.env[self.model].sudo(False).browse(docids)
data.update({
'doc_ids': docids,
'doc_model': self_sudo.model,
'doc_model': self.model,
'docs': docs,
})
return data
Expand Down
5 changes: 5 additions & 0 deletions odoo/addons/base/models/ir_demo.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import models
from odoo.modules.loading import force_demo
from odoo.addons.base.models.ir_module import assert_log_admin_access


class IrDemo(models.TransientModel):

_name = 'ir.demo'
_description = 'Demo'

@assert_log_admin_access
def install_demo(self):
force_demo(self.env.cr)
return {
Expand Down
2 changes: 1 addition & 1 deletion odoo/addons/base/models/ir_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def assert_log_admin_access(method):
def check_and_log(method, self, *args, **kwargs):
user = self.env.user
origin = request.httprequest.remote_addr if request else 'n/a'
log_data = (method.__name__, self.sudo().mapped('name'), user.login, user.id, origin)
log_data = (method.__name__, self.sudo().mapped('display_name'), user.login, user.id, origin)
if not self.env.is_admin():
_logger.warning('DENY access to module.%s on %s to user %s ID #%s via %s', *log_data)
raise AccessDenied()
Expand Down
Loading