mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 22:14:25 +01:00
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:
@@ -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,
|
||||
|
||||
@@ -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("/")
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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" %}
|
||||
@@ -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();
|
||||
// });
|
||||
})
|
||||
|
||||
@@ -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" %}
|
||||
@@ -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">×</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 %}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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">×</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 %}
|
||||
@@ -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
86
CTFd/themes/admin/templates/challenges/create.html
Normal file
86
CTFd/themes/admin/templates/challenges/create.html
Normal 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 %}
|
||||
84
CTFd/themes/admin/templates/challenges/update.html
Normal file
84
CTFd/themes/admin/templates/challenges/update.html
Normal 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 %}
|
||||
@@ -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
134
CTFd/themes/core/templates/challenge.html
Normal file
134
CTFd/themes/core/templates/challenge.html
Normal 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">×</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>
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
23
CTFd/utils/security/sanitize.py
Normal file
23
CTFd/utils/security/sanitize.py
Normal 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
|
||||
@@ -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": ""})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user