Skip to content

Commit b3f7a06

Browse files
AngelFQCclaude
andauthored
Security: enforce CSRF tokens on state-changing admin endpoints
Refs GHSA-cxxm-wpv8-fwh9 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent eab2fa2 commit b3f7a06

5 files changed

Lines changed: 98 additions & 92 deletions

File tree

public/main/admin/access_url_add_users_to_url.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646

4747
Display::page_subheader2($tool_name);
4848

49-
if (!empty($_POST['form_sent'])) {
49+
if (!empty($_POST['form_sent']) && Security::check_token('post')) {
5050
$form_sent = $_POST['form_sent'];
5151
$users = isset($_POST['user_list']) && is_array($_POST['user_list']) ? array_map('intval', $_POST['user_list']) : [];
5252
$url_list = isset($_POST['url_list']) && is_array($_POST['url_list']) ? $_POST['url_list'] : [];
@@ -99,6 +99,7 @@
9999

100100
<form name="formulaire" method="post" action="<?php echo api_get_self(); ?>" class="space-y-6" onsubmit="return confirmSubmission(event)">
101101
<input type="hidden" name="form_sent" value="1" />
102+
<?php echo Security::get_HTML_token(); ?>
102103
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
103104
<div>
104105
<label class="block text-sm font-medium text-gray-700 mb-1">

public/main/admin/add_sessions_to_promotion.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function validate_filter() {
7070
$users = $sessions = [];
7171
$promotion = new Promotion();
7272
$id = (int) ($_GET['id']);
73-
if (isset($_POST['form_sent']) && $_POST['form_sent']) {
73+
if (isset($_POST['form_sent']) && $_POST['form_sent'] && Security::check_token('post')) {
7474
$form_sent = $_POST['form_sent'];
7575
$session_in_promotion_posted = $_POST['session_in_promotion_name'];
7676
if (!is_array($session_in_promotion_posted)) {
@@ -135,6 +135,7 @@ function validate_filter() {
135135
echo Display::input('hidden', 'id', $id);
136136
echo Display::input('hidden', 'form_sent', '1');
137137
echo Display::input('hidden', 'add_type', null);
138+
echo Security::get_HTML_token();
138139
?>
139140

140141
<table border="0" cellpadding="5" cellspacing="0" width="100%">

public/main/admin/course_export.php

Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,62 @@
2626
'',
2727
api_get_current_access_url_id()
2828
);
29-
$formSent = null;
3029
$courses = $selected_courses = [];
3130

32-
if (isset($_POST['formSent']) && $_POST['formSent']) {
33-
$formSent = $_POST['formSent'];
34-
$select_type = (int) ($_POST['select_type']);
35-
$file_type = $_POST['file_type'];
31+
$form = new FormValidator('export', 'post', api_get_self());
32+
$form->protect();
33+
$form->addHeader($tool_name);
34+
$form->addElement(
35+
'radio',
36+
'select_type',
37+
get_lang('Option'),
38+
get_lang('Export all courses'),
39+
'1',
40+
['onclick' => "javascript: if(this.checked){document.getElementById('div-course-list').style.display='none';}"]
41+
);
42+
43+
$form->addElement(
44+
'radio',
45+
'select_type',
46+
'',
47+
get_lang('Export selected courses from the following list'),
48+
'2',
49+
['onclick' => "javascript: if(this.checked){document.getElementById('div-course-list').style.display='block';}"]
50+
);
51+
52+
if (!empty($course_list)) {
53+
$form->addHtml('<div id="div-course-list" style="display:none">');
54+
$coursesInList = [];
55+
foreach ($course_list as $course) {
56+
$coursesInList[$course['code']] = $course['title'].' ('.$course['code'].')';
57+
}
58+
59+
$form->addSelect(
60+
'course_code',
61+
get_lang('Courses to export'),
62+
$coursesInList,
63+
['multiple' => 'multiple']
64+
);
65+
66+
$form->addHtml('</div>');
67+
}
68+
69+
$form->addElement('radio', 'file_type', get_lang('Output file type'), 'CSV', 'csv', null);
70+
$form->addElement('radio', 'file_type', '', 'XLS', 'xls', null);
71+
$form->addElement('radio', 'file_type', null, 'XML', 'xml', null, ['id' => 'file_type_xml']);
72+
73+
$form->setDefaults(['select_type' => '1', 'file_type' => 'csv']);
74+
75+
$form->addButtonExport(get_lang('Export courses'));
76+
77+
if ($form->validate()) {
78+
$values = $form->exportValues();
79+
$select_type = (int) $values['select_type'];
80+
$file_type = $values['file_type'];
3681

3782
if (2 == $select_type) {
3883
// Get selected courses from courses list in form sent
39-
$selected_courses = $_POST['course_code'];
84+
$selected_courses = $values['course_code'] ?? [];
4085
if (is_array($selected_courses)) {
4186
foreach ($course_list as $course) {
4287
if (!in_array($course['code'], $selected_courses)) {
@@ -117,56 +162,15 @@
117162
get_lang('There are no selected courses or the courses list is empty.')
118163
)
119164
);
165+
// Post/Redirect/Get: the CSRF token is single-use (validate() clears it),
166+
// so redirect to render the form again with a fresh token.
167+
header('Location: '.api_get_self());
168+
exit;
120169
}
121170
}
122171

123172
Display::display_header($tool_name);
124173

125-
$form = new FormValidator('export', 'post', api_get_self());
126-
$form->addHeader($tool_name);
127-
$form->addHidden('formSent', 1);
128-
$form->addElement(
129-
'radio',
130-
'select_type',
131-
get_lang('Option'),
132-
get_lang('Export all courses'),
133-
'1',
134-
['onclick' => "javascript: if(this.checked){document.getElementById('div-course-list').style.display='none';}"]
135-
);
136-
137-
$form->addElement(
138-
'radio',
139-
'select_type',
140-
'',
141-
get_lang('Export selected courses from the following list'),
142-
'2',
143-
['onclick' => "javascript: if(this.checked){document.getElementById('div-course-list').style.display='block';}"]
144-
);
145-
146-
if (!empty($course_list)) {
147-
$form->addHtml('<div id="div-course-list" style="display:none">');
148-
$coursesInList = [];
149-
foreach ($course_list as $course) {
150-
$coursesInList[$course['code']] = $course['title'].' ('.$course['code'].')';
151-
}
152-
153-
$form->addSelect(
154-
'course_code',
155-
get_lang('Courses to export'),
156-
$coursesInList,
157-
['multiple' => 'multiple']
158-
);
159-
160-
$form->addHtml('</div>');
161-
}
162-
163-
$form->addElement('radio', 'file_type', get_lang('Output file type'), 'CSV', 'csv', null);
164-
$form->addElement('radio', 'file_type', '', 'XLS', 'xls', null);
165-
$form->addElement('radio', 'file_type', null, 'XML', 'xml', null, ['id' => 'file_type_xml']);
166-
167-
$form->setDefaults(['select_type' => '1', 'file_type' => 'csv']);
168-
169-
$form->addButtonExport(get_lang('Export courses'));
170174
$form->display();
171175

172176
Display :: display_footer();

public/main/admin/course_import.php

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ function save_courses_data($courses)
146146
}
147147

148148
if (!empty($msg)) {
149-
echo Display::return_message($msg, 'normal', false);
149+
Display::addFlash(Display::return_message($msg, 'normal', false));
150150
}
151151
}
152152

@@ -180,65 +180,64 @@ function parse_csv_courses_data($file)
180180
$interbreadcrumb[] = ['url' => 'index.php', 'name' => get_lang('Administration')];
181181

182182
set_time_limit(0);
183-
$csvCustomError = '';
184-
$topStaticErrorHtml = '';
185-
$delimiterError = false;
186-
$errors = [];
187-
Display::display_header($tool_name);
188183

189-
if (isset($_POST['formSent']) && $_POST['formSent']) {
184+
// Build the form first and protect it: FormValidator::protect() adds the CSRF
185+
// token and $form->validate() verifies it before any processing runs.
186+
$form = new FormValidator(
187+
'import',
188+
'post',
189+
api_get_self(),
190+
null,
191+
['enctype' => 'multipart/form-data']
192+
);
193+
$form->protect();
194+
$form->addHeader($tool_name);
195+
$form->addElement('file', 'import_file', get_lang('CSV file import location'));
196+
$form->addElement('checkbox', 'add_me_as_teacher', null, get_lang('Add me as teacher in the imported courses.'));
197+
$form->addButtonImport(get_lang('Import'), 'save');
198+
199+
if ($form->validate()) {
190200
if (empty($_FILES['import_file']['tmp_name'])) {
191-
$error_message = get_lang('The file upload has failed.');
192-
echo Display::return_message($error_message, 'error', false);
201+
Display::addFlash(Display::return_message(get_lang('The file upload has failed.'), 'error', false));
193202
} else {
194203
$allowed_file_mimetype = ['csv'];
195204

196205
$ext_import_file = substr($_FILES['import_file']['name'], strrpos($_FILES['import_file']['name'], '.') + 1);
197206

198207
if (!in_array($ext_import_file, $allowed_file_mimetype)) {
199-
echo Display::return_message(get_lang('You must import a file corresponding to the selected format'), 'error');
208+
Display::addFlash(Display::return_message(get_lang('You must import a file corresponding to the selected format'), 'error'));
200209
} else {
201210
$check = Import::assertCommaSeparated($_FILES['import_file']['tmp_name'], true);
202211
if (true !== $check) {
203-
$csvCustomError = $check;
204-
$topStaticErrorHtml = Display::return_message($csvCustomError, 'error', false);
205-
$delimiterError = true;
212+
Display::addFlash(Display::return_message($check, 'error', false));
206213
} else {
207214
$courses = parse_csv_courses_data($_FILES['import_file']['tmp_name']);
208215
$errors = validate_courses_data($courses);
209216
if (0 == count($errors)) {
210217
save_courses_data($courses);
218+
} else {
219+
$error_message = '<ul>';
220+
foreach ($errors as $error_course) {
221+
$error_message .= '<li>'.get_lang('Line').' '.$error_course['line'].': <strong>'.$error_course['error'].'</strong>: ';
222+
$error_message .= get_lang('Course').': '.$error_course['Title'].' ('.$error_course['Code'].')';
223+
$error_message .= '</li>';
224+
}
225+
$error_message .= '</ul>';
226+
Display::addFlash(Display::return_message($error_message, 'error', false));
211227
}
212228
}
213229
}
214230
}
215-
}
216-
if (!empty($topStaticErrorHtml)) {
217-
echo $topStaticErrorHtml;
218-
}
219-
if (isset($errors) && 0 != count($errors)) {
220-
$error_message = '<ul>';
221-
foreach ($errors as $index => $error_course) {
222-
$error_message .= '<li>'.get_lang('Line').' '.$error_course['line'].': <strong>'.$error_course['error'].'</strong>: ';
223-
$error_message .= get_lang('Course').': '.$error_course['Title'].' ('.$error_course['Code'].')';
224-
$error_message .= '</li>';
225-
}
226-
$error_message .= '</ul>';
227-
echo Display::return_message($error_message, 'error', false);
231+
232+
// Post/Redirect/Get: the CSRF token is single-use (validate() clears it),
233+
// so redirect to render the form again with a fresh token. All messages are
234+
// queued as flashes above and survive the redirect.
235+
header('Location: '.api_get_self());
236+
exit;
228237
}
229238

230-
$form = new FormValidator(
231-
'import',
232-
'post',
233-
api_get_self(),
234-
null,
235-
['enctype' => 'multipart/form-data']
236-
);
237-
$form->addHeader($tool_name);
238-
$form->addElement('file', 'import_file', get_lang('CSV file import location'));
239-
$form->addElement('checkbox', 'add_me_as_teacher', null, get_lang('Add me as teacher in the imported courses.'));
240-
$form->addButtonImport(get_lang('Import'), 'save');
241-
$form->addElement('hidden', 'formSent', 1);
239+
Display::display_header($tool_name);
240+
242241
$form->display();
243242

244243
$content = '

public/main/admin/dashboard_add_courses_to_user.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ function remove_item(origin) {
161161
$UserList = [];
162162

163163
$msg = '';
164-
if (isset($_POST['formSent']) && 1 == (int) ($_POST['formSent'])) {
164+
if (isset($_POST['formSent']) && 1 == (int) ($_POST['formSent']) && Security::check_token('post')) {
165165
$courses_list = isset($_POST['CoursesList']) ? $_POST['CoursesList'] : [];
166166
$affected_rows = CourseManager::subscribeCoursesToDrhManager($user_id, $courses_list);
167167
if ($affected_rows) {
@@ -225,6 +225,7 @@ function remove_item(origin) {
225225
?>
226226
<form name="formulaire" method="post" action="<?php echo api_get_self(); ?>?user=<?php echo $user_id; ?>" style="margin:0px;">
227227
<input type="hidden" name="formSent" value="1" />
228+
<?php echo Security::get_HTML_token(); ?>
228229
<?php
229230
if (!empty($msg)) {
230231
echo Display::return_message($msg, 'normal'); //main API

0 commit comments

Comments
 (0)