Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1621dde
Convert state-changing actions from GET to POST
anonymoususer72041 Jan 6, 2026
9558f3c
fix: correct contact delete confirmation message
anonymoususer72041 Jan 6, 2026
9f73dd0
Provide GET launcher pages for installer AJAX maintenance endpoints
anonymoususer72041 Jan 6, 2026
51e3c07
Add session-based CSRF token helpers
anonymoususer72041 Jan 7, 2026
a15b654
Expose CSRF token to client and include in AJAX POSTs
anonymoususer72041 Jan 7, 2026
840bdd4
Enforce CSRF token for secure AJAX POST requests
anonymoususer72041 Jan 7, 2026
78dfacb
Enforce CSRF token for authenticated POST requests
anonymoususer72041 Jan 7, 2026
c8ee380
fix: include CSRF token in dynamic POST requests
anonymoususer72041 Jan 8, 2026
af1f1b9
fix: include CSRF token in auto-submitted POST forms
anonymoususer72041 Jan 8, 2026
c2b288f
fix: include CSRF token in test POST requests
anonymoususer72041 Jan 8, 2026
01f3c0f
Limit CSRF token leakage via Referrer-Policy
anonymoususer72041 Jan 8, 2026
fefc950
Add SameSite=Lax to session_cookie
anonymoususer72041 Jan 8, 2026
7ef847a
Merge branch 'opencats:master' into security/csrf-protection
anonymoususer72041 Jan 11, 2026
c6ec576
Fix job order delete Behat step
anonymoususer72041 Jan 11, 2026
46b04ef
Make logout submit without JavaScript
anonymoususer72041 Jan 12, 2026
170f50e
Fix GET_POST_requestsSecurity.feature for POST-only delete actions
anonymoususer72041 Jan 12, 2026
37b790f
Add unit tests for CSRF token handling
anonymoususer72041 Jan 15, 2026
a8e822a
Fix careers portal settings template POST action parameters
anonymoususer72041 Jan 25, 2026
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
5 changes: 5 additions & 0 deletions .htaccess
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
IndexIgnore *

Options -Indexes

# Security headers (requires mod_headers; AllowOverride FileInfo or All).
<IfModule mod_headers.c>
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
8 changes: 7 additions & 1 deletion ajax/deleteActivity.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@

$interface = new SecureAJAXInterface();

if ($_SERVER['REQUEST_METHOD'] !== 'POST')
{
$interface->outputXMLErrorPage(-1, 'Invalid request.');
die();
}

if (!$interface->isRequiredIDValid('activityID'))
{
$interface->outputXMLErrorPage(-1, 'Invalid activity ID.');
Expand All @@ -40,7 +46,7 @@

$siteID = $interface->getSiteID();

$activityID = $_REQUEST['activityID'];
$activityID = $_POST['activityID'];

/* Delete the activity entry. */
$activityEntries = new ActivityEntries($siteID);
Expand Down
24 changes: 15 additions & 9 deletions ajax/editActivity.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@

$interface = new SecureAJAXInterface();

if ($_SERVER['REQUEST_METHOD'] !== 'POST')
{
$interface->outputXMLErrorPage(-1, 'Invalid request.');
die();
}

if (!$interface->isRequiredIDValid('activityID'))
{
$interface->outputXMLErrorPage(-1, 'Invalid activity ID.');
Expand All @@ -53,24 +59,24 @@
die();
}

if (!isset($_REQUEST['notes']))
if (!isset($_POST['notes']))
{
$interface->outputXMLErrorPage(-1, 'Invalid notes.');
die();
}

$siteID = $interface->getSiteID();

$activityID = $_REQUEST['activityID'];
$type = $_REQUEST['type'];
$jobOrderID = $_REQUEST['jobOrderID'];
$activityID = $_POST['activityID'];
$type = $_POST['type'];
$jobOrderID = $_POST['jobOrderID'];

/* Decode and trim the activity notes from the company. */
$activityNote = trim(urldecode($_REQUEST['notes']));
$activityDate = trim(urldecode($_REQUEST['date']));
$activityHour = trim(urldecode($_REQUEST['hour']));
$activityMinute = trim(urldecode($_REQUEST['minute']));
$activityAMPM = trim(urldecode($_REQUEST['ampm']));
$activityNote = trim(urldecode($_POST['notes']));
$activityDate = trim(urldecode($_POST['date']));
$activityHour = trim(urldecode($_POST['hour']));
$activityMinute = trim(urldecode($_POST['minute']));
$activityAMPM = trim(urldecode($_POST['ampm']));

if (!DateUtility::validate('-', $activityDate, DATE_FORMAT_MMDDYY))
{
Expand Down
13 changes: 8 additions & 5 deletions ajax/getPipelineJobOrder.php
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,14 @@ function printSortLink($field, $delimiter = "'", $changeDirection = true)
<img src="images/actions/edit.gif" width="16" height="16" class="absmiddle" alt="" style="border: none;" title="Log an Activity / Change Status" />
</a>
<?php endif; ?>
<?php if ($_SESSION['CATS']->getAccessLevel('pipelines.removeFromPipeline') >= ACCESS_LEVEL_DELETE): ?>
<a href="<?php echo($indexFile); ?>?m=joborders&amp;a=removeFromPipeline&amp;jobOrderID=<?php echo($jobOrderID); ?>&amp;candidateID=<?php echo($pipelinesData['candidateID']); ?>" onclick="javascript:return confirm('Remove <?php echo(str_replace('\'', '\\\'', htmlspecialchars($pipelinesData['firstName']))); ?> <?php echo(str_replace('\'', '\\\'', htmlspecialchars($pipelinesData['lastName']))); ?> from the pipeline?')">
<img src="images/actions/delete.gif" width="16" height="16" class="absmiddle" alt="remove" style="border: none;" title="Remove from Job Order" />
</a>
<?php endif; ?>
<?php if ($_SESSION['CATS']->getAccessLevel('pipelines.removeFromPipeline') >= ACCESS_LEVEL_DELETE): ?>
<form method="post" action="<?php echo($indexFile); ?>?m=joborders&amp;a=removeFromPipeline" style="display:inline;" onsubmit="return confirm('Remove <?php echo(str_replace('\'', '\\\'', htmlspecialchars($pipelinesData['firstName']))); ?> <?php echo(str_replace('\'', '\\\'', htmlspecialchars($pipelinesData['lastName']))); ?> from the pipeline?')">
<input type="hidden" name="postback" value="postback" />
<input type="hidden" name="jobOrderID" value="<?php echo($jobOrderID); ?>" />
<input type="hidden" name="candidateID" value="<?php echo($pipelinesData['candidateID']); ?>" />
<input type="image" src="images/actions/delete.gif" width="16" height="16" class="absmiddle" alt="remove" style="border: none;" title="Remove from Job Order" />
</form>
<?php endif; ?>
<?php endif; ?>
</td>
<?php endif; ?>
Expand Down
12 changes: 9 additions & 3 deletions ajax/setCandidateJobOrderRating.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@

$interface = new SecureAJAXInterface();

if ($_SERVER['REQUEST_METHOD'] !== 'POST')
{
$interface->outputXMLErrorPage(-1, 'Invalid request.');
die();
}

if ($_SESSION['CATS']->getAccessLevel('pipelines.editRating') < ACCESS_LEVEL_EDIT)
{
$interface->outputXMLErrorPage(-1, ERROR_NO_PERMISSION);
Expand All @@ -45,16 +51,16 @@
}

if (!$interface->isRequiredIDValid('rating', true, true) ||
$_REQUEST['rating'] < -6 || $_REQUEST['rating'] > 5)
$_POST['rating'] < -6 || $_POST['rating'] > 5)
{
$interface->outputXMLErrorPage(-1, 'Invalid rating.');
die();
}

$siteID = $interface->getSiteID();

$candidateJobOrderID = $_REQUEST['candidateJobOrderID'];
$rating = $_REQUEST['rating'];
$candidateJobOrderID = $_POST['candidateJobOrderID'];
$rating = $_POST['rating'];

$pipelines = new Pipelines($siteID);
$pipelines->updateRatingValue($candidateJobOrderID, $rating);
Expand Down
18 changes: 15 additions & 3 deletions ajax/setColumnWidth.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,21 @@

$interface = new SecureAJAXInterface();

$instance = $_REQUEST['instance'];
$columnName = $_REQUEST['columnName'];
$columnWidth = $_REQUEST['columnWidth'];
if ($_SERVER['REQUEST_METHOD'] !== 'POST')
{
$interface->outputXMLErrorPage(-1, 'Invalid request.');
die();
}

if (!isset($_POST['instance']) || !isset($_POST['columnName']) || !isset($_POST['columnWidth']))
{
$interface->outputXMLErrorPage(-1, 'Invalid request.');
die();
}

$instance = $_POST['instance'];
$columnName = $_POST['columnName'];
$columnWidth = $_POST['columnWidth'];

$columnPreferences = $_SESSION['CATS']->getColumnPreferences($instance);

Expand Down
18 changes: 12 additions & 6 deletions ajax/testEmailSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,16 @@

$interface = new SecureAJAXInterface();

if ($_SERVER['REQUEST_METHOD'] !== 'POST')
{
$interface->outputXMLErrorPage(-1, 'Invalid request.');
die();
}

$siteID = $interface->getSiteID();

if (!isset($_REQUEST['testEmailAddress']) ||
empty($_REQUEST['testEmailAddress']))
if (!isset($_POST['testEmailAddress']) ||
empty($_POST['testEmailAddress']))
{
$interface->outputXMLErrorPage(
-1, 'Invalid test e-mail address.'
Expand All @@ -44,8 +50,8 @@
die();
}

if (!isset($_REQUEST['fromAddress']) ||
empty($_REQUEST['fromAddress']))
if (!isset($_POST['fromAddress']) ||
empty($_POST['fromAddress']))
{
$interface->outputXMLErrorPage(
-1, 'Invalid from e-mail address.'
Expand All @@ -54,8 +60,8 @@
die();
}

$testEmailAddress = $_REQUEST['testEmailAddress'];
$fromAddress = $_REQUEST['fromAddress'];
$testEmailAddress = $_POST['testEmailAddress'];
$fromAddress = $_POST['fromAddress'];

/* Is the test e-mail address specified valid? */
// FIXME: Validate properly.
Expand Down
43 changes: 41 additions & 2 deletions index.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,32 @@ function stripslashes_deep($value)
}
}

if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' &&
$_SESSION['CATS']->isLoggedIn() &&
(!isset($careerPage) || !$careerPage) &&
(!isset($_GET['showCareerPortal']) || $_GET['showCareerPortal'] != '1') &&
(!isset($rssPage) || !$rssPage) &&
(!isset($xmlPage) || !$xmlPage))
{
$module = isset($_GET['m']) ? $_GET['m'] : '';

if ($module == '' || $module == 'logout' ||
ModuleUtility::moduleRequiresAuthentication($module))
{
$token = null;

if (isset($_POST['csrfToken']))
{
$token = $_POST['csrfToken'];
}

if (!$_SESSION['CATS']->isCSRFTokenValid($token))
{
CommonErrors::fatal(COMMONERROR_BADFIELDS, null, 'Invalid request.');
}
}
}

/* Check to see if we are supposed to display the career page. */
if (((isset($careerPage) && $careerPage) ||
(isset($_GET['showCareerPortal']) && $_GET['showCareerPortal'] == '1')))
Expand Down Expand Up @@ -219,6 +245,11 @@ function stripslashes_deep($value)
{
if ($_GET['m'] == 'logout')
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST')
{
CommonErrors::fatal(COMMONERROR_BADFIELDS, null, 'Invalid request.');
}

/* There isn't really a logout module. It's just a few lines. */
$unixName = $_SESSION['CATS']->getUnixName();

Expand All @@ -233,12 +264,20 @@ function stripslashes_deep($value)
$URI .= '&s=' . $unixName;
}

if (isset($_GET['message']))
if (isset($_POST['message']))
{
$URI .= '&message=' . urlencode($_POST['message']);
}
else if (isset($_GET['message']))
{
$URI .= '&message=' . urlencode($_GET['message']);
}

if (isset($_GET['messageSuccess']))
if (isset($_POST['messageSuccess']))
{
$URI .= '&messageSuccess=' . urlencode($_POST['messageSuccess']);
}
else if (isset($_GET['messageSuccess']))
{
$URI .= '&messageSuccess=' . urlencode($_GET['messageSuccess']);
}
Expand Down
14 changes: 14 additions & 0 deletions js/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,20 @@ function AJAX_getPOSTSessionID(sessionCookie)
function AJAX_POST(http, url, POSTData, callBack, timeout, sessionCookie,
silentTimeout)
{
if (POSTData == null)
{
POSTData = '';
}

if (typeof CATSCsrfToken != 'undefined' && CATSCsrfToken !== null &&
CATSCsrfToken !== '')
{
if (POSTData.indexOf('csrfToken=') == -1)
{
POSTData += '&csrfToken=' + encodeURIComponent(CATSCsrfToken);
}
}

/* Add a random hash to the POST data to keep IE from caching it. */
POSTData += AJAX_getRandomPOSTHash();

Expand Down
30 changes: 28 additions & 2 deletions js/lists.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,34 @@ function deleteListFromListView(savedListID, numberEntries)
return;
}

document.location.href = CATSIndexName + '?m=lists&a=deleteStaticList&savedListID=' + savedListID;
var form = document.createElement('form');
form.method = 'post';
form.action = CATSIndexName + '?m=lists&a=deleteStaticList';

var postback = document.createElement('input');
postback.type = 'hidden';
postback.name = 'postback';
postback.value = 'postback';
form.appendChild(postback);

var savedListInput = document.createElement('input');
savedListInput.type = 'hidden';
savedListInput.name = 'savedListID';
savedListInput.value = savedListID;
form.appendChild(savedListInput);

if (typeof CATSCsrfToken != 'undefined' && CATSCsrfToken !== null &&
CATSCsrfToken !== '')
{
var csrfToken = document.createElement('input');
csrfToken.type = 'hidden';
csrfToken.name = 'csrfToken';
csrfToken.value = CATSCsrfToken;
form.appendChild(csrfToken);
}

document.body.appendChild(form);
form.submit();
}

function deleteListRow(savedListID, sessionCookie, numberEntries)
Expand Down Expand Up @@ -350,4 +377,3 @@ function addItemsToList(sessionCookie, dataItemType)
false
);
}

69 changes: 69 additions & 0 deletions js/quickAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,72 @@
/* Create a popup window for adding this candidate to the job order / pipeline */
showPopWin(CATSIndexName + '?m=candidates&a=considerForJobSearch&candidateID=' + menuDataItemId, 750, 390, null);
};

function quickActionPostFromUrl(url, confirmMessage)

Check warning on line 125 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L125

'quickActionPostFromUrl' is defined but never used.

Check warning on line 125 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L125

Function 'quickActionPostFromUrl' has a complexity of 17. Maximum allowed is 4.
{
if (confirmMessage && !confirm(confirmMessage))
{
return false;
}

var parts = url.split('?');

Check notice on line 132 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L132

Strings must use doublequote.
var action = parts[0];
var query = (parts.length > 1) ? parts[1] : '';

Check notice on line 134 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L134

Strings must use doublequote.
var params = {};
var actionParams = [];

if (query.length > 0)
{
var pairs = query.split('&');

Check notice on line 140 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L140

Strings must use doublequote.
for (var i = 0; i < pairs.length; i++)
{
if (pairs[i] === '')

Check notice on line 143 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L143

Strings must use doublequote.
{
continue;
}

var keyValue = pairs[i].split('=');

Check notice on line 148 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L148

Strings must use doublequote.
var key = decodeURIComponent(keyValue[0].replace(/\+/g, ' '));

Check notice on line 149 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L149

Strings must use doublequote.
var value = keyValue.length > 1 ? decodeURIComponent(keyValue[1].replace(/\+/g, ' ')) : '';

Check notice on line 150 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L150

Strings must use doublequote.

if (key === 'm' || key === 'a')

Check notice on line 152 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L152

Strings must use doublequote.
{
actionParams.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));

Check notice on line 154 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L154

Strings must use doublequote.
}
else
{
params[key] = value;
}
}
}

params.postback = 'postback';

Check notice on line 163 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L163

Strings must use doublequote.

if (typeof CATSCsrfToken != 'undefined' && CATSCsrfToken !== null &&

Check notice on line 165 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L165

Strings must use doublequote.
CATSCsrfToken !== '' && typeof params.csrfToken == 'undefined')

Check notice on line 166 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L166

Strings must use doublequote.
{
params.csrfToken = CATSCsrfToken;
}

var form = document.createElement('form');

Check notice on line 171 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L171

Strings must use doublequote.
form.method = 'post';

Check notice on line 172 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L172

Strings must use doublequote.
form.action = action + (actionParams.length ? ('?' + actionParams.join('&')) : '');

Check notice on line 173 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L173

Strings must use doublequote.

for (var name in params)
{
if (!params.hasOwnProperty(name))
{
continue;
}

var input = document.createElement('input');

Check notice on line 182 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L182

Strings must use doublequote.
input.type = 'hidden';

Check notice on line 183 in js/quickAction.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

js/quickAction.js#L183

Strings must use doublequote.
input.name = name;
input.value = params[name];
form.appendChild(input);
}

document.body.appendChild(form);
form.submit();
return false;
}
Loading
Loading