This commit is contained in:
Quentin Legrand 2024-03-13 00:08:45 +01:00 committed by GitHub
commit 179c3f8c62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 194 additions and 30 deletions

View File

@ -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
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
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)
@ -255,7 +274,6 @@ def exception_decorator(func):
message="User '" + user + "' already exist"
)
elif et is GroupDoesntExist:
group = e.group
return self.temp['error.tmpl'].render(
is_admin=is_admin,
alert='danger',

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 -*-
<%
<%
from ldapcherry.csrf import get_csrf_field
from markupsafe import Markup
len_attr = len(attributes)
switch = len_attr / 2
@ -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:
@ -84,6 +85,8 @@ ${form_col(lc1)}
${form_col(lc2)}
</div>
</div>
<%include file="csrf_field.tmpl"/>
% if autofill:
<%
attr_set = []

View File

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

View File

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