mirror of
https://github.com/kakwa/ldapcherry
synced 2024-11-24 10:14:31 +01:00
Add CSRF protection to all forms
This commit is contained in:
parent
856157af79
commit
8be228f142
@ -20,6 +20,7 @@ from ldapcherry.exceptions import *
|
|||||||
from ldapcherry.lclogging import *
|
from ldapcherry.lclogging import *
|
||||||
from ldapcherry.roles import Roles
|
from ldapcherry.roles import Roles
|
||||||
from ldapcherry.attributes import Attributes
|
from ldapcherry.attributes import Attributes
|
||||||
|
from ldapcherry.csrf import validate_csrf
|
||||||
|
|
||||||
# Cherrypy http framework imports
|
# Cherrypy http framework imports
|
||||||
import cherrypy
|
import cherrypy
|
||||||
@ -39,7 +40,6 @@ else:
|
|||||||
|
|
||||||
SESSION_KEY = '_cp_username'
|
SESSION_KEY = '_cp_username'
|
||||||
|
|
||||||
|
|
||||||
class LdapCherry(object):
|
class LdapCherry(object):
|
||||||
|
|
||||||
def _handle_exception(self, e):
|
def _handle_exception(self, e):
|
||||||
@ -388,7 +388,8 @@ class LdapCherry(object):
|
|||||||
for t in ('index.tmpl', 'error.tmpl', 'login.tmpl', '404.tmpl',
|
for t in ('index.tmpl', 'error.tmpl', 'login.tmpl', '404.tmpl',
|
||||||
'searchadmin.tmpl', 'searchuser.tmpl', 'adduser.tmpl',
|
'searchadmin.tmpl', 'searchuser.tmpl', 'adduser.tmpl',
|
||||||
'roles.tmpl', 'groups.tmpl', 'form.tmpl', 'selfmodify.tmpl',
|
'roles.tmpl', 'groups.tmpl', 'form.tmpl', 'selfmodify.tmpl',
|
||||||
'modify.tmpl', 'service_unavailable.tmpl'
|
'modify.tmpl', 'service_unavailable.tmpl', 'csrf_error.tmpl',
|
||||||
|
'csrf_field.tmpl'
|
||||||
):
|
):
|
||||||
self.temp[t] = self.temp_lookup.get_template(t)
|
self.temp[t] = self.temp_lookup.get_template(t)
|
||||||
|
|
||||||
@ -882,15 +883,14 @@ class LdapCherry(object):
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@exception_decorator
|
@exception_decorator
|
||||||
def signin(self, url=None):
|
def signin(self, url=None):
|
||||||
"""simple signin page
|
"""simple signin page"""
|
||||||
"""
|
|
||||||
return self.temp['login.tmpl'].render(url=url)
|
return self.temp['login.tmpl'].render(url=url)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@exception_decorator
|
@exception_decorator
|
||||||
|
@validate_csrf
|
||||||
def login(self, login, password, url=None):
|
def login(self, login, password, url=None):
|
||||||
"""login page
|
"""login page"""
|
||||||
"""
|
|
||||||
auth = self._auth(login, password)
|
auth = self._auth(login, password)
|
||||||
cherrypy.session['isadmin'] = auth['isadmin']
|
cherrypy.session['isadmin'] = auth['isadmin']
|
||||||
cherrypy.session['connected'] = auth['connected']
|
cherrypy.session['connected'] = auth['connected']
|
||||||
@ -950,8 +950,7 @@ class LdapCherry(object):
|
|||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@exception_decorator
|
@exception_decorator
|
||||||
def index(self):
|
def index(self):
|
||||||
"""main page rendering
|
""" main page rendering """
|
||||||
"""
|
|
||||||
self._check_auth(must_admin=False)
|
self._check_auth(must_admin=False)
|
||||||
is_admin = self._check_admin()
|
is_admin = self._check_admin()
|
||||||
sess = cherrypy.session
|
sess = cherrypy.session
|
||||||
@ -1026,6 +1025,7 @@ class LdapCherry(object):
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@exception_decorator
|
@exception_decorator
|
||||||
|
@validate_csrf
|
||||||
def adduser(self, **params):
|
def adduser(self, **params):
|
||||||
""" add user page """
|
""" add user page """
|
||||||
self._check_auth(must_admin=True)
|
self._check_auth(must_admin=True)
|
||||||
@ -1074,6 +1074,7 @@ class LdapCherry(object):
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@exception_decorator
|
@exception_decorator
|
||||||
|
@validate_csrf
|
||||||
def delete(self, user):
|
def delete(self, user):
|
||||||
""" remove user page """
|
""" remove user page """
|
||||||
self._check_auth(must_admin=True)
|
self._check_auth(must_admin=True)
|
||||||
@ -1088,6 +1089,7 @@ class LdapCherry(object):
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@exception_decorator
|
@exception_decorator
|
||||||
|
@validate_csrf
|
||||||
def modify(self, user=None, **params):
|
def modify(self, user=None, **params):
|
||||||
""" modify user page """
|
""" modify user page """
|
||||||
self._check_auth(must_admin=True)
|
self._check_auth(must_admin=True)
|
||||||
@ -1181,6 +1183,7 @@ class LdapCherry(object):
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@exception_decorator
|
@exception_decorator
|
||||||
|
@validate_csrf
|
||||||
def selfmodify(self, **params):
|
def selfmodify(self, **params):
|
||||||
""" self modify user page """
|
""" self modify user page """
|
||||||
self._check_auth(must_admin=False)
|
self._check_auth(must_admin=False)
|
||||||
|
114
ldapcherry/csrf.py
Normal file
114
ldapcherry/csrf.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:set expandtab tabstop=4 shiftwidth=4:
|
||||||
|
#
|
||||||
|
# The MIT License (MIT)
|
||||||
|
# LdapCherry
|
||||||
|
# Copyright (c) 2014 Carpentier Pierre-Francois
|
||||||
|
|
||||||
|
"""
|
||||||
|
Utility functions to generate and verify CSRF tokens.
|
||||||
|
|
||||||
|
For details about CSRF attack and protection, see:
|
||||||
|
* https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)
|
||||||
|
* https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md
|
||||||
|
"""
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
import secrets
|
||||||
|
from ldapcherry.exceptions import MissingCSRFParam, \
|
||||||
|
MissingCSRFCookie, InvalidCSRFToken
|
||||||
|
|
||||||
|
|
||||||
|
# Names of the different parameters (cookie, post parameter, session variable)
|
||||||
|
CSRF_COOKIE_NAME = "csrf_reference"
|
||||||
|
CSRF_INPUT_NAME = "csrf_token"
|
||||||
|
CSRF_SESSION_NAME = "csrf_token"
|
||||||
|
|
||||||
|
|
||||||
|
def get_csrf_cookie():
|
||||||
|
"""
|
||||||
|
Quick utility function to read CSRF cookie value.
|
||||||
|
|
||||||
|
Return None if the cookie is not present.
|
||||||
|
"""
|
||||||
|
if CSRF_COOKIE_NAME in cherrypy.request.cookie:
|
||||||
|
return cherrypy.request.cookie[CSRF_COOKIE_NAME].value
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def set_csrf_cookie(value):
|
||||||
|
"""
|
||||||
|
Quick utility function to set CSRF cookie value.
|
||||||
|
|
||||||
|
Use cherrypy to set the correct header.
|
||||||
|
"""
|
||||||
|
cherrypy.response.cookie[CSRF_COOKIE_NAME] = value
|
||||||
|
|
||||||
|
|
||||||
|
def generate_token(nb_bytes=32):
|
||||||
|
"""
|
||||||
|
Generate and return a random CSRF token.
|
||||||
|
|
||||||
|
Strong entropy generator from `secrets` library is used to ensure
|
||||||
|
the token is not guessable and return value is encoded in hex format.
|
||||||
|
@integer nb_bytes can be specified to set the number of bytes
|
||||||
|
(each byte resulting in two hex digits)
|
||||||
|
"""
|
||||||
|
return secrets.token_hex(nb_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def get_csrf_token():
|
||||||
|
"""
|
||||||
|
Return the CSRF token associated with the user.
|
||||||
|
|
||||||
|
Return the CSRF token from session if it exists.
|
||||||
|
Else, generate a new one and store it (in session + cookie).
|
||||||
|
"""
|
||||||
|
if CSRF_SESSION_NAME not in cherrypy.session:
|
||||||
|
token = generate_token()
|
||||||
|
cherrypy.session[CSRF_SESSION_NAME] = token
|
||||||
|
set_csrf_cookie(token)
|
||||||
|
return cherrypy.session[CSRF_SESSION_NAME]
|
||||||
|
|
||||||
|
|
||||||
|
def get_csrf_field():
|
||||||
|
"""
|
||||||
|
Return an hidden form field containing the CSRF token.
|
||||||
|
|
||||||
|
Return format is a plain string which can be inserted in a template.
|
||||||
|
The token is generated and saved if needed (via get_csrf_token() call).
|
||||||
|
"""
|
||||||
|
template = "<input type=\"hidden\" name=\"{name}\" value=\"{value}\"/>"
|
||||||
|
return template.format(name=CSRF_INPUT_NAME, value=get_csrf_token())
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_valid_token(**params):
|
||||||
|
"""
|
||||||
|
Ensure request is legitimate by comparing cookie and post parameter.
|
||||||
|
|
||||||
|
Raise an exception if CSRF cookie value is different from CSRF
|
||||||
|
post parameter value or if one of them is missing.
|
||||||
|
In this case, the request MUST NOT be processed (it is not genuine).
|
||||||
|
"""
|
||||||
|
if CSRF_INPUT_NAME not in params:
|
||||||
|
raise MissingCSRFParam()
|
||||||
|
if CSRF_COOKIE_NAME not in cherrypy.request.cookie:
|
||||||
|
raise MissingCSRFCookie()
|
||||||
|
if params.get(CSRF_INPUT_NAME) != get_csrf_cookie():
|
||||||
|
raise InvalidCSRFToken()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_csrf(func):
|
||||||
|
"""
|
||||||
|
Decorator ensuring CSRF token is validated before executing request.
|
||||||
|
|
||||||
|
WARNING: only POST requests are checked, for GET requests, you need to call
|
||||||
|
ensure_valid_token() manually.
|
||||||
|
"""
|
||||||
|
def ret(self, *args, **kwargs):
|
||||||
|
if cherrypy.request.method.upper() == 'POST':
|
||||||
|
ensure_valid_token(**kwargs)
|
||||||
|
kwargs.pop(CSRF_INPUT_NAME)
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
return ret
|
@ -223,6 +223,21 @@ class TemplateRenderError(Exception):
|
|||||||
self.log = "Template Render Error: " + error
|
self.log = "Template Render Error: " + error
|
||||||
|
|
||||||
|
|
||||||
|
class MissingCSRFParam(Exception):
|
||||||
|
def __init__(self):
|
||||||
|
self.log = "Missing CSRF post parameter"
|
||||||
|
|
||||||
|
|
||||||
|
class MissingCSRFCookie(Exception):
|
||||||
|
def __init__(self):
|
||||||
|
self.log = "Missing CSRF cookie"
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidCSRFToken(Exception):
|
||||||
|
def __init__(self):
|
||||||
|
self.log = "CSRF validation failed"
|
||||||
|
|
||||||
|
|
||||||
def exception_decorator(func):
|
def exception_decorator(func):
|
||||||
def ret(self, *args, **kwargs):
|
def ret(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
@ -231,6 +246,10 @@ def exception_decorator(func):
|
|||||||
raise e
|
raise e
|
||||||
except cherrypy.HTTPError as e:
|
except cherrypy.HTTPError as e:
|
||||||
raise e
|
raise e
|
||||||
|
except (InvalidCSRFToken, MissingCSRFCookie, MissingCSRFParam) as e:
|
||||||
|
cherrypy.response.status = 403
|
||||||
|
self._handle_exception(e)
|
||||||
|
return self.temp['csrf_error.tmpl'].render()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
cherrypy.response.status = 500
|
cherrypy.response.status = 500
|
||||||
self._handle_exception(e)
|
self._handle_exception(e)
|
||||||
|
16
resources/templates/csrf_error.tmpl
Normal file
16
resources/templates/csrf_error.tmpl
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
<%inherit file="base.tmpl"/>
|
||||||
|
<%block name="core">
|
||||||
|
<div class="row clearfix" style="margin-top:30px">
|
||||||
|
<div class="col-md-4 column"></div>
|
||||||
|
<div class="col-md-4 column well">
|
||||||
|
<div class="alert alert-dismissable alert-danger">
|
||||||
|
<h4>Your request was denied for security reasons.</h4>
|
||||||
|
<p>This may happen from time to time if you don't actively use the tab or if you clear cookies.</p>
|
||||||
|
<p>Try to refresh the page and contact an administrator if the problem persists.</p>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-default blue" href='/'><span class="glyphicon glyphicon-home"></span> Return</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 column"></div>
|
||||||
|
</div>
|
||||||
|
</%block>
|
3
resources/templates/csrf_field.tmpl
Normal file
3
resources/templates/csrf_field.tmpl
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
<%! from ldapcherry.csrf import get_csrf_token, CSRF_INPUT_NAME %>
|
||||||
|
<input type="hidden" name="${CSRF_INPUT_NAME}" value="${get_csrf_token()}"/>
|
@ -1,5 +1,6 @@
|
|||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8 -*-
|
||||||
<%
|
<%
|
||||||
|
from ldapcherry.csrf import get_csrf_field
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
len_attr = len(attributes)
|
len_attr = len(attributes)
|
||||||
switch = len_attr / 2
|
switch = len_attr / 2
|
||||||
@ -84,6 +85,8 @@ ${form_col(lc1)}
|
|||||||
${form_col(lc2)}
|
${form_col(lc2)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<%include file="csrf_field.tmpl"/>
|
||||||
|
|
||||||
% if autofill:
|
% if autofill:
|
||||||
<%
|
<%
|
||||||
attr_set = []
|
attr_set = []
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
<%include file="csrf_field.tmpl"/>
|
||||||
<button class="btn btn-default blue" type="submit"><span class="glyphicon glyphicon-off"></span> Sign in</button>
|
<button class="btn btn-default blue" type="submit"><span class="glyphicon glyphicon-off"></span> Sign in</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8 -*-
|
||||||
<%inherit file="navbar.tmpl"/>
|
<%inherit file="navbar.tmpl"/>
|
||||||
<%block name="core">
|
<%block name="core">
|
||||||
|
<% from ldapcherry.csrf import get_csrf_token %>
|
||||||
<div class="row clearfix">
|
<div class="row clearfix">
|
||||||
<div class="col-md-12 column">
|
<div class="col-md-12 column">
|
||||||
<form method='get' action='/searchadmin' role="form" class="form-inline" data-toggle="validator">
|
<form method='get' action='/searchadmin' role="form" class="form-inline" data-toggle="validator">
|
||||||
@ -55,7 +56,12 @@
|
|||||||
<a href="/modify?user=${user | n,u}" class="btn btn-xs blue pad" ><span class="glyphicon glyphicon-cog"></span> Modify</a>
|
<a href="/modify?user=${user | n,u}" class="btn btn-xs blue pad" ><span class="glyphicon glyphicon-cog"></span> Modify</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/delete?user=${user | n,u}" data-toggle='confirmation-delete' class="btn btn-xs red pad"><span class="glyphicon glyphicon-remove-sign"></span> Delete</a>
|
<form action="/delete?user=${user | n,u}" method='POST'>
|
||||||
|
<%include file="csrf_field.tmpl"/>
|
||||||
|
<button class="btn btn-xs red pad" type="submit" data-toggle='confirmation-delete'>
|
||||||
|
<span class="glyphicon glyphicon-remove-sign"></span> Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
% endfor
|
% endfor
|
||||||
|
Loading…
Reference in New Issue
Block a user