Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions ldapcherry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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
Expand All @@ -39,7 +40,6 @@

SESSION_KEY = '_cp_username'


class LdapCherry(object):

def _handle_exception(self, e):
Expand Down Expand Up @@ -388,7 +388,8 @@ def _load_templates(self, config):
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)

Expand Down Expand Up @@ -882,15 +883,14 @@ def _checkppolicy(self, password):
@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']
Expand Down Expand Up @@ -950,8 +950,7 @@ def logout(self):
@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
Expand Down Expand Up @@ -1026,6 +1025,7 @@ def searchadmin(self, searchstring=None):

@cherrypy.expose
@exception_decorator
@validate_csrf
def adduser(self, **params):
""" add user page """
self._check_auth(must_admin=True)
Expand Down Expand Up @@ -1074,6 +1074,7 @@ def adduser(self, **params):

@cherrypy.expose
@exception_decorator
@validate_csrf
def delete(self, user):
""" remove user page """
self._check_auth(must_admin=True)
Expand All @@ -1088,6 +1089,7 @@ def delete(self, user):

@cherrypy.expose
@exception_decorator
@validate_csrf
def modify(self, user=None, **params):
""" modify user page """
self._check_auth(must_admin=True)
Expand Down Expand Up @@ -1181,6 +1183,7 @@ def default(self, attr='', *args, **params):

@cherrypy.expose
@exception_decorator
@validate_csrf
def selfmodify(self, **params):
""" self modify user page """
self._check_auth(must_admin=False)
Expand Down
114 changes: 114 additions & 0 deletions ldapcherry/csrf.py
Original file line number Diff line number Diff line change
@@ -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
20 changes: 19 additions & 1 deletion ldapcherry/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,21 @@ def __init__(self, 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 ret(self, *args, **kwargs):
try:
Expand All @@ -231,6 +246,10 @@ def ret(self, *args, **kwargs):
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)
Expand All @@ -255,7 +274,6 @@ def ret(self, *args, **kwargs):
message="User '" + user + "' already exist"
)
elif et is GroupDoesntExist:
group = e.group
return self.temp['error.tmpl'].render(
is_admin=is_admin,
alert='danger',
Expand Down
16 changes: 16 additions & 0 deletions resources/templates/csrf_error.tmpl
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions resources/templates/csrf_field.tmpl
Original file line number Diff line number Diff line change
@@ -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()}"/>
9 changes: 6 additions & 3 deletions resources/templates/form.tmpl
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,11 +17,11 @@ for a in sorted(attributes.keys(), key=lambda attr: attributes[attr]['weight']):
counter = counter + 1
%>
<%def name="form_col(l)">
% for a in l:
% for a in l:
<% attr = attributes[a] %>
<div class="form-group">
<div class="input-group">
<%
<%
if modify:
required = ''
else:
Expand Down Expand Up @@ -84,6 +85,8 @@ ${form_col(lc1)}
${form_col(lc2)}
</div>
</div>
<%include file="csrf_field.tmpl"/>

% if autofill:
<%
attr_set = []
Expand Down
31 changes: 16 additions & 15 deletions resources/templates/login.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
12 changes: 9 additions & 3 deletions resources/templates/searchadmin.tmpl
Original file line number Diff line number Diff line change
@@ -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">
Expand Down Expand Up @@ -42,8 +43,8 @@
%for attr in sorted(attrs_list.keys(), key=lambda attr: attrs_list[attr]['weight']):
<td>
% if attr in searchresult[user]:
<%
value = searchresult[user][attr]
<%
value = searchresult[user][attr]
if type(value) is list:
value = ', '.join(value)
%>
Expand All @@ -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
Expand Down