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.roles import Roles
|
||||
from ldapcherry.attributes import Attributes
|
||||
from ldapcherry.csrf import validate_csrf
|
||||
|
||||
# Cherrypy http framework imports
|
||||
import cherrypy
|
||||
@ -39,7 +40,6 @@ else:
|
||||
|
||||
SESSION_KEY = '_cp_username'
|
||||
|
||||
|
||||
class LdapCherry(object):
|
||||
|
||||
def _handle_exception(self, e):
|
||||
@ -388,7 +388,8 @@ class LdapCherry(object):
|
||||
for t in ('index.tmpl', 'error.tmpl', 'login.tmpl', '404.tmpl',
|
||||
'searchadmin.tmpl', 'searchuser.tmpl', 'adduser.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)
|
||||
|
||||
@ -882,15 +883,14 @@ class LdapCherry(object):
|
||||
@cherrypy.expose
|
||||
@exception_decorator
|
||||
def signin(self, url=None):
|
||||
"""simple signin page
|
||||
"""
|
||||
"""simple signin page"""
|
||||
return self.temp['login.tmpl'].render(url=url)
|
||||
|
||||
@cherrypy.expose
|
||||
@exception_decorator
|
||||
@validate_csrf
|
||||
def login(self, login, password, url=None):
|
||||
"""login page
|
||||
"""
|
||||
"""login page"""
|
||||
auth = self._auth(login, password)
|
||||
cherrypy.session['isadmin'] = auth['isadmin']
|
||||
cherrypy.session['connected'] = auth['connected']
|
||||
@ -950,8 +950,7 @@ class LdapCherry(object):
|
||||
@cherrypy.expose
|
||||
@exception_decorator
|
||||
def index(self):
|
||||
"""main page rendering
|
||||
"""
|
||||
""" main page rendering """
|
||||
self._check_auth(must_admin=False)
|
||||
is_admin = self._check_admin()
|
||||
sess = cherrypy.session
|
||||
@ -1026,6 +1025,7 @@ class LdapCherry(object):
|
||||
|
||||
@cherrypy.expose
|
||||
@exception_decorator
|
||||
@validate_csrf
|
||||
def adduser(self, **params):
|
||||
""" add user page """
|
||||
self._check_auth(must_admin=True)
|
||||
@ -1074,6 +1074,7 @@ class LdapCherry(object):
|
||||
|
||||
@cherrypy.expose
|
||||
@exception_decorator
|
||||
@validate_csrf
|
||||
def delete(self, user):
|
||||
""" remove user page """
|
||||
self._check_auth(must_admin=True)
|
||||
@ -1088,6 +1089,7 @@ class LdapCherry(object):
|
||||
|
||||
@cherrypy.expose
|
||||
@exception_decorator
|
||||
@validate_csrf
|
||||
def modify(self, user=None, **params):
|
||||
""" modify user page """
|
||||
self._check_auth(must_admin=True)
|
||||
@ -1181,6 +1183,7 @@ class LdapCherry(object):
|
||||
|
||||
@cherrypy.expose
|
||||
@exception_decorator
|
||||
@validate_csrf
|
||||
def selfmodify(self, **params):
|
||||
""" self modify user page """
|
||||
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
|
||||
|
||||
|
||||
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 ret(self, *args, **kwargs):
|
||||
try:
|
||||
@ -231,6 +246,10 @@ def exception_decorator(func):
|
||||
raise e
|
||||
except cherrypy.HTTPError as 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:
|
||||
cherrypy.response.status = 500
|
||||
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 -*-
|
||||
<%
|
||||
from ldapcherry.csrf import get_csrf_field
|
||||
from markupsafe import Markup
|
||||
len_attr = len(attributes)
|
||||
switch = len_attr / 2
|
||||
@ -84,6 +85,8 @@ ${form_col(lc1)}
|
||||
${form_col(lc2)}
|
||||
</div>
|
||||
</div>
|
||||
<%include file="csrf_field.tmpl"/>
|
||||
|
||||
% if autofill:
|
||||
<%
|
||||
attr_set = []
|
||||
|
@ -11,24 +11,25 @@
|
||||
action='login'
|
||||
% endif
|
||||
>
|
||||
<div class="form-group">
|
||||
<h2 class="form-signin-heading">Please sign in</h2>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon glyphicon glyphicon-user"></span>
|
||||
<input type="text" class="form-control" name="login" placeholder="Login" required autofocus>
|
||||
<div class="form-group">
|
||||
<h2 class="form-signin-heading">Please sign in</h2>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon glyphicon glyphicon-user"></span>
|
||||
<input type="text" class="form-control" name="login" placeholder="Login" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon glyphicon glyphicon-lock"></span>
|
||||
<input type="password" class="form-control" name="password" placeholder="Password" required>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon glyphicon glyphicon-lock"></span>
|
||||
<input type="password" class="form-control" name="password" placeholder="Password" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<button class="btn btn-default blue" type="submit"><span class="glyphicon glyphicon-off"></span> Sign in</button>
|
||||
<div class="form-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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-4 column"></div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="navbar.tmpl"/>
|
||||
<%block name="core">
|
||||
<% from ldapcherry.csrf import get_csrf_token %>
|
||||
<div class="row clearfix">
|
||||
<div class="col-md-12 column">
|
||||
<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>
|
||||
</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>
|
||||
</tr>
|
||||
% endfor
|
||||
|
Loading…
Reference in New Issue
Block a user