Get basic implementation of HTML sanitization working for pages (#1462)

* Closes #1332
* Pages by default now strip script tags and other potential XSS vectors
* lxml and html5lib are now pinned dependencies
* Challenge plugins rewritten to allow for better re-useability of template content and allow more control from the theme side
This commit is contained in:
Kevin Chung
2020-06-12 01:10:27 -04:00
committed by GitHub
parent bd5e6d4552
commit a30437c1fa
27 changed files with 595 additions and 763 deletions

View File

@@ -4,6 +4,7 @@ import sys
import weakref
from distutils.version import StrictVersion
import jinja2
from flask import Flask, Request
from flask_migrate import upgrade
from jinja2 import FileSystemLoader
@@ -147,10 +148,19 @@ def create_app(config="CTFd.config.Config"):
with app.app_context():
app.config.from_object(config)
theme_loader = ThemeLoader(
app.theme_loader = ThemeLoader(
os.path.join(app.root_path, "themes"), followlinks=True
)
app.jinja_loader = theme_loader
# Weird nested solution for accessing plugin templates
app.plugin_loader = jinja2.PrefixLoader(
{
"plugins": jinja2.FileSystemLoader(
searchpath=os.path.join(app.root_path, "plugins"), followlinks=True
)
}
)
# Load from themes first but fallback to loading from the plugin folder
app.jinja_loader = jinja2.ChoiceLoader([app.theme_loader, app.plugin_loader])
from CTFd.models import ( # noqa: F401
db,

View File

@@ -1,12 +1,8 @@
import os
from flask import current_app as app
from flask import render_template, render_template_string, request, url_for
from flask import render_template, request, url_for
from CTFd.admin import admin
from CTFd.models import Challenges, Flags, Solves
from CTFd.plugins.challenges import get_chal_class
from CTFd.utils import binary_type
from CTFd.utils.decorators import admins_only
@@ -50,14 +46,9 @@ def challenges_detail(challenge_id):
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
challenge_class = get_chal_class(challenge.type)
with open(
os.path.join(app.root_path, challenge_class.templates["update"].lstrip("/")),
"rb",
) as update:
tpl = update.read()
if isinstance(tpl, binary_type):
tpl = tpl.decode("utf-8")
update_j2 = render_template_string(tpl, challenge=challenge)
update_j2 = render_template(
challenge_class.templates["update"].lstrip("/"), challenge=challenge
)
update_script = url_for(
"views.static_html", route=challenge_class.scripts["update"].lstrip("/")

View File

@@ -1,6 +1,6 @@
import datetime
from flask import abort, request, url_for
from flask import abort, render_template, request, url_for
from flask_restx import Namespace, Resource
from sqlalchemy.sql import and_
@@ -144,6 +144,9 @@ class ChallengeTypes(Resource):
"name": challenge_class.name,
"templates": challenge_class.templates,
"scripts": challenge_class.scripts,
"create": render_template(
challenge_class.templates["create"].lstrip("/")
),
}
return {"success": True, "data": response}
@@ -274,6 +277,15 @@ class Challenge(Resource):
response["tags"] = tags
response["hints"] = hints
response["view"] = render_template(
chal_class.templates["view"].lstrip("/"),
solves=solves,
files=files,
tags=tags,
hints=[Hints(**h) for h in hints],
challenge=chal,
)
db.session.close()
return {"success": True, "data": response}

View File

@@ -79,6 +79,13 @@ class Challenges(db.Model):
__mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
@property
def html(self):
from CTFd.utils.config.pages import build_html
from CTFd.utils.helpers import markup
return markup(build_html(self.description))
def __init__(self, *args, **kwargs):
super(Challenges, self).__init__(**kwargs)

View File

@@ -1,64 +1 @@
<form method="POST" action="{{ script_root }}/admin/challenges/new" enctype="multipart/form-data">
<div class="form-group">
<label>
Name:<br>
<small class="form-text text-muted">
The name of your challenge
</small>
</label>
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
</div>
<div class="form-group">
<label>
Category:<br>
<small class="form-text text-muted">
The category of your challenge
</small>
</label>
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
</div>
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab"
data-toggle="tab" tabindex="-1">Write</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab" tabindex="-1">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
<div class="form-group">
<label>
Message:<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label>
Value:<br>
<small class="form-text text-muted">
This is how many points are rewarded for solving this challenge.
</small>
</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div>
<input type="hidden" name="state" value="hidden">
<input type="hidden" name="type" value="standard">
<div class="form-group">
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
</div>
</form>
{% extends "admin/challenges/create.html" %}

View File

@@ -9,31 +9,4 @@ CTFd.plugin.run((_CTFd) => {
);
}
});
// $('#desc-edit').on('shown.bs.tab', function (event) {
// if (event.target.hash == '#desc-preview') {
// var editor_value = $('#desc-editor').val();
// $(event.target.hash).html(
// window.challenge.render(editor_value)
// );
// }
// });
// $('#new-desc-edit').on('shown.bs.tab', function (event) {
// if (event.target.hash == '#new-desc-preview') {
// var editor_value = $('#new-desc-editor').val();
// $(event.target.hash).html(
// window.challenge.render(editor_value)
// );
// }
// });
// $("#solve-attempts-checkbox").change(function () {
// if (this.checked) {
// $('#solve-attempts-input').show();
// } else {
// $('#solve-attempts-input').hide();
// $('#max_attempts').val('');
// }
// });
// $(document).ready(function () {
// $('[data-toggle="tooltip"]').tooltip();
// });
})

View File

@@ -1,64 +1 @@
<form method="POST">
<div class="form-group">
<label>
Name<br>
<small class="form-text text-muted">Challenge Name</small>
</label>
<input type="text" class="form-control chal-name" name="name" value="{{ challenge.name }}">
</div>
<div class="form-group">
<label>
Category<br>
<small class="form-text text-muted">Challenge Category</small>
</label>
<input type="text" class="form-control chal-category" name="category" value="{{ challenge.category }}">
</div>
<div class="form-group">
<label>
Message<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ challenge.description }}</textarea>
</div>
<div class="form-group">
<label for="value">
Value<br>
<small class="form-text text-muted">
This is how many points teams will receive once they solve this challenge.
</small>
</label>
<input type="number" class="form-control chal-value" name="value" value="{{ challenge.value }}" required>
</div>
<div class="form-group">
<label>
Max Attempts<br>
<small class="form-text text-muted">Maximum amount of attempts users receive. Leave at 0 for unlimited.</small>
</label>
<input type="number" class="form-control chal-attempts" name="max_attempts" value="{{ challenge.max_attempts }}">
</div>
<div class="form-group">
<label>
State<br>
<small class="form-text text-muted">Changes the state of the challenge (e.g. visible, hidden)</small>
</label>
<select class="form-control custom-select" name="state">
<option value="visible" {% if challenge.state == "visible" %}selected{% endif %}>Visible</option>
<option value="hidden" {% if challenge.state == "hidden" %}selected{% endif %}>Hidden</option>
</select>
</div>
<div>
<button class="btn btn-success btn-outlined float-right" type="submit">
Update
</button>
</div>
</form>
{% extends "admin/challenges/update.html" %}

View File

@@ -1,117 +1,16 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="#challenge">Challenge</a>
</li>
{% if solves == None %}
{% else %}
<li class="nav-item">
<a class="nav-link challenge-solves" href="#solves">
{{ solves }} {% if solves > 1 %}Solves{% else %}Solves{% endif %}
</a>
</li>
{% endif %}
</ul>
<div role="tabpanel">
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
<h2 class='challenge-name text-center pt-3'>{{ name }}</h2>
<h3 class="challenge-value text-center">{{ value }}</h3>
<div class="challenge-tags text-center">
{% for tag in tags %}
<span class='badge badge-info challenge-tag'>{{ tag }}</span>
{% endfor %}
</div>
<span class="challenge-desc">{{ description | safe }}</span>
<div class="challenge-hints hint-row row">
{% for hint in hints %}
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
<a class="btn btn-info btn-hint btn-block load-hint" href="javascript:;" data-hint-id="{{ hint.id }}">
{% if hint.content %}
<small>
View Hint
</small>
{% else %}
{% if hint.cost %}
<small>
Unlock Hint for {{ hint.cost }} points
</small>
{% else %}
<small>
View Hint
</small>
{% endif %}
{% endif %}
</a>
</div>
{% endfor %}
</div>
<div class="row challenge-files text-center pb-3">
{% for file in files %}
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate'
href='{{ file }}'>
<i class="fas fa-download"></i>
<small>
{% set segments = file.split('/') %}
{% set file = segments | last %}
{% set token = file.split('?') | last %}
{% if token %}
{{ file | replace("?" + token, "") }}
{% else %}
{{ file }}
{% endif %}
</small>
</a>
</div>
{% endfor %}
</div>
{% extends "challenge.html" %}
<div class="row submit-row">
<div class="col-md-9 form-group">
<input class="form-control" type="text" name="answer" id="submission-input" placeholder="Flag"/>
<input id="challenge-id" type="hidden" value="{{ id }}">
</div>
<div class="col-md-3 form-group key-submit">
<button type="submit" id="submit-key" tabindex="0"
class="btn btn-md btn-outline-secondary float-right">Submit
</button>
</div>
</div>
<div class="row notification-row">
<div class="col-md-12">
<div id="result-notification" class="alert alert-dismissable text-center w-100"
role="alert" style="display: none;">
<strong id="result-message"></strong>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" id="solves">
<div class="row">
<div class="col-md-12">
<table class="table table-striped text-center">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="challenge-solves-names">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% block description %}
{{ challenge.html }}
{% endblock %}
{% block input %}
<input id="challenge-id" class="challenge-id" type="hidden" value="{{ challenge.id }}">
<input id="challenge-input" class="challenge-input" type="text" name="answer" placeholder="Flag"/>
{% endblock %}
{% block submit %}
<button id="challenge-submit" class="challenge-submit" type="submit">
Submit
</button>
{% endblock %}

View File

@@ -15,7 +15,7 @@ CTFd._internal.challenge.postRender = function () { }
CTFd._internal.challenge.submit = function (preview) {
var challenge_id = parseInt(CTFd.lib.$('#challenge-id').val())
var submission = CTFd.lib.$('#submission-input').val()
var submission = CTFd.lib.$('#challenge-input').val()
var body = {
'challenge_id': challenge_id,

View File

@@ -1,88 +1,43 @@
<form method="POST" action="{{ script_root }}/admin/chal/new" enctype="multipart/form-data">
<div class="form-group">
<div class="alert alert-secondary" role="alert">
Dynamic value challenges decrease in value as they receive solves. The more solves a dynamic challenge has,
the
lower its value is to everyone who has solved it.
</div>
</div>
{% extends "admin/challenges/create.html" %}
<div class="form-group">
<label for="name">Name<br>
<small class="form-text text-muted">
The name of your challenge
</small>
</label>
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
</div>
<div class="form-group">
<label for="category">Category<br>
<small class="form-text text-muted">
The category of your challenge
</small>
</label>
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
</div>
{% block header %}
<div class="alert alert-secondary" role="alert">
Dynamic value challenges decrease in value as they receive solves. The more solves a dynamic challenge has,
the lower its value is to everyone who has solved it.
</div>
{% endblock %}
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab"
data-toggle="tab">Write</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge. The description supports HTML and
Markdown.
</small>
</label>
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
</div>
</div>
{% block value %}
<div class="form-group">
<label for="value">Initial Value<br>
<small class="form-text text-muted">
This is how many points the challenge is worth initially.
</small>
</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
<div class="form-group">
<label for="value">Initial Value<br>
<small class="form-text text-muted">
This is how many points the challenge is worth initially.
</small>
</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div>
</div>
<div class="form-group">
<label for="value">Decay Limit<br>
<small class="form-text text-muted">
The amount of solves before the challenge reaches its minimum value
</small>
</label>
<input type="number" class="form-control" name="decay" placeholder="Enter decay limit" required>
</div>
<div class="form-group">
<label for="value">Decay Limit<br>
<small class="form-text text-muted">
The amount of solves before the challenge reaches its minimum value
</small>
</label>
<input type="number" class="form-control" name="decay" placeholder="Enter decay limit" required>
</div>
<div class="form-group">
<label for="value">Minimum Value<br>
<small class="form-text text-muted">
This is the lowest that the challenge can be worth
</small>
</label>
<input type="number" class="form-control" name="minimum" placeholder="Enter minimum value" required>
</div>
{% endblock %}
<div class="form-group">
<label for="value">Minimum Value<br>
<small class="form-text text-muted">
This is the lowest that the challenge can be worth
</small>
</label>
<input type="number" class="form-control" name="minimum" placeholder="Enter minimum value" required>
</div>
<input type="hidden" name="state" value="hidden">
<input type="hidden" value="dynamic" name="type" id="chaltype">
<div class="form-group">
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
</div>
</form>
{% block type %}
<input type="hidden" value="dynamic" name="type" id="chaltype">
{% endblock %}

View File

@@ -1,29 +1,12 @@
// Markdown Preview
$('#desc-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#desc-preview'){
var editor_value = $('#desc-editor').val();
$(event.target.hash).html(
window.challenge.render(editor_value)
);
}
});
$('#new-desc-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#new-desc-preview'){
var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html(
window.challenge.render(editor_value)
);
}
});
$("#solve-attempts-checkbox").change(function() {
if(this.checked) {
$('#solve-attempts-input').show();
} else {
$('#solve-attempts-input').hide();
$('#max_attempts').val('');
}
});
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});
CTFd.plugin.run((_CTFd) => {
const $ = _CTFd.lib.$
const md = _CTFd.lib.markdown()
$('a[href="#new-desc-preview"]').on('shown.bs.tab', function (event) {
if (event.target.hash == '#new-desc-preview') {
var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html(
md.render(editor_value)
);
}
});
})

View File

@@ -1,91 +1,39 @@
<form method="POST">
<div class="form-group">
<label for="name">Name<br>
<small class="form-text text-muted">
The name of your challenge
</small>
</label>
<input type="text" class="form-control chal-name" name="name" value="{{ challenge.name }}">
</div>
<div class="form-group">
<label for="category">Category<br>
<small class="form-text text-muted">
The category of your challenge
</small>
</label>
<input type="text" class="form-control chal-category" name="category" value="{{ challenge.category }}">
</div>
{% extends "admin/challenges/update.html" %}
<div class="form-group">
<label for="message-text" class="control-label">Message<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ challenge.description }}</textarea>
</div>
{% block value %}
<div class="form-group">
<label for="value">Current Value<br>
<small class="form-text text-muted">
This is how many points the challenge is worth right now.
</small>
</label>
<input type="number" class="form-control chal-value" name="value" value="{{ challenge.value }}" disabled>
</div>
<div class="form-group">
<label for="value">Current Value<br>
<small class="form-text text-muted">
This is how many points the challenge is worth right now.
</small>
</label>
<input type="number" class="form-control chal-value" name="value" value="{{ challenge.value }}" disabled>
</div>
<div class="form-group">
<label for="value">Initial Value<br>
<small class="form-text text-muted">
This is how many points the challenge was worth initially.
</small>
</label>
<input type="number" class="form-control chal-initial" name="initial" value="{{ challenge.initial }}" required>
</div>
<div class="form-group">
<label for="value">Initial Value<br>
<small class="form-text text-muted">
This is how many points the challenge was worth initially.
</small>
</label>
<input type="number" class="form-control chal-initial" name="initial" value="{{ challenge.initial }}" required>
</div>
<div class="form-group">
<label for="value">Decay Limit<br>
<small class="form-text text-muted">
The amount of solves before the challenge reaches its minimum value
</small>
</label>
<input type="number" class="form-control chal-decay" name="decay" value="{{ challenge.decay }}" required>
</div>
<div class="form-group">
<label for="value">Decay Limit<br>
<small class="form-text text-muted">
The amount of solves before the challenge reaches its minimum value
</small>
</label>
<input type="number" class="form-control chal-decay" name="decay" value="{{ challenge.decay }}" required>
</div>
<div class="form-group">
<label for="value">Minimum Value<br>
<small class="form-text text-muted">
This is the lowest that the challenge can be worth
</small>
</label>
<input type="number" class="form-control chal-minimum" name="minimum" value="{{ challenge.minimum }}" required>
</div>
<div class="form-group">
<label>
Max Attempts<br>
<small class="form-text text-muted">Maximum amount of attempts users receive. Leave at 0 for unlimited.</small>
</label>
<input type="number" class="form-control chal-attempts" name="max_attempts"
value="{{ challenge.max_attempts }}">
</div>
<div class="form-group">
<label>
State<br>
<small class="form-text text-muted">Changes the state of the challenge (e.g. visible, hidden)</small>
</label>
<select class="form-control custom-select" name="state">
<option value="visible" {% if challenge.state == "visible" %}selected{% endif %}>Visible</option>
<option value="hidden" {% if challenge.state == "hidden" %}selected{% endif %}>Hidden</option>
</select>
</div>
<div>
<button class="btn btn-success btn-outlined float-right" type="submit">
Update
</button>
</div>
</form>
<div class="form-group">
<label for="value">Minimum Value<br>
<small class="form-text text-muted">
This is the lowest that the challenge can be worth
</small>
</label>
<input type="number" class="form-control chal-minimum" name="minimum" value="{{ challenge.minimum }}" required>
</div>
{% endblock %}

View File

@@ -1,118 +1,16 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="#challenge">Challenge</a>
</li>
{% if solves == None %}
{% else %}
<li class="nav-item">
<a class="nav-link challenge-solves" href="#solves">
{{ solves }} {% if solves > 1 %}Solves{% else %}Solves{% endif %}
</a>
</li>
{% endif %}
</ul>
<div role="tabpanel">
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
<h2 class='challenge-name text-center pt-3'>{{ name }}</h2>
<h3 class="challenge-value text-center">{{ value }}</h3>
<div class="challenge-tags text-center">
{% for tag in tags %}
<span class='badge badge-info challenge-tag'>{{ tag }}</span>
{% endfor %}
</div>
<span class="challenge-desc">{{ description | safe }}</span>
<div class="challenge-hints hint-row row">
{% for hint in hints %}
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
<a class="btn btn-info btn-hint btn-block load-hint" href="javascript:;" data-hint-id="{{ hint.id }}">
{% if hint.hint %}
<small>
View Hint
</small>
{% else %}
{% if hint.cost %}
<small>
Unlock Hint for {{ hint.cost }} points
</small>
{% else %}
<small>
View Hint
</small>
{% endif %}
{% endif %}
</a>
</div>
{% endfor %}
</div>
<div class="row challenge-files text-center pb-3">
{% for file in files %}
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate'
href='{{ file }}'>
<i class="fas fa-download"></i>
<small>
{% set segments = file.split('/') %}
{% set file = segments | last %}
{% set token = file.split('?') | last %}
{% if token %}
{{ file | replace("?" + token, "") }}
{% else %}
{{ file }}
{% endif %}
</small>
</a>
</div>
{% endfor %}
</div>
{% extends "challenge.html" %}
<div class="row submit-row">
<div class="col-md-9 form-group">
<input class="form-control" type="text" name="answer" id="submission-input"
placeholder="Flag"/>
<input id="challenge-id" type="hidden" value="{{ id }}">
</div>
<div class="col-md-3 form-group key-submit">
<button type="submit" id="submit-key" tabindex="0"
class="btn btn-md btn-outline-secondary float-right">Submit
</button>
</div>
</div>
<div class="row notification-row">
<div class="col-md-12">
<div id="result-notification" class="alert alert-dismissable text-center w-100"
role="alert" style="display: none;">
<strong id="result-message"></strong>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" id="solves">
<div class="row">
<div class="col-md-12">
<table class="table table-striped text-center">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="challenge-solves-names">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% block description %}
{{ challenge.html }}
{% endblock %}
{% block input %}
<input id="challenge-id" class="challenge-id" type="hidden" value="{{ challenge.id }}">
<input id="challenge-input" class="challenge-input" type="text" name="answer" placeholder="Flag"/>
{% endblock %}
{% block submit %}
<button id="challenge-submit" class="challenge-submit" type="submit">
Submit
</button>
{% endblock %}

View File

@@ -132,42 +132,33 @@ function renderSubmissionResponse(response, cb) {
function loadChalTemplate(challenge) {
CTFd._internal.challenge = {};
$.getScript(CTFd.config.urlRoot + challenge.scripts.view, function() {
$.get(CTFd.config.urlRoot + challenge.templates.create, function(
template_data
) {
const template = nunjucks.compile(template_data);
$("#create-chal-entry-div").html(
template.render({
nonce: CTFd.config.csrfNonce,
script_root: CTFd.config.urlRoot
})
);
let template_data = challenge.create;
$("#create-chal-entry-div").html(template_data);
$.getScript(CTFd.config.urlRoot + challenge.scripts.create, function() {
$("#create-chal-entry-div form").submit(function(event) {
event.preventDefault();
const params = $("#create-chal-entry-div form").serializeJSON();
CTFd.fetch("/api/v1/challenges", {
method: "POST",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(params)
$.getScript(CTFd.config.urlRoot + challenge.scripts.create, function() {
$("#create-chal-entry-div form").submit(function(event) {
event.preventDefault();
const params = $("#create-chal-entry-div form").serializeJSON();
CTFd.fetch("/api/v1/challenges", {
method: "POST",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(params)
})
.then(function(response) {
return response.json();
})
.then(function(response) {
return response.json();
})
.then(function(response) {
if (response.success) {
$("#challenge-create-options #challenge_id").val(
response.data.id
);
$("#challenge-create-options").modal();
}
});
});
.then(function(response) {
if (response.success) {
$("#challenge-create-options #challenge_id").val(
response.data.id
);
$("#challenge-create-options").modal();
}
});
});
});
});
@@ -270,87 +261,57 @@ $(() => {
$.getScript(
CTFd.config.urlRoot + challenge_data.type_data.scripts.view,
function() {
$.get(
CTFd.config.urlRoot + challenge_data.type_data.templates.view,
function(template_data) {
$("#challenge-window").empty();
var template = nunjucks.compile(template_data);
// window.challenge.data = challenge_data;
// window.challenge.preRender();
challenge.data = challenge_data;
challenge.preRender();
$("#challenge-window").empty();
challenge_data["description"] = challenge.render(
challenge_data["description"]
);
challenge_data["script_root"] = CTFd.config.urlRoot;
$("#challenge-window").append(challenge_data.view);
$("#challenge-window").append(template.render(challenge_data));
$(".challenge-solves").click(function(e) {
getsolves($("#challenge-id").val());
});
$(".nav-tabs a").click(function(e) {
e.preventDefault();
$(this).tab("show");
});
// Handle modal toggling
$("#challenge-window").on("hide.bs.modal", function(event) {
$("#submission-input").removeClass("wrong");
$("#submission-input").removeClass("correct");
$("#incorrect-key").slideUp();
$("#correct-key").slideUp();
$("#already-solved").slideUp();
$("#too-fast").slideUp();
});
$(".load-hint").on("click", function(event) {
loadHint($(this).data("hint-id"));
});
$("#submit-key").click(function(e) {
e.preventDefault();
$("#submit-key").addClass("disabled-button");
$("#submit-key").prop("disabled", true);
CTFd._internal.challenge
.submit(true)
.then(renderSubmissionResponse);
// Preview passed as true
});
$("#submission-input").keyup(function(event) {
if (event.keyCode == 13) {
$("#submit-key").click();
}
});
$(".input-field").bind({
focus: function() {
$(this)
.parent()
.addClass("input--filled");
$label = $(this).siblings(".input-label");
},
blur: function() {
if ($(this).val() === "") {
$(this)
.parent()
.removeClass("input--filled");
$label = $(this).siblings(".input-label");
$label.removeClass("input--hide");
}
}
});
challenge.postRender();
window.location.replace(
window.location.href.split("#")[0] + "#preview"
);
$("#challenge-window").modal();
}
$("#challenge-window #challenge-input").addClass("form-control");
$("#challenge-window #challenge-submit").addClass(
"btn btn-md btn-outline-secondary float-right"
);
$(".challenge-solves").hide();
$(".nav-tabs a").click(function(e) {
e.preventDefault();
$(this).tab("show");
});
// Handle modal toggling
$("#challenge-window").on("hide.bs.modal", function(event) {
$("#challenge-input").removeClass("wrong");
$("#challenge-input").removeClass("correct");
$("#incorrect-key").slideUp();
$("#correct-key").slideUp();
$("#already-solved").slideUp();
$("#too-fast").slideUp();
});
$(".load-hint").on("click", function(event) {
loadHint($(this).data("hint-id"));
});
$("#challenge-submit").click(function(e) {
e.preventDefault();
$("#challenge-submit").addClass("disabled-button");
$("#challenge-submit").prop("disabled", true);
CTFd._internal.challenge
.submit(true)
.then(renderSubmissionResponse);
// Preview passed as true
});
$("#challenge-input").keyup(function(event) {
if (event.keyCode == 13) {
$("#challenge-submit").click();
}
});
challenge.postRender();
window.location.replace(
window.location.href.split("#")[0] + "#preview"
);
$("#challenge-window").modal();
}
);
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,86 @@
{% block header %}
{% endblock %}
<form method="POST" action="{{ script_root }}/admin/challenges/new" enctype="multipart/form-data">
{% block name %}
<div class="form-group">
<label>
Name:<br>
<small class="form-text text-muted">
The name of your challenge
</small>
</label>
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
</div>
{% endblock %}
{% block category %}
<div class="form-group">
<label>
Category:<br>
<small class="form-text text-muted">
The category of your challenge
</small>
</label>
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
</div>
{% endblock %}
{% block message %}
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab"
data-toggle="tab" tabindex="-1">Write</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab" tabindex="-1">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
<div class="form-group">
<label>
Message:<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
</div>
</div>
{% endblock %}
{% block value %}
<div class="form-group">
<label>
Value:<br>
<small class="form-text text-muted">
This is how many points are rewarded for solving this challenge.
</small>
</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div>
{% endblock %}
{% block state %}
<input type="hidden" name="state" value="hidden">
{% endblock %}
{% block type %}
<input type="hidden" name="type" value="standard">
{% endblock %}
{% block submit %}
<div class="form-group">
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
</div>
{% endblock %}
</form>
{% block footer %}
{% endblock %}

View File

@@ -0,0 +1,84 @@
{% block header %}
{% endblock %}
<form method="POST">
{% block name %}
<div class="form-group">
<label>
Name<br>
<small class="form-text text-muted">Challenge Name</small>
</label>
<input type="text" class="form-control chal-name" name="name" value="{{ challenge.name }}">
</div>
{% endblock %}
{% block category %}
<div class="form-group">
<label>
Category<br>
<small class="form-text text-muted">Challenge Category</small>
</label>
<input type="text" class="form-control chal-category" name="category" value="{{ challenge.category }}">
</div>
{% endblock %}
{% block message %}
<div class="form-group">
<label>
Message<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ challenge.description }}</textarea>
</div>
{% endblock %}
{% block value %}
<div class="form-group">
<label for="value">
Value<br>
<small class="form-text text-muted">
This is how many points teams will receive once they solve this challenge.
</small>
</label>
<input type="number" class="form-control chal-value" name="value" value="{{ challenge.value }}" required>
</div>
{% endblock %}
{% block max_attempts %}
<div class="form-group">
<label>
Max Attempts<br>
<small class="form-text text-muted">Maximum amount of attempts users receive. Leave at 0 for unlimited.</small>
</label>
<input type="number" class="form-control chal-attempts" name="max_attempts" value="{{ challenge.max_attempts }}">
</div>
{% endblock %}
{% block state %}
<div class="form-group">
<label>
State<br>
<small class="form-text text-muted">Changes the state of the challenge (e.g. visible, hidden)</small>
</label>
<select class="form-control custom-select" name="state">
<option value="visible" {% if challenge.state == "visible" %}selected{% endif %}>Visible</option>
<option value="hidden" {% if challenge.state == "hidden" %}selected{% endif %}>Hidden</option>
</select>
</div>
{% endblock %}
{% block submit %}
<div>
<button class="btn btn-success btn-outlined float-right" type="submit">
Update
</button>
</div>
{% endblock %}
</form>
{% block footer %}
{% endblock %}

View File

@@ -1,6 +1,5 @@
import "./main";
import "bootstrap/js/dist/tab";
import nunjucks from "nunjucks";
import { ezQuery, ezAlert } from "../ezq";
import { htmlEntities } from "../utils";
import Moment from "moment";
@@ -46,21 +45,16 @@ const displayChal = chal => {
$.getScript(config.urlRoot + chal.script),
$.get(config.urlRoot + chal.template)
]).then(responses => {
const challenge_data = responses[0].data;
const template_data = responses[2];
const challenge = CTFd._internal.challenge;
$("#challenge-window").empty();
const template = nunjucks.compile(template_data);
challenge.data = challenge_data;
challenge.preRender();
challenge_data["description"] = challenge.render(
challenge_data["description"]
$("#challenge-window").append(responses[0].data.view);
$("#challenge-window #challenge-input").addClass("form-control");
$("#challenge-window #challenge-submit").addClass(
"btn btn-md btn-outline-secondary float-right"
);
challenge_data["script_root"] = CTFd.config.urlRoot;
$("#challenge-window").append(template.render(challenge_data));
let modal = $("#challenge-window").find(".modal-dialog");
if (
@@ -92,8 +86,8 @@ const displayChal = chal => {
// Handle modal toggling
$("#challenge-window").on("hide.bs.modal", function(event) {
$("#submission-input").removeClass("wrong");
$("#submission-input").removeClass("correct");
$("#challenge-input").removeClass("wrong");
$("#challenge-input").removeClass("correct");
$("#incorrect-key").slideUp();
$("#correct-key").slideUp();
$("#already-solved").slideUp();
@@ -104,10 +98,10 @@ const displayChal = chal => {
loadHint($(this).data("hint-id"));
});
$("#submit-key").click(function(event) {
$("#challenge-submit").click(function(event) {
event.preventDefault();
$("#submit-key").addClass("disabled-button");
$("#submit-key").prop("disabled", true);
$("#challenge-submit").addClass("disabled-button");
$("#challenge-submit").prop("disabled", true);
CTFd._internal.challenge
.submit()
.then(renderSubmissionResponse)
@@ -115,25 +109,9 @@ const displayChal = chal => {
.then(markSolves);
});
$("#submission-input").keyup(event => {
$("#challenge-input").keyup(event => {
if (event.keyCode == 13) {
$("#submit-key").click();
}
});
$(".input-field").bind({
focus: function() {
$(this)
.parent()
.addClass("input--filled");
},
blur: function() {
const $this = $(this);
if ($this.val() === "") {
$this.parent().removeClass("input--filled");
const $label = $this.siblings(".input-label");
$label.removeClass("input--hide");
}
$("#challenge-submit").click();
}
});
@@ -151,7 +129,7 @@ function renderSubmissionResponse(response) {
const result_message = $("#result-message");
const result_notification = $("#result-notification");
const answer_input = $("#submission-input");
const answer_input = $("#challenge-input");
result_notification.removeClass();
result_message.text(result.message);
@@ -223,8 +201,8 @@ function renderSubmissionResponse(response) {
}
setTimeout(function() {
$(".alert").slideUp();
$("#submit-key").removeClass("disabled-button");
$("#submit-key").prop("disabled", false);
$("#challenge-submit").removeClass("disabled-button");
$("#challenge-submit").prop("disabled", false);
}, 3000);
}
@@ -371,9 +349,9 @@ $(() => {
}
});
$("#submission-input").keyup(function(event) {
$("#challenge-input").keyup(function(event) {
if (event.keyCode == 13) {
$("#submit-key").click();
$("#challenge-submit").click();
}
});
@@ -392,8 +370,8 @@ $(() => {
});
$("#challenge-window").on("hide.bs.modal", function(event) {
$("#submission-input").removeClass("wrong");
$("#submission-input").removeClass("correct");
$("#challenge-input").removeClass("wrong");
$("#challenge-input").removeClass("correct");
$("#incorrect-key").slideUp();
$("#correct-key").slideUp();
$("#already-solved").slideUp();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,134 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="#challenge">Challenge</a>
</li>
{% block solves %}
<li class="nav-item">
<a class="nav-link challenge-solves" href="#solves">
{% if solves != None %}
{{ solves }} {% if solves > 1 %}Solves{% else %}Solves{% endif %}
{% endif %}
</a>
</li>
{% endblock %}
</ul>
<div role="tabpanel">
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
<h2 class='challenge-name text-center pt-3'>
{{ challenge.name }}
</h2>
<h3 class="challenge-value text-center">
{{ challenge.value }}
</h3>
<div class="challenge-tags text-center">
{% block tags %}
{% for tag in tags %}
<span class='badge badge-info challenge-tag'>{{ tag }}</span>
{% endfor %}
{% endblock %}
</div>
<span class="challenge-desc">{% block description %}{{ challenge.html }}{% endblock %}</span>
<div class="challenge-hints hint-row row">
{% for hint in hints %}
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
<a class="btn btn-info btn-hint btn-block load-hint" href="javascript:;" data-hint-id="{{ hint.id }}">
{% if hint.content %}
<small>
View Hint
</small>
{% else %}
{% if hint.cost %}
<small>
Unlock Hint for {{ hint.cost }} points
</small>
{% else %}
<small>
View Hint
</small>
{% endif %}
{% endif %}
</a>
</div>
{% endfor %}
</div>
<div class="row challenge-files text-center pb-3">
{% for file in files %}
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate'
href='{{ file }}'>
<i class="fas fa-download"></i>
<small>
{% set segments = file.split('/') %}
{% set file = segments | last %}
{% set token = file.split('?') | last %}
{% if token %}
{{ file | replace("?" + token, "") }}
{% else %}
{{ file }}
{% endif %}
</small>
</a>
</div>
{% endfor %}
</div>
<div class="row submit-row">
<div class="col-md-9 form-group">
{% block input %}
<input id="challenge-id" type="hidden" value="{{ challenge.id }}">
<input class="form-control" type="text" name="answer" id="submission-input" placeholder="Flag"/>
{% endblock %}
</div>
<div class="col-md-3 form-group key-submit">
{% block submit %}
<button type="submit" id="submit-key" class="btn btn-md btn-outline-secondary float-right">
Submit
</button>
{% endblock %}
</div>
</div>
<div class="row notification-row">
<div class="col-md-12">
<div id="result-notification" class="alert alert-dismissable text-center w-100"
role="alert" style="display: none;">
<strong id="result-message"></strong>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" id="solves">
<div class="row">
<div class="col-md-12">
<table class="table table-striped text-center">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="challenge-solves-names">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,13 @@
from CTFd.cache import cache
from CTFd.models import Pages, db
from CTFd.utils import markdown
from CTFd.utils.security.sanitize import sanitize_html
def build_html(html):
html = markdown(html)
html = sanitize_html(html)
return html
@cache.memoize()
@@ -12,8 +20,14 @@ def get_pages():
@cache.memoize()
def get_page(route):
return db.session.execute(
page = db.session.execute(
Pages.__table__.select()
.where(Pages.route == route)
.where(Pages.draft.isnot(True))
).fetchone()
if page:
# Convert the row into a transient ORM object so this change isn't commited accidentally
p = Pages(**page)
p.content = build_html(p.content)
return p
return None

View File

@@ -38,7 +38,7 @@ def get_registered_admin_stylesheets():
def override_template(template, html):
app.jinja_loader.overriden_templates[template] = html
app.theme_loader.overriden_templates[template] = html
def get_configurable_plugins():

View File

@@ -0,0 +1,23 @@
# Bandit complains about security issues with lxml.
# These issues have been addressed in the past and do not apply to parsing HTML.
from lxml.html import html5parser, tostring # nosec B410
from lxml.html.clean import Cleaner # nosec B410
from lxml.html.defs import safe_attrs # nosec B410
cleaner = Cleaner(
page_structure=False,
embedded=False,
frames=False,
forms=False,
links=False,
meta=False,
style=False,
safe_attrs=(safe_attrs | set(["style"])),
annoying_tags=False,
)
def sanitize_html(html):
html = html5parser.fragment_fromstring(html, create_parent="div")
html = cleaner.clean_html(tostring(html)).decode()
return html

View File

@@ -17,7 +17,7 @@ from CTFd.models import (
UserTokens,
db,
)
from CTFd.utils import config, get_config, markdown, set_config
from CTFd.utils import config, get_config, set_config
from CTFd.utils import user as current_user
from CTFd.utils import validators
from CTFd.utils.config import is_setup
@@ -318,7 +318,7 @@ def static_html(route):
if page.auth_required and authed() is False:
return redirect(url_for("auth.login", next=request.full_path))
return render_template("page.html", content=markdown(page.content))
return render_template("page.html", content=page.content)
@views.route("/files", defaults={"path": ""})

View File

@@ -22,4 +22,6 @@ flask-marshmallow==0.10.1
marshmallow-sqlalchemy==0.17.0
boto3==1.13.9
marshmallow==2.20.2
lxml==4.5.1
html5lib==1.0.1
WTForms==2.3.1