[ADD] *: CSRF protection in forms and routes

* make CSRF protection the default on all non-SAFE methods
  note: there currently is no way to call a CSRF-protected endpoint
  without a form-encoded entity-body as that's the only place we get the
  CSRF token from.
* simple CSRF token generation: just use the HMAC'd session id, no
  generating a new random token per session then HMAC it
* use constant-time equal function to avoid timing attacks
* assert that a database secret is configured before hashing/validating
  the CSRF token
* opt-out database manager from CSRF: The super-admin password serves
  the purpose of a CSRF token in the database manager screens.
  There is no request database to obtain the
  secret and generate a CSRF token.
This commit is contained in:
Christophe Simonis
2015-10-01 01:36:50 +02:00
committed by Olivier Dony
parent 058b33544f
commit 7636b510a2
33 changed files with 139 additions and 32 deletions
@@ -38,6 +38,7 @@
<template id="auth_signup.signup" name="Sign up login">
<t t-call="web.login_layout">
<form class="oe_signup_form" role="form" method="post" t-if="not message">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<t t-call="auth_signup.fields">
<t t-set="only_passwords" t-value="bool(token)"/>
@@ -67,6 +68,7 @@
</div>
<form class="oe_reset_password_form" role="form" method="post" t-if="not message">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<t t-if="token">
<t t-call="auth_signup.fields">
+1 -1
View File
@@ -4,7 +4,7 @@ import json
from openerp.http import Controller, route
class ImportController(Controller):
@route('/base_import/set_file')
@route('/base_import/set_file', methods=['POST'])
def set_file(self, req, file, import_id, jsonp='callback'):
import_id = int(import_id)
+1 -1
View File
@@ -37,7 +37,7 @@ class OgoneController(http.Controller):
new_id = acquirer.s2s_process(data)
return new_id
@http.route(['/payment/ogone/s2s/create'], type='http', auth='public')
@http.route(['/payment/ogone/s2s/create'], type='http', auth='public', methods=["POST"])
def ogone_s2s_create(self, **post):
acquirer_id = int(post.get('acquirer_id'))
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
+1
View File
@@ -55,6 +55,7 @@
<template id="ogone_s2s_form">
<form method="post" t-att-action="'/payment/ogone/s2s/create' if not json else '/payment/ogone/s2s/create_json'">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div t-att-class="'row' if bootstrap_formatting else ''">
<div t-att-class="'form-group col-md-8' if bootstrap_formatting else 'form-group'">
<label class="control-label" for="cc_number">Card number</label>
+1 -1
View File
@@ -14,7 +14,7 @@ class OgoneController(http.Controller):
@http.route([
'/payment/transfer/feedback',
], type='http', auth='none')
], type='http', auth='none', csrf=False)
def transfer_form_feedback(self, **post):
cr, uid, context = request.cr, SUPERUSER_ID, request.context
_logger.info('Beginning form_feedback with post data %s', pprint.pformat(post)) # debug
@@ -1,4 +1,4 @@
# -*- coding: utf-'8' "-*-"
# -*- coding: utf-8 -*-
from openerp.addons.payment.models.payment_acquirer import ValidationError
from openerp.osv import osv
+2 -2
View File
@@ -4,6 +4,7 @@
<template id="transfer_acquirer_button">
<form t-if="acquirer" t-att-action="tx_url" method="post" target="_self">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<t t-if="return_url">
<input type='hidden' name='return_url' t-att-value='return_url'/>
</t>
@@ -11,8 +12,7 @@
<input type='hidden' name='amount' t-att-value='amount'/>
<input type='hidden' name='currency' t-att-value='currency.name'/>
<!-- submit -->
<button type="submit" width="100px"
t-att-class="submit_class">
<button type="submit" width="100px" t-att-class="submit_class">
<img t-if="not submit_txt" src="/payment_transfer/static/src/img/transfer_icon.png"/>
<span t-if="submit_txt"><t t-esc="submit_txt"/> <span class="fa fa-long-arrow-right"/></span>
</button>
+1 -1
View File
@@ -108,7 +108,7 @@ class WebsiteSurvey(http.Controller):
# Survey displaying
@http.route(['/survey/fill/<model("survey.survey"):survey>/<string:token>',
'/survey/fill/<model("survey.survey"):survey>/<string:token>/<string:prev>'],
type='http', auth='public', website=True)
type='http', auth='public', website=True, methods=['POST'])
def fill_survey(self, survey, token, prev=None, **post):
'''Display and validates a survey'''
cr, uid, context = request.cr, request.uid, request.context
+1
View File
@@ -117,6 +117,7 @@
</div>
<form role="form" method="post" class="js_surveyform" t-att-name="'%s_%s' % (survey.id, page.id)" t-att-action="'/survey/fill/%s/%s' % (slug(survey), token)" t-att-data-prefill="'/survey/prefill/%s/%s/%s' % (slug(survey), token, slug(page))" t-att-data-validate="'/survey/validate/%s' % (slug(survey))" t-att-data-submit="'/survey/submit/%s' % (slug(survey))">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="page_id" t-att-value="page.id" />
<input type="hidden" name="token" t-att-value="token" />
<t t-foreach='page.question_ids' t-as='question'>
+6 -6
View File
@@ -736,7 +736,7 @@ class Database(http.Controller):
def manager(self, **kw):
return self._render_template()
@http.route('/web/database/create', type='http', auth="none")
@http.route('/web/database/create', type='http', auth="none", methods=['POST'], csrf=False)
def create(self, master_pwd, name, lang, password, **post):
try:
request.session.proxy("db").create_database(master_pwd, name, bool(post.get('demo')), lang, password)
@@ -746,7 +746,7 @@ class Database(http.Controller):
error = "Database creation error: %s" % e
return self._render_template(error=error)
@http.route('/web/database/duplicate', type='http', auth="none")
@http.route('/web/database/duplicate', type='http', auth="none", methods=['POST'], csrf=False)
def duplicate(self, master_pwd, name, new_name):
try:
request.session.proxy("db").duplicate_database(master_pwd, name, new_name)
@@ -755,7 +755,7 @@ class Database(http.Controller):
error = "Database duplication error: %s" % e
return self._render_template(error=error)
@http.route('/web/database/drop', type='http', auth="none")
@http.route('/web/database/drop', type='http', auth="none", methods=['POST'], csrf=False)
def drop(self, master_pwd, name):
try:
request.session.proxy("db").drop(master_pwd, name)
@@ -764,7 +764,7 @@ class Database(http.Controller):
error = "Database deletion error: %s" % e
return self._render_template(error=error)
@http.route('/web/database/backup', type='http', auth="none")
@http.route('/web/database/backup', type='http', auth="none", methods=['POST'], csrf=False)
def backup(self, master_pwd, name, backup_format = 'zip'):
try:
openerp.service.db.check_super(master_pwd)
@@ -782,7 +782,7 @@ class Database(http.Controller):
error = "Database backup error: %s" % e
return self._render_template(error=error)
@http.route('/web/database/restore', type='http', auth="none")
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
def restore(self, master_pwd, backup_file, name, copy=False):
try:
data = base64.b64encode(backup_file.read())
@@ -792,7 +792,7 @@ class Database(http.Controller):
error = "Database restore error: %s" % e
return self._render_template(error=error)
@http.route('/web/database/change_password', type='http', auth="none")
@http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False)
def change_password(self, master_pwd, master_pwd_new):
try:
request.session.proxy("db").change_admin_password(master_pwd, master_pwd_new)
+1
View File
@@ -202,6 +202,7 @@
<t t-call="web.login_layout">
<form class="oe_login_form" role="form" t-attf-action="/web/login{{ '?debug' if debug else '' }}" method="post" onsubmit="this.action = this.action + location.hash">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group field-db" t-if="databases and len(databases) &gt; 1">
<label for="db" class="control-label">Database</label>
@@ -88,6 +88,8 @@
enctype="multipart/form-data"
target="fileframe"
class="form-inline">
<!-- why is this template rendered client side? -->
<!-- input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/-->
<div class="well">
<div class="form-group pull-left">
<input type="file" name="upload" t-att-accept="widget.accept" multiple="multiple" style="position: absolute; opacity: 0; width: 1px; height: 1px;"/>
+1
View File
@@ -87,6 +87,7 @@ response = request.website.render("website.template_partner_post", values)
<div class="container">
<div class="row">
<form class="form-horizontal" action="/website/action/website.action_partner_post" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group">
<label class="col-sm-2 control-label">Recipient</label>
<div class="col-sm-10">
@@ -19,6 +19,7 @@
</div>
<div class="modal-body">
<form action="/web_editor/attachment/add" method="post" enctype="multipart/form-data" >
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group">
<input name="upload" type="file" value="Choose images" multiple="multiple" accept="image/*"/>
</div>
@@ -846,6 +846,7 @@
<p>An error occured while rendering the template <code t-esc="qweb_exception.qweb['template']"/>.</p>
<p>If this error is caused by a change of yours in the templates, you have the possibility to reset one or more templates to their <strong>factory settings</strong>.</p>
<form action="/website/reset_templates" method="post" id="reset_templates_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<ul class="oe_template_fallback">
<li t-foreach="views" t-as="view">
<label>
+1 -1
View File
@@ -314,7 +314,7 @@ class WebsiteBlog(http.Controller):
new_blog_post = request.registry['blog.post'].browse(cr, uid, new_blog_post_id, context=context)
return werkzeug.utils.redirect("/blog/%s/post/%s?enable_editor=1" % (slug(new_blog_post.blog_id), slug(new_blog_post)))
@http.route('/blog/post_duplicate', type='http', auth="public", website=True)
@http.route('/blog/post_duplicate', type='http', auth="public", website=True, methods=['POST'])
def blog_post_copy(self, blog_post_id, **post):
""" Duplicate a blog.
@@ -195,10 +195,11 @@
<t t-set="object" t-value="blog_post"/>
<t t-set="publish_edit" t-value="True"/>
<li>
<form class="duplicate hidden" action="/blog/post_duplicate">
<input name="blog_post_id" t-att-value="blog_post.id"/>
</form>
<a href="#" class="duplicate" onclick="$(this).prev('form').submit()">Duplicate</a>
<form class="duplicate hidden" action="/blog/post_duplicate" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input name="blog_post_id" t-att-value="blog_post.id"/>
</form>
<a href="#" class="duplicate" onclick="$(this).prev('form').submit()">Duplicate</a>
</li>
</t>
</div>
+1
View File
@@ -5,6 +5,7 @@
<xpath expr="//div[@name='mail_button']" position="replace">
<div>
<form action="/website_form/" method="post" data-model_name="crm.lead" data-success_page="/page/website_crm.contactus_thanks" class="s_website_form form-horizontal container-fluid mt32" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group form-field o_website_form_required_custom">
<label class="col-md-3 col-sm-4 control-label" for="contact_name">Your Name</label>
<div class="col-md-7 col-sm-8">
@@ -379,6 +379,7 @@
<!-- Registration Templates -->
<template id="registration_template">
<form id="registration_form" t-attf-action="/event/#{slug(event)}/registration/new" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<table itemprop="offers" class="table table-striped">
<thead>
<tr>
@@ -428,6 +429,7 @@
<div id="modal_attendees_registration" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg">
<form id="attendee_registration" t-attf-action="/event/#{slug(event)}/registration/confirm" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
@@ -335,6 +335,7 @@
</div>
<section id="forms" t-if="event.show_track_proposal">
<form class="form-horizontal mt32 js_website_submit_form" t-attf-action="/event/#{event.id}/track_proposal/post" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group">
<label class="col-md-3 col-sm-4 control-label" for="partner_name">Your Name</label>
<div class="col-md-7 col-sm-8">
@@ -33,6 +33,7 @@
<!-- helper -->
<template id="link_button">
<form method="POST" t-att-action="url">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<button t-attf-class="fa btn-link #{classes} #{karma and 'karma_required text-muted' or ''}" t-attf-data-karma="#{karma}">
<t t-esc="label"/></button>
</form>
@@ -479,6 +480,7 @@
posts will be featured.
</p>
<form t-att-action="action_url" method="post" role="form" class="tag_text form-horizontal js_website_submit_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
<input type="hidden" name="karma_retag" t-attf-value="#{forum.karma_retag}" id="karma_retag"/>
<div class="form-group">
@@ -521,6 +523,7 @@
<b>Share</b> Something Awesome.
</p>
<form t-attf-action="/forum/#{slug(forum)}/new?post_type=discussion" method="post" role="form" class="tag_text js_website_submit_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="text" name="post_name" required="True" t-attf-value="#{post_name}"
class="form-control mb16" placeholder="Your Discussion Title..."/>
<input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
@@ -558,6 +561,7 @@
<li>Provide enough details and, if possible, give an example.</li>
</ul>
<form t-attf-action="/forum/#{slug(forum)}/new?post_type=question" method="post" role="form" class="tag_text js_website_submit_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="text" name="post_name" required="True" pattern=".*\S.*" t-attf-value="#{post_name}"
class="form-control mb16" placeholder="Your Question Title..." title="Title must not be empty"/>
<input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
@@ -583,6 +587,7 @@
<h3 t-if="not is_answer">Edit <span t-field="post.post_type"/></h3>
<h3 t-if="is_answer">Edit reply</h3>
<form t-attf-action="/forum/#{slug(forum)}/post/#{slug(post)}/save" method="post" role="form" class="tag_text js_website_submit_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div t-if="not is_answer">
<input type="text" name="post_name" required="True" pattern=".*\S.*" t-attf-value="#{post.name}"
class="form-control mb8" placeholder="Edit your Post" title="Title must not be empty"/>
@@ -620,6 +625,7 @@
them.
</p>
<form t-attf-action="/forum/#{ slug(forum) }/#{offensive and 'post' or 'question'}/#{slug(question)}/#{offensive and 'mark_as_offensive' or 'close'}" method="post" role="form" class="form-horizontal mt32 mb64 js_website_submit_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input name="post_id" t-att-value="question.id" type="hidden"/>
<div class="form-group">
<label class="col-md-3 control-label" for="reason">Post:</label>
@@ -659,6 +665,7 @@
<form t-attf-id="reply#{ object._name.replace('.','') + '-' + str(object.id) }" class="collapse js_website_submit_form"
t-attf-action="/forum/#{ slug(forum) }/#{slug(object)}/reply" method="post" role="form">
<h3 class="mt8">Your Reply</h3>
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
<textarea name="content" t-attf-id="content-#{str(object.id)}" class="form-control load_editor" required="True"
t-att-data-karma="forum.karma_editor"/>
@@ -678,6 +685,7 @@
- it really helps to select the best questions and answers!
</p>
<form t-attf-action="/forum/#{ slug(forum) }/#{slug(question)}/reply" method="post" class="js_website_submit_form" role="form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
<textarea name="content" t-attf-id="content-#{str(question.id)}" class="form-control load_editor" required="True"
t-att-data-karma="forum.karma_editor"/>
@@ -1043,6 +1051,7 @@
<div class="css_editable_mode_hidden">
<form t-attf-id="comment#{ object._name.replace('.','') + '-' + str(object.id) }" class="collapse oe_comment_grey js_website_submit_form"
t-attf-action="/forum/#{slug(forum)}/post/#{slug(object)}/comment" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input name="post_id" t-att-value="object.id" type="hidden" class="mt8"/>
<textarea name="comment" class="form-control" placeholder="Comment this post..."/>
<button type="submit" class="btn btn-primary mt8">Post</button>
@@ -1249,6 +1258,7 @@
</div>
<div class="col-md-10">
<form t-attf-action="/forum/#{slug(forum)}/user/#{slug(user)}/save" method="post" role="form" class="form-horizontal js_website_submit_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input name="user_id" t-att-value="user.id" type="hidden"/>
<div class="form-group">
<label class="col-md-2 control-label mb16" for="user_name">Real name</label>
@@ -198,6 +198,7 @@
<div class="container">
<div class="well">
<form t-attf-action="/forum/#{ slug(forum) }/promote_ok" method="post" role="form" class="form-horizontal">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input name="post_id" t-att-value="post.id" type="hidden"/>
<div class="form-group">
<label class="col-md-3 control-label" for="reason">Question:</label>
@@ -152,6 +152,7 @@
<div class="row">
<section id="forms">
<form action="/website_form/" method="post" class="s_website_form form-horizontal container-fluid mt32" enctype="multipart/form-data" data-model_name="hr.applicant" data-success_page="/page/website_hr_recruitment.thankyou" t-att-data-form_field_department_id="job and job.department_id.id or False" t-att-data-form_field_job_id="job and job.id or False">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group form-field o_website_form_required_custom">
<div class="col-md-3 col-sm-4 text-right">
<label class="control-label" for="partner_name">Your Name</label>
@@ -44,6 +44,7 @@
<t t-set="can_comment" t-value="sha_in or token or not is_user_public"/>
<section class="mb32 hidden-print" t-if="can_comment">
<form class="o_website_chatter_form" t-attf-action="/website_mail/post/#{chatter_mode}" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<img class="img pull-left img-circle" t-attf-src="/web/image/res.partner/#{user_id.partner_id.id}/image_small/50x50" style="width: 50px; margin-right: 10px;"/>
<div class="pull-left mb32" style="width: 75%%">
<textarea rows="4" name="message" class="form-control" placeholder="Write a message..."></textarea>
@@ -4,7 +4,8 @@
<template id="unsubscribe">
<div class="container o_unsubscribe_form">
<div class="row">
<form t-attf-action="/mail/mailing/unsubscribe" method="POST" id="unsubscribe_form">
<form action="/mail/mailing/unsubscribe" method="POST" id="unsubscribe_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="email" t-att-value="email"/>
<input type="hidden" name="mailing_id" t-att-value="mailing_id"/>
@@ -69,4 +70,4 @@
</t>
</template>
</data>
</openerp>
</openerp>
@@ -60,6 +60,7 @@
<div class="row">
<div class="col-md-6">
<form method="post" action="/website_payment/delete/">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group">
<select name="delete_pm_id" class="form-control" >
<t t-foreach="pms" t-as="pm">
@@ -63,6 +63,7 @@
</div>
<h1>Contact Details</h1>
<form action="/my/account" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="row o_website_portal_details">
<div class="col-md-8">
<div class="row">
+2 -2
View File
@@ -77,7 +77,7 @@ class sale_quote(http.Controller):
context=render_ctx)
return request.website.render('website_quote.so_quotation', values)
@http.route(['/quote/accept'], type='json', auth="public", website=True)
@http.route(['/quote/accept/<int:order_id>'], type='http', auth="public", website=True, methods=['POST'])
def accept(self, order_id, token=None, signer=None, sign=None, **post):
order_obj = request.registry.get('sale.order')
order = order_obj.browse(request.cr, SUPERUSER_ID, order_id)
@@ -91,7 +91,7 @@ class sale_quote(http.Controller):
_message_post_helper(message=message, res_id=order_id, res_model='sale.order', attachments=attachments, **({'token': token, 'token_field': 'access_token'} if token else {}))
return True
@http.route(['/quote/<int:order_id>/<token>/decline'], type='http', auth="public", website=True)
@http.route(['/quote/<int:order_id>/<token>/decline'], type='http', auth="public", methods=['POST'], website=True)
def decline(self, order_id, token, **post):
order_obj = request.registry.get('sale.order')
order = order_obj.browse(request.cr, SUPERUSER_ID, order_id)
@@ -229,7 +229,8 @@
<!-- modal relative to the actions Accept/Reject/Cancel -->
<div class="modal fade" id="modalaccept" role="dialog" aria-hidden="true">
<div class="modal-dialog" t-if="not quotation.require_payment and not need_payment">
<form id="accept" method="POST" t-attf-action="/quote/accept/#{quotation.id}/?token=#{quotation.access_token}" class="js_accept_json modal-content js_website_submit_form">
<form id="accept" method="POST" t-attf-action="/quote/accept/#{quotation.id}/?token=#{quotation.access_token}" class="js_accept_json modal-content js_website_submit_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&amp;times;</button>
<h4 class="modal-title">Validate Order</h4>
@@ -305,6 +306,7 @@
<div class="modal fade" id="modaldecline" role="dialog" aria-hidden="true">
<div class="modal-dialog">
<form id="decline" method="POST" t-attf-action="/quote/#{quotation.id}/#{quotation.access_token}/decline" class="modal-content">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&amp;times;</button>
<h4 class="modal-title">Reject This Quote</h4>
+8 -3
View File
@@ -66,6 +66,7 @@
<template id="products_item" name="Products item">
<form action="/shop/cart/update" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div itemscope="itemscope" itemtype="http://schema.org/Product">
<div class="ribbon-wrapper">
<div class="ribbon btn btn-danger">Sale</div>
@@ -366,7 +367,7 @@
<span itemprop="url" style="display:none;" t-esc="'/shop/product/%s' % slug(product)"/>
<form t-att-action="keep('/shop/cart/update')" class="js_add_cart_variants" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="js_product">
<t t-placeholder="select">
<input type="hidden" class="product_id" name="product_id" t-att-value="int(product.product_variant_ids[0]) if len(product.product_variant_ids) == 1 else '0'"/>
@@ -668,6 +669,7 @@
<h1 class="mb32">Extra Step</h1>
<div class="row">
<form class="form-horizontal" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group">
<label for="x_test" class="col-sm-2 control-label">Field 1</label>
<div class="col-sm-10">
@@ -898,7 +900,8 @@
}'/>
</td>
<td class="text-center">
<form action="/shop/cart/update" method="post">
<form action="/shop/cart/update" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input name="product_id" t-att-value="product.id" type="hidden"/>
<a class="btn btn-link a-submit"><strong>Add to Cart</strong></a>
</form>
@@ -925,6 +928,7 @@
<p class="bg-warning">This promo code is not available</p>
</t>
<form t-if="website_sale_order and website_sale_order.website_order_line" action="/shop/pricelist" method="post" class="mb32">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="input-group">
<input name="promo" class='form-control' type="text" placeholder="code..." t-att-value="website_sale_order.pricelist_id.code or ''"/>
<div class="input-group-btn">
@@ -947,7 +951,7 @@
</t>
<h1>Your Address</h1>
<form action="/shop/confirm_order" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="row">
<div class="col-md-8 oe_cart">
<h3 class="page-header mt16">Billing Information
@@ -1209,6 +1213,7 @@
<div class="js_payment mb64 row" t-if="not website_sale_order.amount_total" id="payment_method">
<div class="col-lg-8 col-sm-8">
<form target="_self" action="/shop/payment/validate" method="post" class="pull-right">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<a style="width:127px;" class="btn btn-primary a-submit">
<span t-if="order.amount_total > 0">Pay Now <span class="fa fa-long-arrow-right"></span></span>
<span t-if="order.amount_total == 0">Confirm Order <span class="fa fa-long-arrow-right"></span></span>
@@ -544,6 +544,7 @@
</div>
<div t-att-class="comments and 'tab-pane fade active in' or 'tab-pane fade'" id="discuss">
<form id="comment" class="js_website_submit_form" t-attf-action="/slides/slide/#{slide.id}/comment" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="media">
<span class="pull-left" t-if="not is_public_user">
<img class="img img-circle media-object"
+66 -6
View File
@@ -6,9 +6,9 @@ import ast
import collections
import contextlib
import datetime
import errno
import functools
import getpass
import hashlib
import hmac
import inspect
import logging
import mimetypes
@@ -17,7 +17,6 @@ import pprint
import random
import re
import sys
import tempfile
import threading
import time
import traceback
@@ -26,6 +25,7 @@ import warnings
from zlib import adler32
import babel.core
import passlib.utils
import psycopg2
import json
import werkzeug.contrib.sessions
@@ -43,11 +43,10 @@ except ImportError:
psutil = None
import openerp
from openerp import SUPERUSER_ID
from openerp.service.server import memory_info
from openerp.service import security, model as service_model
from openerp.tools.func import lazy_property
from openerp.tools import ustr
from openerp.tools import ustr, consteq
_logger = logging.getLogger(__name__)
rpc_request = logging.getLogger(__name__ + '.rpc.request')
@@ -372,6 +371,48 @@ class WebRequest(object):
"""
return self.session
def csrf_token(self, time_limit=3600):
""" Generates and returns a CSRF token for the current session
:param time_limit: the CSRF token should only be valid for the
specified duration (in second), by default 1h,
``None`` for the token to be valid as long as the
current user's session is.
:type time_limit: int | None
:returns: ASCII token string
"""
token = self.session.sid
max_ts = '' if not time_limit else int(time.time() + time_limit)
msg = '%s%s' % (token, max_ts)
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
assert secret, "CSRF protection requires a configured database secret"
hm = hmac.new(str(secret), msg, hashlib.sha1).hexdigest()
return '%so%s' % (hm, max_ts)
def validate_csrf(self, csrf):
if not csrf:
return False
try:
hm, _, max_ts = str(csrf).rpartition('o')
except UnicodeEncodeError:
return False
if max_ts:
try:
if int(max_ts) < int(time.time()):
return False
except ValueError:
return False
token = self.session.sid
msg = '%s%s' % (token, max_ts)
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
assert secret, "CSRF protection requires a configured database secret"
hm_expected = hmac.new(str(secret), msg, hashlib.sha1).hexdigest()
return consteq(hm, hm_expected)
def route(route=None, **kw):
"""
Decorator marking the decorated method as being a handler for
@@ -397,9 +438,21 @@ def route(route=None, **kw):
:param methods: A sequence of http methods this route applies to. If not
specified, all methods are allowed.
:param cors: The Access-Control-Allow-Origin cors directive value.
:param bool csrf: Whether CSRF protection should be enabled for the route.
Defaults to ``True``.
CSRF protection only applies to *UNSAFE* methods as
defined by :rfc:`7231`: GET, HEAD, TRACE and OPTIONS are
safe, all other methods are unsafe.
CSRF protection requires a csrf token to be sent as part
of the request's form data, it can be obtained via
:meth:`request.csrf_token()
<openerp.http.WebRequest.csrf_token>`
"""
routing = kw.copy()
assert not 'type' in routing or routing['type'] in ("http", "json")
assert 'type' not in routing or routing['type'] in ("http", "json")
def decorator(f):
if route:
if isinstance(route, list):
@@ -706,6 +759,12 @@ class HttpRequest(WebRequest):
}
return Response(status=200, headers=headers)
if request.httprequest.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE') \
and request.endpoint.routing.get('csrf', True): # csrf checked by default
token = self.params.pop('csrf_token', None)
if not self.validate_csrf(token):
raise werkzeug.exceptions.BadRequest('Invalid CSRF Token')
r = self._call_function(**self.params)
if not r:
r = Response(status=204) # no content
@@ -1295,6 +1354,7 @@ class Response(werkzeug.wrappers.Response):
"""
view_obj = request.registry["ir.ui.view"]
uid = self.uid or request.uid or openerp.SUPERUSER_ID
self.qcontext['request'] = request
return view_obj.render(
request.cr, uid, self.template, self.qcontext,
context=request.context)
+8
View File
@@ -12,6 +12,7 @@ from contextlib import contextmanager
import subprocess
import logging
import os
import passlib.utils
import socket
import sys
import threading
@@ -1157,3 +1158,10 @@ def formatLang(env, value, digits=None, grouping=True, monetary=False, dp=False,
elif currency_obj and currency_obj.position == 'before':
res = '%s %s' % (currency_obj.symbol, res)
return res
def _consteq(str1, str2):
""" Constant-time string comparison. Suitable to compare bytestrings of fixed,
known length only, because length difference is optimized. """
return len(str1) == len(str2) and sum(ord(x)^ord(y) for x, y in zip(str1, str2)) == 0
consteq = getattr(passlib.utils, 'consteq', _consteq)