Skip to content

Commit 25698a7

Browse files
authored
Sandbox themes (CTFd#534)
* Use the Jinja sandbox to render themes * Prevent get_config from accessing config.py values * Add get_app_config * Add tests for sandbox
1 parent 4772c63 commit 25698a7

File tree

3 files changed

+61
-17
lines changed

3 files changed

+61
-17
lines changed

CTFd/__init__.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
from distutils.version import StrictVersion
55
from flask import Flask
6-
from jinja2 import FileSystemLoader
6+
from jinja2 import FileSystemLoader, select_autoescape
7+
from jinja2.sandbox import SandboxedEnvironment
78
from sqlalchemy.engine.url import make_url
89
from sqlalchemy.exc import OperationalError, ProgrammingError
910
from sqlalchemy_utils import database_exists, create_database
@@ -63,7 +64,12 @@ def create_app(config='CTFd.config.Config'):
6364
app = Flask(__name__)
6465
with app.app_context():
6566
app.config.from_object(config)
66-
app.jinja_loader = ThemeLoader(os.path.join(app.root_path, 'themes'), followlinks=True)
67+
theme_loader = ThemeLoader(os.path.join(app.root_path, 'themes'), followlinks=True)
68+
app.jinja_env = SandboxedEnvironment(
69+
loader=theme_loader,
70+
autoescape=select_autoescape(['html', 'xml'])
71+
)
72+
app.jinja_loader = theme_loader
6773

6874
from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking
6975

CTFd/utils.py

+10-15
Original file line numberDiff line numberDiff line change
@@ -516,18 +516,13 @@ def delete_file(file_id):
516516

517517

518518
@cache.memoize()
519-
def get_config(key):
519+
def get_app_config(key):
520520
value = app.config.get(key)
521-
if value:
522-
if value and value.isdigit():
523-
return int(value)
524-
elif value and isinstance(value, six.string_types):
525-
if value.lower() == 'true':
526-
return True
527-
elif value.lower() == 'false':
528-
return False
529-
else:
530-
return value
521+
return value
522+
523+
524+
@cache.memoize()
525+
def get_config(key):
531526
config = Config.query.filter_by(key=key).first()
532527
if config and config.value:
533528
value = config.value
@@ -774,7 +769,7 @@ def update_check(force=False):
774769

775770

776771
def export_ctf(segments=None):
777-
db = dataset.connect(get_config('SQLALCHEMY_DATABASE_URI'))
772+
db = dataset.connect(get_app_config('SQLALCHEMY_DATABASE_URI'))
778773
if segments is None:
779774
segments = ['challenges', 'teams', 'both', 'metadata']
780775

@@ -826,7 +821,7 @@ def export_ctf(segments=None):
826821
backup_zip.writestr('db/alembic_version.json', result_file.read())
827822

828823
# Backup uploads
829-
upload_folder = os.path.join(os.path.normpath(app.root_path), get_config('UPLOAD_FOLDER'))
824+
upload_folder = os.path.join(os.path.normpath(app.root_path), app.config.get('UPLOAD_FOLDER'))
830825
for root, dirs, files in os.walk(upload_folder):
831826
for file in files:
832827
parent_dir = os.path.basename(root)
@@ -838,7 +833,7 @@ def export_ctf(segments=None):
838833

839834

840835
def import_ctf(backup, segments=None, erase=False):
841-
side_db = dataset.connect(get_config('SQLALCHEMY_DATABASE_URI'))
836+
side_db = dataset.connect(get_app_config('SQLALCHEMY_DATABASE_URI'))
842837
if segments is None:
843838
segments = ['challenges', 'teams', 'both', 'metadata']
844839

@@ -924,7 +919,7 @@ def import_ctf(backup, segments=None, erase=False):
924919
entry_id = entry.pop('id', None)
925920
# This is a hack to get SQlite to properly accept datetime values from dataset
926921
# See Issue #246
927-
if get_config('SQLALCHEMY_DATABASE_URI').startswith('sqlite'):
922+
if get_app_config('SQLALCHEMY_DATABASE_URI').startswith('sqlite'):
928923
for k, v in entry.items():
929924
if isinstance(v, six.string_types):
930925
match = re.match(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d", v)

tests/test_themes.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
from tests.helpers import *
5+
from jinja2.sandbox import SecurityError
6+
7+
8+
def test_themes_run_in_sandbox():
9+
"""Does get_config and set_config work properly"""
10+
app = create_ctfd()
11+
with app.app_context():
12+
try:
13+
app.jinja_env.from_string("{{ ().__class__.__bases__[0].__subclasses__()[40]('./test_utils.py').read() }}").render()
14+
except SecurityError:
15+
pass
16+
except Exception as e:
17+
raise e
18+
destroy_ctfd(app)
19+
20+
21+
def test_themes_cant_access_configpy_attributes():
22+
"""Themes should not be able to access config.py attributes"""
23+
app = create_ctfd()
24+
with app.app_context():
25+
assert app.config['SECRET_KEY'] == 'AAAAAAAAAAAAAAAAAAAA'
26+
assert app.jinja_env.from_string("{{ get_config('SECRET_KEY') }}").render() != app.config['SECRET_KEY']
27+
destroy_ctfd(app)
28+
29+
30+
def test_themes_escape_html():
31+
"""Themes should escape XSS properly"""
32+
app = create_ctfd()
33+
with app.app_context():
34+
team = gen_team(app.db, name="<script>alert(1)</script>")
35+
team.affiliation = "<script>alert(1)</script>"
36+
team.website = "<script>alert(1)</script>"
37+
team.country = "<script>alert(1)</script>"
38+
39+
with app.test_client() as client:
40+
r = client.get('/teams')
41+
assert r.status_code == 200
42+
assert "<script>alert(1)</script>" not in r.get_data(as_text=True)
43+
destroy_ctfd(app)

0 commit comments

Comments
 (0)