mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
fix(forms): encode form action URLs and configured attributes (#8437)
* fix(forms): preserve action URLs in HTML and JavaScript Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs(changelog): record form action URL fix for issue 8438 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(forms): encode configured form name and enctype attributes Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net>
This commit is contained in:
1 parent
bb4ceaae30
commit
769c59ec9e
3 files changed
+174
-3
No files matched your search
@@ -0,0 +1 @@
|
||||
Preserve form action URLs and configured attributes with context-specific encoding
|
||||
+3
-3
@@ -47,7 +47,7 @@ function draw_edit_form(array $array) : void {
|
||||
|
||||
if (cacti_sizeof($fields_array)) {
|
||||
if (!isset($config_array['no_form_tag'])) {
|
||||
print "<form class='cactiForm' method='post' autocomplete='off' action='" . ($config_array['post_to'] ?? get_current_page()) . "'" . ((isset($config_array['form_name'])) ? " name='" . $config_array['form_name'] . "'" : '') . ((isset($config_array['enctype'])) ? " enctype='" . $config_array['enctype'] . "'" : '') . '>';
|
||||
print "<form class='cactiForm' method='post' autocomplete='off' action='" . html_escape_url($config_array['post_to'] ?? get_current_page()) . "'" . ((isset($config_array['form_name'])) ? " name='" . html_escape_attr($config_array['form_name']) . "'" : '') . ((isset($config_array['enctype'])) ? " enctype='" . html_escape_attr($config_array['enctype']) . "'" : '') . '>';
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
@@ -2180,7 +2180,7 @@ function form_start(mixed $action, string $id = '', bool $multipart = false) : v
|
||||
|
||||
$form_action = $action;
|
||||
|
||||
print "<form class='cactiFormStart' id='$form_id' name='$form_id' action='$form_action' autocomplete='off' method='post'" . ($multipart ? " enctype='multipart/form-data'" : '') . '>';
|
||||
print "<form class='cactiFormStart' id='$form_id' name='$form_id' action='" . html_escape_url($form_action) . "' autocomplete='off' method='post'" . ($multipart ? " enctype='multipart/form-data'" : '') . '>';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2277,7 +2277,7 @@ function form_end(bool $ajax = true) : void {
|
||||
}
|
||||
|
||||
$(function() {
|
||||
formValidate('#<?php print $form_id; ?>', '<?php print $form_action; ?>');
|
||||
formValidate('#<?php print $form_id; ?>', <?php print cacti_js_encode((string) $form_action); ?>);
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
require_once dirname(__DIR__, 3) . '/Helpers/CactiStubs.php';
|
||||
require_once CACTI_PATH_INCLUDE . '/global.php';
|
||||
|
||||
dataset('form action URLs', [
|
||||
'plain' => ['graphs.php'],
|
||||
'query' => ["/cacti/graphs.php?filter=Bob's switch&rows=20#results"],
|
||||
'entities' => ['/cacti/graphs.php?filter=©"&'],
|
||||
'Unicode' => ["/cacti/graphs.php?filter=東京 \"switch\"\u{2028}next\u{2029}last"],
|
||||
'script text' => ["/cacti/graphs.php?filter=</script><p>notes</p>\\'"],
|
||||
'empty' => [''],
|
||||
'zero' => ['0'],
|
||||
]);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->savedFormGlobals = [];
|
||||
|
||||
foreach (['form_id', 'form_action', '_SESSION'] as $name) {
|
||||
$this->savedFormGlobals[$name] = [
|
||||
'present' => array_key_exists($name, $GLOBALS),
|
||||
'value' => $GLOBALS[$name] ?? null,
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
foreach ($this->savedFormGlobals as $name => $saved) {
|
||||
if ($saved['present']) {
|
||||
$GLOBALS[$name] = $saved['value'];
|
||||
} else {
|
||||
unset($GLOBALS[$name]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves form action URLs in HTML and AJAX initialization', function (string $action) {
|
||||
ob_start();
|
||||
|
||||
try {
|
||||
form_start($action, 'edit_form', true);
|
||||
form_end();
|
||||
$html = ob_get_contents();
|
||||
} finally {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$document = new DOMDocument();
|
||||
$previous = libxml_use_internal_errors(true);
|
||||
|
||||
try {
|
||||
expect($document->loadHTML('<?xml encoding="UTF-8">' . $html))->toBeTrue();
|
||||
} finally {
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($previous);
|
||||
}
|
||||
|
||||
$forms = $document->getElementsByTagName('form');
|
||||
expect($forms->length)->toBe(1);
|
||||
$form = $forms->item(0);
|
||||
expect($form->getAttribute('action'))->toBe($action)
|
||||
->and($form->getAttribute('id'))->toBe('edit_form')
|
||||
->and($form->getAttribute('name'))->toBe('edit_form')
|
||||
->and($form->getAttribute('method'))->toBe('post')
|
||||
->and($form->getAttribute('enctype'))->toBe('multipart/form-data')
|
||||
->and($GLOBALS['form_action'])->toBe($action);
|
||||
|
||||
$scripts = $document->getElementsByTagName('script');
|
||||
expect($scripts->length)->toBe(1);
|
||||
$script = $scripts->item(0)->textContent;
|
||||
expect(preg_match('/formValidate\(\'#edit_form\', (.*)\);/', $script, $matches))->toBe(1);
|
||||
expect(json_decode($matches[1], true, 512, JSON_THROW_ON_ERROR))->toBe($action)
|
||||
->and($document->getElementsByTagName('p')->length)->toBe(0);
|
||||
})->with('form action URLs');
|
||||
|
||||
it('preserves configured edit form action URLs', function (string $action) {
|
||||
ob_start();
|
||||
|
||||
try {
|
||||
draw_edit_form([
|
||||
'config' => ['post_to' => $action],
|
||||
'fields' => ['id' => ['method' => 'hidden', 'value' => 7]],
|
||||
]);
|
||||
print '</form>';
|
||||
$html = ob_get_contents();
|
||||
} finally {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$document = new DOMDocument();
|
||||
$previous = libxml_use_internal_errors(true);
|
||||
|
||||
try {
|
||||
expect($document->loadHTML('<?xml encoding="UTF-8">' . $html))->toBeTrue();
|
||||
} finally {
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($previous);
|
||||
}
|
||||
|
||||
$forms = $document->getElementsByTagName('form');
|
||||
expect($forms->length)->toBe(1)
|
||||
->and($forms->item(0)->getAttribute('action'))->toBe($action)
|
||||
->and($forms->item(0)->getAttribute('method'))->toBe('post')
|
||||
->and($document->getElementsByTagName('input')->item(0)->getAttribute('value'))->toBe('7')
|
||||
->and($document->getElementsByTagName('script')->length)->toBe(0);
|
||||
})->with('form action URLs');
|
||||
|
||||
it('preserves generated form IDs and the non-AJAX form mode', function () {
|
||||
$ids = [];
|
||||
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
ob_start();
|
||||
|
||||
try {
|
||||
form_start('edit.php');
|
||||
$ids[] = $GLOBALS['form_id'];
|
||||
form_end(false);
|
||||
$html = ob_get_contents();
|
||||
} finally {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
expect($html)->not->toContain('<script')
|
||||
->not->toContain('multipart/form-data')
|
||||
->toContain('</form>');
|
||||
}
|
||||
|
||||
expect($ids[0])->toMatch('/^form[0-9]+$/')
|
||||
->and($ids[1])->toMatch('/^form[0-9]+$/')
|
||||
->not->toBe($ids[0]);
|
||||
});
|
||||
|
||||
it('keeps configured form attributes as literal values', function (string $name, string $enctype) {
|
||||
ob_start();
|
||||
|
||||
try {
|
||||
draw_edit_form([
|
||||
'config' => ['post_to' => 'edit.php', 'form_name' => $name, 'enctype' => $enctype],
|
||||
'fields' => ['id' => ['method' => 'hidden', 'value' => 7]],
|
||||
]);
|
||||
print '</form>';
|
||||
$html = ob_get_contents();
|
||||
} finally {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$document = new DOMDocument();
|
||||
expect($document->loadHTML($html))->toBeTrue();
|
||||
$form = $document->getElementsByTagName('form')->item(0);
|
||||
expect($form->getAttribute('name'))->toBe($name)
|
||||
->and($form->getAttribute('enctype'))->toBe($enctype)
|
||||
->and($form->attributes->length)->toBe(6);
|
||||
})->with([
|
||||
'normal' => ['edit_form', 'multipart/form-data'],
|
||||
'name quotes and entities' => ["Bob's "form", 'multipart/form-data'],
|
||||
'enctype quoting' => ['edit_form', "multipart/form-data' data-extra='value"],
|
||||
]);
|
||||
Reference in new issue
Block a user