Commit 99453749 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Autorization for REST requests

parent f23f0c7d
......@@ -7680,6 +7680,8 @@ Form.parameter:
There are no `special-column-names`_ available in 'restSqlData' or 'restSqlList'. Especially there are no
SIPs possible, cause REST typically does not offer sessions/cookies which are needed for SIPs.
Autorizatioin
.. _applicationTest:
Application Test
......
......@@ -18,6 +18,8 @@ require_once(__DIR__ . '/../core/exceptions/DbException.php');
$restId=array();
$restForm=array();
$status='HTTP/1.0 409 Bad Request';
try {
try {
$form = OnString::splitPathInfoToIdForm($_SERVER['PATH_INFO'], $restId, $restForm);
......@@ -29,6 +31,7 @@ try {
$qfq = new QuickFormQuery(['bodytext' => $bodytext]);
$answer = $qfq->rest($restId, $restForm);
$status='HTTP/1.0 200 OK';
} catch (qfq\CodeException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
......@@ -43,6 +46,8 @@ try {
} catch (\Exception $e) {
$answer[API_MESSAGE] = "Generic Exception: " . $e->getMessage();
}
header($status);
header("Content-Type: application/json");
echo json_encode($answer);
......@@ -227,7 +227,6 @@ const ERROR_SMALLER_THAN_MIN = 1083;
const ERROR_LARGER_THAN_MAX = 1084;
const ERROR_INVALID_DECIMAL_FORMAT = 1085;
const ERROR_INVALID_DATE = 1086;
const ERROR_FORM_REST = 1087;
// Subrecord
const ERROR_SUBRECORD_MISSING_COLUMN_ID = 1100;
......@@ -363,6 +362,9 @@ const ERROR_FORM_RESERVED_NAME = 2800;
const ERROR_IMPORT_MISSING_EXPLICIT_TYPE = 2900;
const ERROR_IMPORT_LIST_SHEET_NAMES = 2901;
// REST
const ERROR_FORM_REST = 3000;
const ERROR_REST_AUTHORIZATION = 3001;
//
// Store Names: Identifier
//
......@@ -541,6 +543,7 @@ const SYSTEM_SECURITY_ATTACK_DELAY_DEFAULT = 5; // Detected attack causes x seco
const SYSTEM_SECURITY_SHOW_MESSAGE = 'securityShowMessage'; // Detected attack shows an error message
const SYSTEM_SECURITY_GET_MAX_LENGTH = 'securityGetMaxLength'; // Trim every character (before conversion) to SECURITY_GET_MAX_LENGTH chars;
const SYSTEM_SECURITY_GET_MAX_LENGTH_DEFAULT = 50; // Default max length for get variables
const SYSTEM_SECURITY_FAILED_AUTH_DELAY = 'securityFailedAuthDelay'; // Failed auth causes x seconds delay
const GET_EXTRA_LENGTH_TOKEN = '_';
......@@ -991,6 +994,7 @@ const F_SHOW_ID_IN_FORM_TITLE = SYSTEM_SHOW_ID_IN_FORM_TITLE;
const F_REST_SQL_LIST = 'restSqlList';
const F_REST_SQL_DATA = 'restSqlData';
const F_REST_PARAM = 'restParam';
const F_REST_TOKEN = 'restToken';
const CLIENT_REST_ID = '_id';
const CLIENT_REST_FORM = '_form';
......@@ -1745,4 +1749,8 @@ const DND_SUBRECORD_FORM_ID = 'dnd-subrecord-form-id';
const DND_ORD_HTML_ID_PREFIX = 'qfq-dnd-ord-id-';
// Application Test: SELENIUM
const ATTRIBUTE_DATA_REFERENCE = 'data-reference';
\ No newline at end of file
const ATTRIBUTE_DATA_REFERENCE = 'data-reference';
// REST
const HTTP_HEADER_AUTHORIZATION = 'Authorization';
......@@ -365,9 +365,10 @@ class QuickFormQuery {
}
}
// Session Expire happens quite late, cause it can be configured per form.
// Check 'session expire' happens quite late, cause it can be configured per form.
Session::checkSessionExpired($this->formSpec[F_SESSION_TIMEOUT_SECONDS]);
if ($formName !== false) {
// Validate only if there is a 'real' form (not a FORM_DELETE with only a tablename).
$sipFound = $this->validateForm($foundInStore, $formMode);
......@@ -604,7 +605,7 @@ class QuickFormQuery {
*/
private function doRestGet() {
$this->copyGenericRestParamToNamed();
$this->nameGenericRestParam();
$r = $this->store::getVar(TYPO3_RECORD_ID, STORE_TYPO3);
$key = empty($r) ? F_REST_SQL_LIST : F_REST_SQL_DATA;
......@@ -617,6 +618,32 @@ class QuickFormQuery {
}
/**
* Checks if $serverToken matches HTTP_HEADER_AUTHORIZATION,
* If not: throw an exception.
*
* @param string|array $serverToken
* @throws CodeException
* @throws UserFormException
*/
private function restCheckAuthToken($serverToken) {
// No serverToken: no check necessary
if ($serverToken === '') {
return;
}
if ($serverToken === $this->store::getVar(HTTP_HEADER_AUTHORIZATION, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALL)) {
return;
}
// Delay before answering.
$seconds = $this->store::getVar(SYSTEM_SECURITY_FAILED_AUTH_DELAY, STORE_SYSTEM);
sleep($seconds);
throw new UserFormException('Missing or wrong authorization token', ERROR_REST_AUTHORIZATION);
}
/**
* STORE_CLIENT: copy parameter _id1,_id2,...,_idN to named variables, specified via $this->formSpec[F_REST_PARAM] (CSV list)
*
......@@ -624,7 +651,7 @@ class QuickFormQuery {
* @throws UserFormException
* @throws UserReportException
*/
private function copyGenericRestParamToNamed() {
private function nameGenericRestParam() {
$paramNames = explode(',', $this->formSpec[F_REST_PARAM] ?? '');
......@@ -991,8 +1018,17 @@ class QuickFormQuery {
// and for evaluating variables in the Form title
$this->store->fillStoreWithRecord($form[F_TABLE_NAME], $recordId, $this->dbArray[$this->dbIndexData], $form[F_PRIMARY_KEY]);
// In case $form[F_REST_TOKEN] is a query which results to an empty answer; every token will fail.
$flagRestToken = !empty($form[F_REST_TOKEN]);
// Evaluate all fields
$formSpec = $this->evaluate->parseArray($form);
// If it is empty, set it to true to force the TOKEN check (which will always fail)
if ($flagRestToken && $form[F_REST_TOKEN] == '') {
$form[F_REST_TOKEN] = true;
}
$parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);
$formSpec = HelperFormElement::setLanguage($formSpec, $parameterLanguageFieldName);
......@@ -1371,7 +1407,7 @@ class QuickFormQuery {
}
/**
* Check if loading of the given form is permitted. If not, throw an exception.
* Check if the form loading is permitted. If not, throw an exception.
*
* @param string $formNameFoundInStore
* @param string $formMode
......@@ -1436,8 +1472,13 @@ class QuickFormQuery {
throw new CodeException("Unknown permission mode: '" . $permitMode . "'", ERROR_FORM_UNKNOWN_PERMISSION_MODE);
}
if ($formMode == FORM_REST && $permitMode != FORM_PERMISSION_REST) {
throw new UserFormException("Try to load a non-REST form in REST mode", ERROR_FORM_REST);
if ($formMode == FORM_REST) {
$this->restCheckAuthToken($this->formSpec[F_REST_TOKEN] ?? '');
if ($permitMode != FORM_PERMISSION_REST) {
throw new UserFormException("Try to load a non-REST form in REST mode", ERROR_FORM_REST);
}
}
// Form Definition valid?
......@@ -1449,7 +1490,7 @@ class QuickFormQuery {
return $sipFound;
}
$sipArray = $this->store->getStore(STORE_SIP);
// Check: requiredParameter: '' or 'form' or 'form,grId' or 'form #formname for form,grId'
// Check: requiredParameter: '' or 'form' or 'form,grId' or 'form #formname for form,grId'
$requiredParameter = ($r > 0) ? $this->formSpec[F_REQUIRED_PARAMETER_EDIT] : $this->formSpec[F_REQUIRED_PARAMETER_NEW];
if (trim($requiredParameter) == '') {
......
......@@ -34,6 +34,8 @@ class Client {
Sanitize::digitCheckAndCleanGet(CLIENT_PAGE_TYPE);
Sanitize::digitCheckAndCleanGet(CLIENT_PAGE_LANGUAGE);
$header = self::getHeader();
if (isset($_GET)) {
$get = $_GET; // do not use urldecode() - http://php.net/manual/de/function.urldecode.php#refsect1-function.urldecode-notes
}
......@@ -57,8 +59,39 @@ class Client {
$server[CLIENT_REMOTE_ADDRESS] = '0.0.0.0';
}
$arr = array_merge($get, $post, $cookie, $server);
$arr = array_merge($header, $get, $post, $cookie, $server);
return Sanitize::normalize($arr);
}
/**
* @return array
*/
private static function getHeader() {
$arr = array();
// getallheaders() does not exist for phpunit tests
if (!function_exists('getallheaders')) {
return array();
}
$headers = getallheaders();
foreach ([HTTP_HEADER_AUTHORIZATION] as $key) {
if (isset($headers[$key])) {
$line = $headers[$key];
$delimiter = (strpos($line, '=') === false) ? ':' : '=';
// Header: 'Authorization: Token token=1234'
$split = explode($delimiter, $line, 2);
if (isset($split[1])) {
$arr[$key] = OnString::trimQuote($split[1]);
}
}
}
return $arr;
}
}
\ No newline at end of file
......@@ -363,6 +363,8 @@ class Config {
SYSTEM_FLAG_PRODUCTION => 'yes',
SYSTEM_THROW_GENERAL_ERROR => 'auto',
SYSTEM_SECURITY_FAILED_AUTH_DELAY => '3',
];
// To let run legacy code
......
......@@ -10,6 +10,8 @@ namespace qfq;
use PHPUnit\Framework\TestCase;
#require_once(__DIR__ . '/../../../../Source/core/Constants.php');
/**
* Class StoreTest
* @package qfq
......@@ -31,7 +33,6 @@ class StoreTest extends TestCase {
public function setUp() {
// Client Variables has to setup before the first instantiation of 'Store'
$_GET[CLIENT_RECORD_ID] = '1234';
// $_GET[CLIENT_SIP] = '12badcaffee34';
$_GET['key01'] = '1234';
$_POST['key02'] = '2345';
$_POST['key03'] = '3456';
......@@ -400,8 +401,9 @@ class StoreTest extends TestCase {
'LDAP_1_RDN' => 'LDAP_1_RDN',
'LDAP_1_PASSWORD' => 'LDAP_1_PASSWORD',
SYSTEM_LABEL_ALIGN => SYSTEM_LABEL_ALIGN_LEFT,
F_FE_DATA_PATTERN_ERROR=> F_FE_DATA_PATTERN_ERROR_DEFAULT,
F_FE_DATA_PATTERN_ERROR_SYSTEM=> F_FE_DATA_PATTERN_ERROR_DEFAULT,
F_FE_DATA_PATTERN_ERROR => F_FE_DATA_PATTERN_ERROR_DEFAULT,
F_FE_DATA_PATTERN_ERROR_SYSTEM => F_FE_DATA_PATTERN_ERROR_DEFAULT,
SYSTEM_SECURITY_FAILED_AUTH_DELAY => '3',
];
$body = <<< EOT
......@@ -439,7 +441,7 @@ EOT;
# The following won't be checked by content, cause they will change on different installations.
// foreach ([ SYSTEM_SQL_LOG, SYSTEM_MAIL_LOG, SYSTEM_SITE_PATH, SYSTEM_EXT_PATH, SYSTEM_SEND_E_MAIL] as $key) {
foreach ([SYSTEM_SITE_PATH, SYSTEM_EXT_PATH, SYSTEM_SEND_E_MAIL, SYSTEM_SESSION_TIMEOUT_SECONDS] as $key) {
$this->assertTrue(isset($config[$key]), "Missing default value for '$key' " );
$this->assertTrue(isset($config[$key]), "Missing default value for '$key' ");
unset ($config[$key]);
}
// check default values
......
{
"require": {
"phpoffice/phpspreadsheet": "^1.3",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^6.5"
},
"autoload": {
"psr-4": {
"qfq\\": ["qfq/",
"Source/api/",
"Source/external/",
"Source/core/",
"Source/core/database/",
"Source/core/exceptions/",
"Source/core/form/",
"Source/core/helper/",
"Source/core/report/",
"Source/core/store/",
"Source/core/typo3/"]
}
}
}
......@@ -103,6 +103,9 @@ securityShowMessage = true
# cat=security/security; type=string; label='GET'-Parameter max length:Default is '50'. GET vars longer than 'x' character triggers an `attack-detected`.
securityGetMaxLength = 50
# cat=security/security; type=string; label=Failed auth delay in seconds:Default is '3'.
securityFailedAuthDelay = 3
# cat=security/security; type=string; label=Session Timeout in seconds:Default is empty to take the php.ini system value (minimum of 'session.cookie_lifetime' and 'session.gc_maxlifetime').
sessionTimeoutSeconds =
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment