1
0
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:
Quentin Legrand 2019-06-12 17:10:16 +02:00
parent 856157af79
commit 8be228f142
8 changed files with 194 additions and 29 deletions

View File

@ -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
View 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

View File

@ -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)

View 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>

View 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()}"/>

View File

@ -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
@ -16,11 +17,11 @@ for a in sorted(attributes.keys(), key=lambda attr: attributes[attr]['weight']):
counter = counter + 1 counter = counter + 1
%> %>
<%def name="form_col(l)"> <%def name="form_col(l)">
% for a in l: % for a in l:
<% attr = attributes[a] %> <% attr = attributes[a] %>
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<% <%
if modify: if modify:
required = '' required = ''
else: else:
@ -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 = []

View File

@ -11,24 +11,25 @@
action='login' action='login'
% endif % endif
> >
<div class="form-group"> <div class="form-group">
<h2 class="form-signin-heading">Please sign in</h2> <h2 class="form-signin-heading">Please sign in</h2>
<div class="input-group"> <div class="input-group">
<span class="input-group-addon glyphicon glyphicon-user"></span> <span class="input-group-addon glyphicon glyphicon-user"></span>
<input type="text" class="form-control" name="login" placeholder="Login" required autofocus> <input type="text" class="form-control" name="login" placeholder="Login" required autofocus>
</div>
</div> </div>
</div> <div class="form-group">
<div class="form-group"> <div class="input-group">
<div class="input-group"> <span class="input-group-addon glyphicon glyphicon-lock"></span>
<span class="input-group-addon glyphicon glyphicon-lock"></span> <input type="password" class="form-control" name="password" placeholder="Password" required>
<input type="password" class="form-control" name="password" placeholder="Password" required> </div>
</div> </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>
</form> </form>
</div> </div>
<div class="col-md-4 column"></div> <div class="col-md-4 column"></div>

View File

@ -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">
@ -42,8 +43,8 @@
%for attr in sorted(attrs_list.keys(), key=lambda attr: attrs_list[attr]['weight']): %for attr in sorted(attrs_list.keys(), key=lambda attr: attrs_list[attr]['weight']):
<td> <td>
% if attr in searchresult[user]: % if attr in searchresult[user]:
<% <%
value = searchresult[user][attr] value = searchresult[user][attr]
if type(value) is list: if type(value) is list:
value = ', '.join(value) value = ', '.join(value)
%> %>
@ -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