Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions platform/CTFd/plugins/exam_mode/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from flask import Blueprint, render_template, request, redirect, url_for, session
from CTFd.models import db, Users, UserFieldEntries, UserFields, Configs
from CTFd.utils.decorators import admins_only
from CTFd.plugins import register_admin_plugin_menu_bar
from CTFd.utils import set_config, get_config

def load(app):
# Register menu item
register_admin_plugin_menu_bar(
title='Exam Mode',
route='/admin/exam_mode/'
)

# Create blueprint
exam_mode = Blueprint(
'exam_mode',
__name__,
template_folder='templates',
url_prefix='/admin/exam_mode'
)

@exam_mode.route('/', methods=['GET'])
@admins_only
def index():
enabled = get_config('exam_mode_enabled', False)
allowed_ids = get_config('exam_mode_allowed_ids', '')
return render_template('exam_mode_config.html', exam_mode_enabled=enabled, exam_mode_allowed_ids=allowed_ids)

@exam_mode.route('/update', methods=['POST'])
@admins_only
def update_config():
enabled = request.form.get('exam_mode_enabled') == 'on'
allowed_ids_text = request.form.get('exam_mode_allowed_ids', '').strip()

# Save config
set_config('exam_mode_enabled', 'true' if enabled else 'false')
set_config('exam_mode_allowed_ids', allowed_ids_text)

# Parse allowed IDs
allowed_ids = set(line.strip() for line in allowed_ids_text.splitlines() if line.strip())

# Get Student ID field
student_id_field = UserFields.query.filter_by(name="Student ID Number").first()

if not student_id_field:
# If field doesn't exist, we can't filter by it, so maybe just warn?
# For now, let's assume it exists as per requirements.
pass

# Bulk update logic
users = Users.query.filter_by(type='user').all()

for user in users:
should_ban = False

if enabled:
# Check if user has allowed student ID
user_student_id = None
if student_id_field:
entry = UserFieldEntries.query.filter_by(user_id=user.id, field_id=student_id_field.id).first()
if entry:
user_student_id = entry.value

if user_student_id and user_student_id in allowed_ids:
should_ban = False
else:
should_ban = True
else:
# If disabled, unban everyone (or revert to previous state? Requirement says unban)
should_ban = False

user.banned = should_ban

db.session.commit()

return redirect(url_for('exam_mode.index'))

app.register_blueprint(exam_mode)
50 changes: 50 additions & 0 deletions platform/CTFd/plugins/exam_mode/templates/exam_mode_config.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{% extends "admin/base.html" %}

{% block stylesheets %}
<style>
.form-group {
margin-bottom: 2rem;
}
</style>
{% endblock %}

{% block content %}
<div class="jumbotron">
<div class="container">
<h1>Exam Mode Configuration</h1>
</div>
</div>

<div class="container">
<div class="row">
<div class="col-md-12">
<form method="POST" action="{{ url_for('exam_mode.update_config') }}">
<input type="hidden" name="nonce" value="{{ Session.nonce }}">

<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="exam_mode_enabled" name="exam_mode_enabled" {% if exam_mode_enabled %}checked{% endif %}>
<label class="custom-control-label" for="exam_mode_enabled">Enable Exam Mode</label>
</div>
<small class="form-text text-muted">
When enabled, all users NOT in the allowed list will be <strong>BANNED</strong>.
<br>
<span class="text-danger">WARNING: Enabling this will modify the 'banned' status of users in the database.</span>
</small>
</div>

<div class="form-group">
<label for="exam_mode_allowed_ids">Allowed Student IDs (One per line)</label>
<textarea class="form-control" id="exam_mode_allowed_ids" name="exam_mode_allowed_ids" rows="10">{{ exam_mode_allowed_ids }}</textarea>
<small class="form-text text-muted">Enter the Student IDs of users who should be allowed access during the exam.</small>
</div>

<button type="submit" class="btn btn-primary float-right">Save & Apply</button>
</form>
</div>
</div>
</div>
{% endblock %}

{% block scripts %}
{% endblock %}
5 changes: 5 additions & 0 deletions platform/CTFd/plugins/sql_challenges/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,11 @@ def attempt(cls, challenge, request):
import logging
logging.info(f"SQL Challenge attempt - Test mode: {is_test}, User ID: {user_id}, User Name: {user_name}, IP: {client_ip}")

# Enforce duplicate login check (session validation)
# This will abort if the session is invalid (e.g. duplicate login)
from CTFd.utils.user import get_current_user
get_current_user()

if not submission:
return ChallengeResponse(
status="incorrect",
Expand Down
24 changes: 0 additions & 24 deletions platform/CTFd/themes/admin/static/assets/htmlmixed-DzKIYTqA.js

This file was deleted.

This file was deleted.

12 changes: 0 additions & 12 deletions platform/CTFd/themes/admin/static/assets/pages/configs-zcDLjIaq.js

This file was deleted.

This file was deleted.

111 changes: 110 additions & 1 deletion platform/CTFd/themes/ddps/assets/js/sql_challenge.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,49 @@ function showErrorToast(message) {
}, 5000);
}

// Show session expired modal
function showSessionExpiredModal() {
console.log('[SQL Challenge] showSessionExpiredModal called');

// Check if modal already exists
const existingModal = document.getElementById('session-expired-modal');
if (existingModal) {
console.log('[SQL Challenge] Modal already exists, ensuring it is visible');
existingModal.classList.add('show');
existingModal.style.display = 'block';
existingModal.style.zIndex = '9999';
return;
}

console.log('[SQL Challenge] Creating new session expired modal');
const modalHtml = `
<div class="modal fade show" id="session-expired-modal" tabindex="-1" role="dialog" aria-modal="true" style="display: block; background-color: rgba(0,0,0,0.5); z-index: 9999;">
<div class="modal-dialog modal-dialog-centered" role="document" style="z-index: 10000;">
<div class="modal-content">
<div class="modal-header border-bottom-0">
<h5 class="modal-title text-danger">
<i class="fas fa-exclamation-circle me-2"></i>Session Expired
</h5>
</div>
<div class="modal-body text-center py-4">
<p class="mb-0">Your session has expired or you have logged in from another device.</p>
<p class="text-muted small mt-2">Please reload the page to continue.</p>
</div>
<div class="modal-footer border-top-0 justify-content-center">
<button type="button" class="btn btn-primary px-4" onclick="window.location.reload()">
<i class="fas fa-sync-alt me-2"></i>Reload Page
</button>
</div>
</div>
</div>
</div>
`;

document.body.insertAdjacentHTML('beforeend', modalHtml);
document.body.classList.add('modal-open');
console.log('[SQL Challenge] Modal injected into DOM');
}

// Clear saved code when challenge is solved
function clearSavedCode() {
const challengeId = document.getElementById('challenge-id').value;
Expand Down Expand Up @@ -323,13 +366,39 @@ async function executeSQLQuery() {
clearTimeout(timeoutId);

// Response received
console.log('[SQL Challenge] Response status:', response.status);
console.log('[SQL Challenge] Response redirected:', response.redirected);

if (!response.ok) {
console.error('Response not OK:', response.statusText);

if (response.status === 401 || response.status === 403) {
console.log('[SQL Challenge] 401/403 detected, showing modal');
showSessionExpiredModal();
return;
}
}

// Check for redirect (which might have returned 200 OK for the login page)
if (response.redirected && response.url.includes('/login')) {
console.log('[SQL Challenge] Redirect to login detected, showing modal');
showSessionExpiredModal();
return;
}

// First get the response as text to debug
const responseText = await response.text();
// Response text received

// Check if response text looks like an error page (HTML)
if (responseText.includes('You don\'t have the permission') ||
responseText.includes('Session expired') ||
responseText.includes('Sign In')) {
console.log('[SQL Challenge] Error page content detected, showing modal');
showSessionExpiredModal();
return;
}
// Response text received

// Try to parse as JSON
let result;
Expand Down Expand Up @@ -451,7 +520,47 @@ async function submitSQLChallenge() {

clearTimeout(timeoutId);

const result = await response.json();
console.log('[SQL Challenge] Submit response status:', response.status);
console.log('[SQL Challenge] Submit response redirected:', response.redirected);

if (!response.ok) {
console.error('Submit response not OK:', response.statusText);
if (response.status === 401 || response.status === 403) {
console.log('[SQL Challenge] 401/403 detected, showing modal');
showSessionExpiredModal();
return;
}
}

// Check for redirect
if (response.redirected && response.url.includes('/login')) {
console.log('[SQL Challenge] Redirect to login detected, showing modal');
showSessionExpiredModal();
return;
}

// First get the response as text
const responseText = await response.text();

// Check if response text looks like an error page (HTML)
if (responseText.includes('You don\'t have the permission') ||
responseText.includes('Session expired') ||
responseText.includes('Sign In')) {
console.log('[SQL Challenge] Error page content detected, showing modal');
showSessionExpiredModal();
return;
}

// Try to parse as JSON
let result;
try {
result = JSON.parse(responseText);
} catch (e) {
console.error('Failed to parse submit response as JSON:', e);
console.error('Submit response was:', responseText);
showErrorToast('Server returned invalid JSON response');
return;
}
// Submit result received

// Check if result has the expected structure
Expand Down
Loading