diff --git a/CTFd/plugins/__init__.py b/CTFd/plugins/__init__.py index 6239cce..d76d304 100644 --- a/CTFd/plugins/__init__.py +++ b/CTFd/plugins/__init__.py @@ -125,6 +125,19 @@ def get_user_page_menu_bar(): return db_pages() + USER_PAGE_MENU_BAR +def bypass_csrf_protection(f): + """ + Decorator that allows a route to bypass the need for a CSRF nonce on POST requests. + + This should be considered beta and may change in future versions. + + :param f: A function that needs to bypass CSRF protection + :return: Returns a function with the _bypass_csrf attribute set which tells CTFd to not require CSRF protection. + """ + f._bypass_csrf = True + return f + + def init_plugins(app): """ Searches for the load function in modules in the CTFd/plugins folder. This function is called with the current CTFd diff --git a/CTFd/utils.py b/CTFd/utils.py index 39e50c8..8b7a2b6 100644 --- a/CTFd/utils.py +++ b/CTFd/utils.py @@ -190,6 +190,9 @@ def init_utils(app): @app.before_request def csrf(): + func = app.view_functions[request.endpoint] + if hasattr(func, '_bypass_csrf'): + return if not session.get('nonce'): session['nonce'] = sha512(os.urandom(10)) if request.method == "POST": diff --git a/tests/test_plugin_utils.py b/tests/test_plugin_utils.py index 5abc4f6..a81b177 100644 --- a/tests/test_plugin_utils.py +++ b/tests/test_plugin_utils.py @@ -12,7 +12,8 @@ from CTFd.plugins import ( register_admin_plugin_menu_bar, get_admin_plugin_menu_bar, register_user_page_menu_bar, - get_user_page_menu_bar + get_user_page_menu_bar, + bypass_csrf_protection ) from freezegun import freeze_time from mock import patch @@ -145,3 +146,29 @@ def test_register_user_page_menu_bar(): assert menu_item.title == 'test_user_menu_link' assert menu_item.route == '/test_user_href' destroy_ctfd(app) + + +def test_bypass_csrf_protection(): + """ + Test that the bypass_csrf_protection decorator functions properly + """ + app = create_ctfd() + + with app.app_context(): + with app.test_client() as client: + r = client.post('/login') + output = r.get_data(as_text=True) + assert r.status_code == 403 + + def bypass_csrf_protection_test_route(): + return "Success", 200 + + # Hijack an existing route to avoid any kind of hacks to create a test route + app.view_functions['auth.login'] = bypass_csrf_protection(bypass_csrf_protection_test_route) + + with app.test_client() as client: + r = client.post('/login') + output = r.get_data(as_text=True) + assert r.status_code == 200 + assert output == "Success" + destroy_ctfd(app)