FIX_FAQ_and_FE_Content

This commit is contained in:
sanjeev.p 2026-03-12 15:25:56 +05:30
parent 5515499403
commit fc7941ed26
7 changed files with 265 additions and 46 deletions

View File

@ -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;
}
}

View File

@ -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;">

View File

@ -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 || '';
}

View File

@ -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 || '';
}

View File

@ -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);

View File

@ -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();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB