FIX_FAQ_and_FE_Content
This commit is contained in:
parent
5515499403
commit
fc7941ed26
@ -226,6 +226,14 @@ class AppContentManagementController extends AdminController
|
||||
|
||||
if ($this->request->getMethod() === 'post') {
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* STEP 1: INITIAL VALIDATION
|
||||
* --------------------------------------------------------------------------
|
||||
* These are the basic validation rules. For 'content' and 'notes', we only
|
||||
* check if they are provided and within the allowed length.
|
||||
* The more advanced security check for script tags happens next.
|
||||
*/
|
||||
$rules = [
|
||||
'fe_id' => [
|
||||
'rules' => 'permit_empty|integer|is_natural',
|
||||
@ -258,22 +266,20 @@ class AppContentManagementController extends AdminController
|
||||
'regex_match' => 'Heading contains invalid characters. Only letters, numbers, spaces and basic punctuation are allowed'
|
||||
]
|
||||
],
|
||||
'content' => [
|
||||
'rules' => 'required|max_length[5000]|regex_match[/^[a-zA-Z0-9 _\-.,;:!?()&\/\r\n]+$/]',
|
||||
'content' => [
|
||||
'rules' => 'required|max_length[5000]',
|
||||
'errors' => [
|
||||
'required' => 'Content is required',
|
||||
'max_length' => 'Content cannot exceed 5000 characters',
|
||||
'regex_match' => 'Content contains invalid characters. Only letters, numbers, spaces and basic punctuation are allowed'
|
||||
'required' => 'Content is required',
|
||||
'max_length' => 'Content cannot exceed 5000 characters',
|
||||
]
|
||||
],
|
||||
'notes' => [
|
||||
'rules' => 'required|max_length[1500]|regex_match[/^[a-zA-Z0-9 _\-.,;:!?()&\/\r\n]+$/]',
|
||||
'rules' => 'required|max_length[1500]',
|
||||
'errors' => [
|
||||
'required' => 'Notes are required',
|
||||
'max_length' => 'Notes cannot exceed 1500 characters',
|
||||
'regex_match' => 'Notes contains invalid characters. Only letters, numbers, spaces and basic punctuation are allowed'
|
||||
'required' => 'Notes are required',
|
||||
'max_length' => 'Notes cannot exceed 1500 characters',
|
||||
]
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
@ -284,9 +290,60 @@ class AppContentManagementController extends AdminController
|
||||
'errors' => $this->validator->getErrors()
|
||||
]);
|
||||
}
|
||||
$request_post_data = $this->request->getPost();
|
||||
$data = sanitizeInputArrayAdvanced($request_post_data);
|
||||
$id = $data['fe_id'] ?? null;
|
||||
|
||||
/**************************************************************************
|
||||
* REFACTORED SANITIZATION LOGIC (XSS Protection)
|
||||
**************************************************************************
|
||||
*
|
||||
* Per the user's request, we are avoiding the generic `sanitizeInputArrayAdvanced`
|
||||
* on the `content` and `notes` fields, as they require special HTML
|
||||
* handling.
|
||||
*
|
||||
* The new process is:
|
||||
* 1. Get the raw `content` and `notes` directly from the POST request.
|
||||
* 2. Perform the critical XSS validation on this raw content using `hasXssTags()`.
|
||||
* If it fails, the request is rejected immediately. This satisfies all
|
||||
* the failure test cases (Tests 4-9).
|
||||
* 3. Take all *other* POST data and sanitize it using the generic
|
||||
* `sanitizeInputArrayAdvanced` function.
|
||||
* 4. Sanitize the now-validated `content` and `notes` using our specific
|
||||
* `sanitizeHtml()` function, which allows safe HTML.
|
||||
* 5. Combine the sanitized data into a final array for database insertion.
|
||||
*
|
||||
*************************************************************************/
|
||||
|
||||
// Step 1: Get raw `content` and `notes`.
|
||||
$rawContent = $this->request->getPost('content');
|
||||
$rawNotes = $this->request->getPost('notes');
|
||||
|
||||
// Step 2: Perform critical XSS validation on raw input.
|
||||
$xssErrors = [];
|
||||
if ($this->hasXssTags($rawContent)) {
|
||||
$xssErrors['content'] = 'Content contains restricted tags. Script, iframe and event handlers are not allowed';
|
||||
}
|
||||
if ($this->hasXssTags($rawNotes)) {
|
||||
$xssErrors['notes'] = 'Notes contains restricted tags. Script, iframe and event handlers are not allowed';
|
||||
}
|
||||
|
||||
if (!empty($xssErrors)) {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
'status' => false,
|
||||
'message' => 'Input validation failed',
|
||||
'code' => 400,
|
||||
'errors' => $xssErrors
|
||||
]);
|
||||
}
|
||||
|
||||
// Step 3: Sanitize all *other* POST data.
|
||||
$otherPostData = $this->request->getPost();
|
||||
unset($otherPostData['content'], $otherPostData['notes']);
|
||||
$data = sanitizeInputArrayAdvanced($otherPostData);
|
||||
|
||||
// Step 4 & 5: Sanitize and re-combine `content` and `notes`.
|
||||
$data['content'] = $this->sanitizeHtml($rawContent);
|
||||
$data['notes'] = $this->sanitizeHtml($rawNotes);
|
||||
|
||||
$id = $data['fe_id'] ?? null;
|
||||
|
||||
if (!empty($id) && (!ctype_digit((string)$id) || (int)$id <= 0)) {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
@ -416,19 +473,17 @@ class AppContentManagementController extends AdminController
|
||||
]
|
||||
],
|
||||
'question' => [
|
||||
'rules' => 'required|max_length[1000]|regex_match[/^[a-zA-Z0-9 _\-.,;:!?()&\/\r\n]+$/]',
|
||||
'rules' => 'required|max_length[1000]',
|
||||
'errors' => [
|
||||
'required' => 'Question is required',
|
||||
'max_length' => 'Question cannot exceed 1000 characters',
|
||||
'regex_match' => 'Question contains invalid characters. Only letters, numbers, spaces and basic punctuation are allowed'
|
||||
]
|
||||
],
|
||||
'answer' => [
|
||||
'rules' => 'required|max_length[5000]|regex_match[/^[a-zA-Z0-9 _\-.,;:!?()&\/\r\n]+$/]',
|
||||
'rules' => 'required|max_length[5000]',
|
||||
'errors' => [
|
||||
'required' => 'Answer is required',
|
||||
'max_length' => 'Answer cannot exceed 5000 characters',
|
||||
'regex_match' => 'Answer contains invalid characters. Only letters, numbers, spaces and basic punctuation are allowed'
|
||||
]
|
||||
]
|
||||
];
|
||||
@ -443,9 +498,50 @@ class AppContentManagementController extends AdminController
|
||||
]);
|
||||
}
|
||||
|
||||
$request_post_data = $this->request->getPost();
|
||||
$sanitized_post_data = sanitizeInputArrayAdvanced($request_post_data);
|
||||
$data = array_filter($sanitized_post_data, fn($v) => $v !== '' && $v !== null);
|
||||
/**************************************************************************
|
||||
* XSS PROTECTION FOR 'question' and 'answer'
|
||||
**************************************************************************
|
||||
*
|
||||
* Applying the same security model as `frontend_content`.
|
||||
*
|
||||
* 1. Validate raw `question` and `answer` for malicious tags using `hasXssTags()`.
|
||||
* If found, reject the request immediately.
|
||||
* 2. Sanitize all *other* fields using the generic `sanitizeInputArrayAdvanced`.
|
||||
* 3. Sanitize the `question` and `answer` using the HTML-aware `sanitizeHtml()`
|
||||
* function to allow safe tags before saving.
|
||||
*
|
||||
*************************************************************************/
|
||||
|
||||
// Step 1: Validate raw input for XSS threats.
|
||||
$rawQuestion = $this->request->getPost('question');
|
||||
$rawAnswer = $this->request->getPost('answer');
|
||||
$xssErrors = [];
|
||||
|
||||
if ($this->hasXssTags($rawQuestion)) {
|
||||
$xssErrors['question'] = 'Question contains restricted tags. Script, iframe and event handlers are not allowed';
|
||||
}
|
||||
if ($this->hasXssTags($rawAnswer)) {
|
||||
$xssErrors['answer'] = 'Answer contains restricted tags. Script, iframe and event handlers are not allowed';
|
||||
}
|
||||
|
||||
if (!empty($xssErrors)) {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => 'Input validation failed',
|
||||
'code' => 400,
|
||||
'errors' => $xssErrors,
|
||||
'ref' => $ref
|
||||
]);
|
||||
}
|
||||
|
||||
// Step 2 & 3: Sanitize and combine data.
|
||||
$otherPostData = $this->request->getPost();
|
||||
unset($otherPostData['question'], $otherPostData['answer']);
|
||||
$data = sanitizeInputArrayAdvanced($otherPostData);
|
||||
|
||||
$data['question'] = $this->sanitizeHtml($rawQuestion);
|
||||
$data['answer'] = $this->sanitizeHtml($rawAnswer);
|
||||
|
||||
$id = $data['faq_id'] ?? null;
|
||||
|
||||
if (!empty($id) && (!ctype_digit((string)$id) || (int)$id <= 0)) {
|
||||
@ -470,12 +566,6 @@ class AppContentManagementController extends AdminController
|
||||
$msg = "Updated";
|
||||
}
|
||||
|
||||
// if ($returnType === 'web') {
|
||||
// return redirect()->back()->with($status ? 'success' : 'error', "FAQ $msg " . ($status ? 'successfully' : 'failed'));
|
||||
// }
|
||||
|
||||
// return $this->response->setJSON([
|
||||
// ])->setStatusCode($result ? 200 : 400);
|
||||
return $this->response->setJSON([
|
||||
'status' => $status ? 'success' : 'error',
|
||||
'message' => "FAQ $msg " . ($status ? 'successfully' : 'failed'),
|
||||
@ -611,4 +701,104 @@ class AppContentManagementController extends AdminController
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// Add these two private methods inside AppContentManagementController
|
||||
|
||||
|
||||
/**
|
||||
* =================================================================================
|
||||
* HTML SANITIZATION & VALIDATION HELPER METHODS
|
||||
* =================================================================================
|
||||
* The following two methods are the core of the XSS protection logic.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* sanitizeHtml()
|
||||
*
|
||||
* This function cleans a string of HTML, ensuring it is safe to display in a browser.
|
||||
* It allows a specific set of safe HTML tags and removes any dangerous attributes
|
||||
* from those tags.
|
||||
*
|
||||
* @param string $input The raw HTML string from user input.
|
||||
* @return string The cleaned, safe HTML string.
|
||||
*/
|
||||
private function sanitizeHtml(string $input): string
|
||||
{
|
||||
/**
|
||||
* Define a whitelist of allowed HTML tags. Any tag not in this list will be
|
||||
* completely removed. We are allowing basic formatting, lists, tables, etc.
|
||||
*/
|
||||
// ✅ Added <s>, <u>, <h1>-<h6>, <blockquote>, <pre>, <code>, <hr> for Jodit support
|
||||
$allowed_tags = '<p><b><i><s><u><strong><em><ul><ol><li><br><a><img><table><thead><tbody><tr><th><td><span><div><h1><h2><h3><h4><h5><h6><blockquote><pre><code><hr><sub><sup>';
|
||||
|
||||
// Use strip_tags() to remove all tags that are not in our whitelist.
|
||||
$clean = strip_tags($input, $allowed_tags);
|
||||
|
||||
/**
|
||||
* Define a blacklist of dangerous attributes. These are often used for XSS
|
||||
* attacks (e.g., `onclick`, `onmouseover`). We search for and remove these
|
||||
* attributes from any remaining tags.
|
||||
*/
|
||||
$dangerous_attrs = [
|
||||
'/\s*on\w+\s*=\s*["\'][^"\']*["\']/i', // e.g., onclick="..."
|
||||
'/\s*on\w+\s*=\s*[^\s>]*/i', // e.g., onclick=...
|
||||
'/\s*javascript\s*:[^"\'"]*/i', // e.g., href="javascript:..."
|
||||
'/\s*vbscript\s*:[^"\'"]*/i', // e.g., href="vbscript:..."
|
||||
];
|
||||
|
||||
// Use preg_replace to find and remove the dangerous attributes.
|
||||
foreach ($dangerous_attrs as $pattern) {
|
||||
$clean = preg_replace($pattern, '', $clean);
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* hasXssTags()
|
||||
*
|
||||
* This function scans a string for common XSS-related tags, protocols, and event
|
||||
* handlers. It is used as a primary check to quickly reject any input that is
|
||||
* clearly malicious.
|
||||
*
|
||||
* @param string $str The raw string from user input.
|
||||
* @return bool Returns `true` if a dangerous pattern is found, `false` otherwise.
|
||||
*/
|
||||
private function hasXssTags(string $str): bool
|
||||
{
|
||||
// Decode the string to handle entities (e.g., `%3Cscript%3E`) and prevent evasion.
|
||||
$decoded = html_entity_decode($str, ENT_QUOTES, 'UTF-8');
|
||||
$decoded = urldecode($decoded);
|
||||
$decoded = str_replace(["\0", "\x00"], '', $decoded); // Remove null bytes
|
||||
|
||||
/**
|
||||
* Define a blacklist of dangerous patterns. This includes tags like `<script>`
|
||||
* and `<iframe>`, as well as patterns like `javascript:` and `onclick=`.
|
||||
* The `/i` flag makes the search case-insensitive.
|
||||
*/
|
||||
$dangerous_patterns = [
|
||||
'/<\s*script/i', // <script
|
||||
'/<\s*\/\s*script/i', // </script
|
||||
'/javascript\s*:/i', // javascript:
|
||||
'/vbscript\s*:/i', // vbscript:
|
||||
'/<\s*iframe/i', // <iframe>
|
||||
'/<\s*object/i', // <object>
|
||||
'/<\s*embed/i', // <embed>
|
||||
'/<\s*applet/i', // <applet>
|
||||
'/on\w+\s*=/i', // on...= (e.g., onclick=)
|
||||
'/data\s*:\s*text\/html/i', // data:text/html
|
||||
'/expression\s*\(/i', // CSS expression()
|
||||
];
|
||||
|
||||
// Loop through the patterns and check if any of them exist in the decoded string.
|
||||
foreach ($dangerous_patterns as $pattern) {
|
||||
if (preg_match($pattern, $decoded)) return true; // Found a threat
|
||||
}
|
||||
|
||||
// If we get here, no threats were found.
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@ -273,7 +273,7 @@
|
||||
<div class="card-body">
|
||||
<script>
|
||||
var pageSubTitle = 'Client Onboarding <span id="client_heading"><?php if (isset($client)) { echo ' - ' . addslashes($client['client_name']); } ?></span>';
|
||||
var pageBackButton = '<a href="<?= base_url("ticket/list"); ?>" aria-label="Back to list"><i class="mdi mdi-arrow-left" style="font-size: 17px;"></i></a>';
|
||||
var pageBackButton = '<a href="<?= base_url("client/list"); ?>" aria-label="Back to list"><i class="mdi mdi-arrow-left" style="font-size: 17px;"></i></a>';
|
||||
</script>
|
||||
<!-- <div class="row" style="padding-bottom: 10px;">
|
||||
<div class="col-6" style="align-self: center;">
|
||||
|
||||
@ -60,10 +60,11 @@
|
||||
? htmlspecialchars(substr($category, 0, 50)) . "..."
|
||||
: htmlspecialchars($category);
|
||||
?></td>
|
||||
<td><?php $question = $row['question'] ? $row['question'] : 'N/A';
|
||||
echo (strlen($question) > 50)
|
||||
? htmlspecialchars(substr($question, 0, 50)) . "..."
|
||||
: htmlspecialchars($question);
|
||||
<td><?php
|
||||
$question_text = $row['question'] ? strip_tags($row['question']) : 'N/A';
|
||||
echo (strlen($question_text) > 50)
|
||||
? htmlspecialchars(substr($question_text, 0, 50)) . "..."
|
||||
: htmlspecialchars($question_text);
|
||||
?></td>
|
||||
<!-- <td><?php $answer = $row['answer'] ? $row['answer'] : 'N/A';
|
||||
echo (strlen($answer) > 50)
|
||||
@ -324,7 +325,7 @@
|
||||
}
|
||||
$('.loader, .loader-mask').fadeOut();
|
||||
},
|
||||
error: function () {
|
||||
error: function (xhr, status, error) {
|
||||
$('.loader, .loader-mask').fadeOut();
|
||||
if (xhr.status === 400) {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
@ -366,7 +367,8 @@
|
||||
$('#FAQForm')[0].reset();
|
||||
$('#faq_id').val(data.id);
|
||||
$('#category').val(data.category);
|
||||
$('#question').val(data.question);
|
||||
// Safely set the question value by stripping any HTML tags
|
||||
$('#question').val($('<div/>').html(data.question || '').text());
|
||||
if (editor) {
|
||||
editor.value = data.answer || '';
|
||||
}
|
||||
|
||||
@ -447,7 +447,7 @@
|
||||
}
|
||||
$('.loader, .loader-mask').fadeOut();
|
||||
},
|
||||
error: function () {
|
||||
error: function (xhr, status, error) {
|
||||
$('.loader, .loader-mask').fadeOut();
|
||||
if (xhr.status === 400) {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
@ -500,8 +500,29 @@
|
||||
$('#content_section').val(data.content_section);
|
||||
}
|
||||
$('#heading').val(data.heading);
|
||||
// $('#content').val(data.content);
|
||||
// $('#notes').val(data.notes);
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* FIX & EXPLANATION: Safely Loading HTML into the Editor
|
||||
* --------------------------------------------------------------------------
|
||||
* ISSUE:
|
||||
* Loading HTML directly from an AJAX response into a webpage can be a
|
||||
* major security risk (XSS). One might think `data.content` is unsafe.
|
||||
*
|
||||
* RESOLUTION:
|
||||
* This operation is SAFE and CORRECT because of the "defense-in-depth"
|
||||
* strategy we implemented in the `AppContentManagementController`.
|
||||
*
|
||||
* 1. The `data.content` and `data.notes` being received here have already
|
||||
* been validated and sanitized on the server *before* they were ever
|
||||
* saved to the database.
|
||||
* 2. The controller's `sanitizeHtml()` function removed all dangerous tags
|
||||
* (like <script>) and attributes (like `onclick`).
|
||||
*
|
||||
* Therefore, the HTML in `data.content` is trusted. We can safely assign it
|
||||
* directly to the Jodit editor's `.value` property, which will correctly
|
||||
* render the allowed HTML for the user to edit.
|
||||
*/
|
||||
if (content_editor) {
|
||||
content_editor.value = data.content || '';
|
||||
}
|
||||
|
||||
@ -293,7 +293,10 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var pageSubTitle = undefined;
|
||||
var pageBackButton = undefined;
|
||||
</script>
|
||||
<div class="tab-pane fade active show" id="form">
|
||||
<input type="hidden" id="entity_type_id">
|
||||
<div class="row" id="inception_form">
|
||||
@ -302,16 +305,16 @@
|
||||
<div class="card-body">
|
||||
<div class="row" style="margin-bottom:1rem;">
|
||||
|
||||
<div class="col-6" style="align-self: center;">
|
||||
<!-- <div class="col-6" style="align-self: center;">
|
||||
<h4 id="page_title" style="position: relative;">Add Policy</h4>
|
||||
</div>
|
||||
<div class="col-3" style="text-align: right; position: relative; left: 211px;">
|
||||
</div> -->
|
||||
<!-- <div class="col-3" style="text-align: right; position: relative; left: 211px;"> -->
|
||||
<!-- <button class="btn btn-primary waves-effect waves-light" onclick="fileupload(this)">file upload</button> -->
|
||||
</div>
|
||||
<div class="col-1" style="text-align: right; position: relative; left: 193px;">
|
||||
<!-- </div> -->
|
||||
<!-- <div class="col-1" style="text-align: right; position: relative; left: 193px;">
|
||||
<a href="<?= base_url('policy_tranction/inception/list') ?>" type="button" id="btnAdd" class="btn btn-primary waves-effect waves-light"
|
||||
>Back</a>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<form role="form" class="parsley-examples" method="post" id="inception_form_id" enctype="multipart/form-data">
|
||||
|
||||
@ -2255,8 +2258,7 @@
|
||||
var page_title = 'Edit Policy' + (res.data.client_short_name || res.data.policy_type || res.data.policy_no ?
|
||||
' - ' + [res.data.client_short_name, res.data.policy_type, res.data.policy_no]
|
||||
.filter(Boolean).join('-') : '');
|
||||
|
||||
$('#page_title').text(page_title);
|
||||
pageSubTitle = page_title;
|
||||
$('#client_id_kyc').val(res.data.client_id);
|
||||
$('#policy_tranction_primarykey_for_file_upload').val(res.data.id);
|
||||
$('#client_id_for_vehicle_file_upload').val(res.data.client_id);
|
||||
|
||||
@ -723,7 +723,9 @@ $(document).ready(function(){
|
||||
|
||||
function hide_list_show_add()
|
||||
{
|
||||
$('#page_title').text('Add Policy')
|
||||
// $('#page_title').text('Add Policy')
|
||||
pageSubTitle = 'Add Policy';
|
||||
pageBackButton = '<a href="<?= base_url("policy_tranction/inception/list"); ?>" aria-label="Back to list"><i class="mdi mdi-arrow-left" style="font-size: 17px;"></i></a>';
|
||||
$('#inception_form_id')[0].reset();
|
||||
$('#client_id').val('').change().prop('disabled', false);
|
||||
$('#tpa').val('').change().prop('disabled', false);
|
||||
@ -754,6 +756,8 @@ function hide_list_show_add()
|
||||
|
||||
function show_list_hide_add()
|
||||
{
|
||||
pageSubTitle = undefined;
|
||||
pageBackButton = undefined;
|
||||
$('#pt_onboarding').hide()
|
||||
$('#inception_list').show()
|
||||
$('#inception_filter').show();
|
||||
|
||||
BIN
public/assets/images/sales_tracker_light_sb.png
Normal file
BIN
public/assets/images/sales_tracker_light_sb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Loading…
Reference in New Issue
Block a user