Compare commits

...

15 Commits

Author SHA1 Message Date
95537fb60e Extend MD_JAIL with .user_ini proposals for restricting maximum inputs 2020-11-12 19:54:43 +01:00
5130477e4b Add static function to propose security settings
Close #3, see #4
2020-11-12 00:12:11 +01:00
ae39bdf741 Disable currently unused function MD_JAIL->_apply_basedir_restrictions() 2020-11-11 17:29:03 +01:00
d7c89275e7 Merge branch 'master' of https://gitea.armuli.eu/museum-digital/MD_STD 2020-11-11 17:27:33 +01:00
2bfc7a0dcd Add CLI output option to MD_JAIL 2020-11-11 17:25:41 +01:00
6a6f71cf10 Add class MD_JAIL for forcing coders to set time and memory limits 2020-11-11 17:20:56 +01:00
8e3d97aa7f Move array_diff / array_values into different lines in MD_STD::scandir
This leads a significant reduction in RAM usage.
2020-11-09 14:17:54 +01:00
aa67de1e54 Add class MD_STD_SEC for basic security operations 2020-11-08 19:34:57 +01:00
50d3a20b01 Add type-safe drop-in replacement for mime_content_type() 2020-11-08 18:54:40 +01:00
cb8c786284 Add check to ensure finfo_open works in ensure_file function 2020-11-08 13:06:05 +01:00
306efa3769 Add .gitattributes, git template 2020-11-08 00:13:01 +01:00
1c86051997 Add a function to ensure a file exists, optionally checking the mime
type
2020-11-08 00:12:02 +01:00
2f68acdfc1 Make error messages for disallowed values more explicit 2020-10-24 12:46:18 +02:00
43bc39d425 Add function createTextSnippet() for shortening text to an expected
length

Close #1
2020-10-23 16:13:02 +02:00
711bd49048 Add function minizeHTMLString() 2020-10-21 21:16:18 +02:00
7 changed files with 458 additions and 3 deletions

32
.git.template Normal file
View File

@ -0,0 +1,32 @@
# If applied, this commit will ...
# Why was this change necessary? Improvements brought about by the
# change.
# End
# Format
# --------------------
# (If applied, this commit will...) <subject> (Max 72 char)
# |<---- Preferably using up to 50 chars --->|<------------------->|
# Example:
# Implement automated commit messages
# (Optional) Explain why this change is being made
# |<---- Try To Limit Each Line to a Maximum Of 72 Characters ---->|
# (Optional) Provide links or keys to any relevant tickets, articles or other resources
# Example: Github issue #23
# --- COMMIT END ---
#
# Remember to:
# * Capitalize the subject line
# * Use the imperative mood in the subject line
# * Do not end the subject line with a period
# * Separate subject from body with a blank line
# * Use the body to explain what and why vs. how
# * Can use multiple lines with "-" or "*" for bullet points in body
# --------------------
# Continuous integration messages

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
*.php text eol=lf diff=php
*.css text eol=lf diff=css

260
MD_JAIL.php Normal file
View File

@ -0,0 +1,260 @@
<?PHP
/**
* Provides class MD_JAIL.
*/
declare(strict_types = 1);
/**
* A class that, once initialized, forces the programmer to make security instructions implicit.
* If an object of the class has been created, not specifying security instructions
* leads to an error.
* A restriction on basic file operations is not practical in an md context because of
* the way transations are loaded through MDTlLoader.
*/
final class MD_JAIL {
const STATUS_NONE = 0;
const STATUS_STARTED = 1;
const STATUS_SPECIFIED = 2; // Determines that everything is fine.
/** @var integer */
private int $_status = self::STATUS_NONE;
/**
* @var integer
* Maximum execution time in seconds.
*/
public int $max_execution_time;
/**
* @var string[]
* Specifies which paths may be used by this script.
*/
private array $_open_basedir = [];
/**
* @var string
* Specifies the maximum RAM the script may use.
*/
public string $memory_limit;
/**
* Static function providing an advisory on how to harden the php.ini or
* .user.ini.
*
* @param array{shell_access_whitelist: string[], sys_function_whitelist: string[], file_function_whitelist: string[], file_uploads: bool, allow_url_fopen: bool, max_input_vars: integer, max_input_nesting_level: integer, post_max_size: string, curl: bool} $requested_resources Requested resources.
*
* @return string
*/
public static function check_server_setup(array $requested_resources):string {
# ini_set("open_basedir", __DIR__ . "/../../");
$disable_functions = [
"dl", # Loads a PHP extension at runtime
"opcache_get_status", # Gets opcache status
"phpinfo", # For obvious reasons
"parse_ini_file", # For obvious reasons
"show_source", "highlight_file", # Aliases; serve the content of a PHP file
"php_uname", # Returns information about the operating system PHP is running on
"phpcredits", # Credits page of PHP authors
"php_strip_whitespace", # Return PHP source with stripped comments and whitespace
"popen", "pclose", # Similar to fopen, but capable of shell access
"virtual", # Perform an Apache sub-request
];
$disable_aliases = [
"ini_set" => "ini_alter", # Alias of ini_set(), ini_set is preferred
"disk_free_space" => "diskfreespace", # Alias of disk_free_space()
"PHP_SAPI" => "php_sapi_name", # Alias of constant PHP_SAPI
"stream_set_write_buffer" => "set_file_buffer", # Alias of stream_set_write_buffer()
];
$shell_access_functions = array_diff(["exec", "passthru", "proc_close", "proc_get_status", "proc_nice", "proc_open", "proc_terminate", "shell_exec", "system"], $requested_resources['shell_access_whitelist']);
$sys_functions = array_diff(["get_current_user", "getmyuid", "getmygid", "getmypid", "getmyinode", "getlastmod", "getenv", "putenv"], $requested_resources['sys_function_whitelist']);
$file_functions = array_diff(["chgrp", "chgrp", "lchgrp", "lchown", "link", "linkinfo", "symlink"], $requested_resources['file_function_whitelist']);
if ($requested_resources['curl'] === false) {
$disable_functions[] = "curl_init";
$disable_functions[] = "curl_exec";
$disable_functions[] = "curl_multi_init";
$disable_functions[] = "curl_multi_exec";
}
$disabledWOAliases = array_merge($disable_functions, $shell_access_functions, $sys_functions, $file_functions);
$output = '## php.ini' . PHP_EOL . PHP_EOL;
$output .= PHP_EOL . "php_value[disable_functions] = \"" . implode(", ", array_merge($disabledWOAliases, $disable_aliases)) . "\"";
if ($requested_resources['file_uploads'] === false) {
$output .= PHP_EOL . "php_value[file_uploads] = 0";
}
if ($requested_resources['allow_url_fopen'] === false) {
$output .= PHP_EOL . "php_value[allow_url_fopen] = 0";
}
$output .= PHP_EOL . PHP_EOL . '## .user.ini' . PHP_EOL;
if ($requested_resources['file_uploads'] === false) {
$output .= PHP_EOL . "upload_max_filesize = 1";
}
if ($requested_resources['max_input_vars'] != ini_get("max_input_vars")) {
$output .= PHP_EOL . "max_input_vars = " . $requested_resources['max_input_vars'];
}
if ($requested_resources['max_input_nesting_level'] != ini_get("max_input_nesting_level")) {
$output .= PHP_EOL . "max_input_nesting_level = " . $requested_resources['max_input_nesting_level'];
}
if ($requested_resources['post_max_size'] != ini_get("post_max_size")) {
$output .= PHP_EOL . "post_max_size = " . $requested_resources['post_max_size'];
}
$output .= PHP_EOL . PHP_EOL . '## PHPStan Directives' . PHP_EOL . PHP_EOL . " disallowedFunctionCalls:" . PHP_EOL;
foreach ($disable_aliases as $to_keep => $disabled) {
$output .= '
-
function: \'' . $disabled . '()\'
message: \'use ' . $to_keep . ' instead\'';
}
foreach ($disabledWOAliases as $disabled) {
$output .= '
- function: \'' . $disabled . '()\'';
}
return $output;
}
/**
* Registers an additional accessible directory for open_basedir.
*
* @param string $dir Directory to register.
*
* @return void
*/
public function register_accessible_dir(string $dir):void {
$this->_open_basedir[] = $dir;
}
/**
* Applies the memory limit setting.
*
* @return void
*/
private function _apply_memory_limit():void {
if (!isset($this->memory_limit)) {
throw new MDJailSecurityOptionNotSetException("It has not been specified, which memory limit the script should hold. Set MD_JAIL->memory_limit = string.");
}
if (ini_set("memory_limit", $this->memory_limit) === false) {
throw new Exception('Failed to change memory_limit to ' . $this->memory_limit);
}
}
/**
* Applies the maximum execution time setting.
*
* @return void
*/
private function _apply_time_limit():void {
if (!isset($this->max_execution_time)) {
throw new MDJailSecurityOptionNotSetException("It has not been specified, which maximum execution time the script should hold. Set MD_JAIL->max_execution_time = integer.");
}
if (set_time_limit($this->max_execution_time) === false) {
throw new Exception('Failed to change max_execution_time to ' . $this->max_execution_time);
}
}
/**
* Applies basedir restrictions.
*
* @return void
*/
private function _apply_basedir_restriction():void {
if (empty($this->_open_basedir)) {
throw new MDJailSecurityOptionNotSetException("It has not been specified, which memory limit the script should hold. Set MD_JAIL->open_basedir = string.");
}
if (ini_set("open_basedir", implode(':', $this->_open_basedir)) === false) {
throw new Exception('Failed to set open_basedir restrictions');
}
}
/**
* Enforces security options previously set.
*
* @return void
*/
public function enforce():void {
// Special instructions on CLI, so as to not disturb PHPUnit
if (PHP_SAPI === 'cli') {
if (!isset($this->memory_limit)) {
throw new MDJailSecurityOptionNotSetException("It has not been specified, which memory limit the script should hold. Set MD_JAIL->memory_limit = string.");
}
if (!isset($this->max_execution_time)) {
throw new MDJailSecurityOptionNotSetException("It has not been specified, which maximum execution time the script should hold. Set MD_JAIL->max_execution_time = integer.");
}
$this->_status = self::STATUS_SPECIFIED;
$this->__destruct();
}
$this->_apply_memory_limit();
$this->_apply_time_limit();
// Set accessible file paths
$this->_apply_basedir_restriction();
$this->_status = self::STATUS_SPECIFIED;
$this->__destruct();
}
/**
* Setup function. Registers a shutdown function that throws an error
* if the security specifications have not been made.
*
* @return void
*/
public function __construct() {
$this->_status = self::STATUS_STARTED;
}
public function __destruct() {
if ($this->_status !== self::STATUS_SPECIFIED) {
echo "Security specifications need to be set.";
if (!isset($this->memory_limit)) {
echo "Set memory limit";
}
if (!isset($this->max_execution_time)) {
echo "Set max_execution_time";
}
if (empty($this->_open_basedir)) {
echo "Set open_basedir";
}
throw new MDJailSecurityOptionNotSetException("Security specifications need to be set.");
}
}
}

View File

@ -63,7 +63,13 @@ final class MD_STD {
throw new MDFileDoesNotExist("There is no file {$filepath}");
}
return \array_values(\array_diff($output, ['.', '..', '.git']));
// Remove unwanted files from list
$output = \array_diff($output, ['.', '..', '.git']);
// Return array values, to make it a list rather than an associative array.
// This should be done in a separate line, as it observably leads to a
// significant reduction in the used RAM.
return \array_values($output);
}
@ -340,4 +346,90 @@ final class MD_STD {
}
/**
* Function for minimizing HTML, trimming each line.
*
* @param string $input Input.
*
* @return string
*/
public static function minimizeHTMLString(string $input):string {
$input = \explode(PHP_EOL, $input);
$output = "";
foreach ($input as $line) $output .= \trim($line) . PHP_EOL;
return $output;
}
/**
* This function cuts down a string and adds a period in case it's longer
* than length to create a snippet.
*
* @param string $string Input text to cut down.
* @param integer $length Length of the snippet to create.
*
* @return string
*/
public static function createTextSnippet(string $string, int $length = 180):string {
if (\mb_strlen($string) > $length) {
$string = \mb_substr($string, 0, $length);
if (($lastWhitespace = \mb_strrpos($string, ' ')) !== false) {
$string = \mb_substr($string, 0, $lastWhitespace);
}
$string .= '...';
}
return $string;
}
/**
* Wrapper around the finfo functions to get the mime content type of a file.
*
* @param string $filepath Expected file path.
*
* @return string
*/
public static function mime_content_type(string $filepath):string {
if (!($finfo = \finfo_open(FILEINFO_MIME_TYPE))) {
throw new Exception("Cannot open finfo context");
}
if (!($mime_type = finfo_file($finfo, $filepath))) {
throw new MDWrongFileType("Cannot get mime type of file: " . basename($filepath));
}
\finfo_close($finfo);
return $mime_type;
}
/**
* Checks if a file exists, with one of the expected mime types.
*
* @param string $filepath File path of the file that needs to exist.
* @param string[] $accepted_mimetype Mime type the file should have.
*
* @return void
*/
public static function ensure_file(string $filepath, array $accepted_mimetype = []) {
if (!\file_exists($filepath)) {
throw new MDFileDoesNotExist("File " . basename($filepath) . " does not exist");
}
// Check for mime type follows. If no check is to be done, ignore this.
if (empty($accepted_mimetype)) {
return;
}
$mime_type = self::mime_content_type($filepath);
if (!\in_array($mime_type, $accepted_mimetype, true)) {
throw new MDWrongFileType("Incorrect mime type of file " . \basename($filepath) . ". Mime type is " . \mime_content_type($filepath) . ", accepted any of ['" . \implode("', '", $accepted_mimetype) . "']");
}
}
}

View File

@ -107,7 +107,7 @@ final class MD_STD_IN {
else $output = self::sanitize_text($default);
if (!empty($allowed) and !\in_array($output, $allowed, true)) {
Throw new MDpageParameterNotFromListException("Parameter `{$var_name}` must be any of the allowed values: " . implode(', ', $allowed));
Throw new MDpageParameterNotFromListException("Parameter `{$var_name}` must be any of the allowed values: '" . implode('\', \'', $allowed) . "'");
}
return $output;
@ -132,7 +132,7 @@ final class MD_STD_IN {
else $output = self::sanitize_text($default);
if (!empty($allowed) and !\in_array($output, $allowed, true)) {
Throw new MDpageParameterNotFromListException("Parameter `{$var_name}` must be any of the allowed values: " . implode(', ', $allowed));
Throw new MDpageParameterNotFromListException("Parameter `{$var_name}` must be any of the allowed values: '" . implode('\', \'', $allowed) . "'");
}
return $output;

48
MD_STD_SEC.php Normal file
View File

@ -0,0 +1,48 @@
<?PHP
/**
* Gathers wrappers for handling basic security operations.
*/
declare(strict_types = 1);
/**
* Class providing static functions with basic security operations.
*/
final class MD_STD_SEC {
/**
* Function for retrieving the anti-csrf token or generating it if need be.
*
* @return string
*/
public static function getAntiCsrfToken():string {
if (empty($_SESSION['csrf-token'])) {
$_SESSION['csrf-token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf-token'];
}
/**
* Function for validating anti-csrf tokens. Each anti-csrf token is removed
* after use.
*
* @return boolean
*/
public static function validateAntiCsrfToken():bool {
$validity = false;
if (!empty($_POST['csrf-token'])
&& !empty($_SESSION['csrf-token'])
&& hash_equals($_SESSION['csrf-token'], $_POST['csrf-token']) === true
) {
$validity = true;
}
$_SESSION['csrf-token'] = null; unset($_SESSION['csrf-token']);
return $validity;
}
}

View File

@ -0,0 +1,21 @@
<?PHP
declare(strict_types = 1);
/**
* Exception thrown by MDJail if a required security option has not been set.
*/
final class MDJailSecurityOptionNotSetException extends Exception {
/**
* Error message.
*
* @return string
*/
public function errorMessage() {
//error message
$errorMsg = 'A security option of MD_JAIL has not been set: <b>' . $this->getMessage() . '</b>).';
return $errorMsg;
}
}