Compare commits

...

28 Commits

Author SHA1 Message Date
8aa9d94acf Remove inline if clauses 2021-02-06 20:08:37 +01:00
217e1fc86b Consistently use "null" over "NULL" 2021-02-06 19:55:54 +01:00
605fd88b6e Use new line for unsetting variable 2021-02-06 17:35:11 +01:00
7a252c6bfa Improve sanitization of color inputs 2021-01-30 22:56:00 +01:00
89e06769f1 Add class for validating color codes 2021-01-28 21:47:59 +01:00
9d4d326d6a Add check for directory existence in MD_STD::mkdir 2021-01-21 11:03:07 +01:00
298e2238a8 Add stricter wrapper around unlink() 2021-01-06 12:49:35 +01:00
d28c245a1a Add wrapper around mkdir, that throws an exception on errors 2020-12-21 14:50:28 +01:00
2b4abf6338 Add function for running multiple curl queries simultaneously 2020-12-14 02:01:53 +01:00
34c2d57e5b Add function for converting strings to color codes 2020-12-11 14:01:41 +01:00
287fb02f8c Pipe STDOUT to /dev/null in MD_STD::exec_edit, actual STDOUT is for
STDERR
2020-12-09 13:34:31 +01:00
d028ac0176 Remove check for curl init working 2020-12-08 20:42:54 +01:00
ddab52b1a5 Add check against curl_init failure in runCurl 2020-12-08 11:36:54 +01:00
ada82f07b6 Reduce multipliers in brute force protection 2020-12-07 15:31:36 +01:00
cad5b4a6f8 Add missing return statement to disable MD_JAIL::enforce on CLI usage 2020-12-06 17:06:43 +01:00
6a7f91ef1d Use shell_exec in exec_edit 2020-12-05 20:48:51 +01:00
6db2b4cc1f Add MD_STD::exec_edit to run edit and pipe STDERR to a php exception 2020-12-04 21:33:11 +01:00
4c5097701f Add wrapper around levenstein that crops strings to the max allowed
length
2020-12-03 12:39:47 +01:00
886acead63 Stop using cache in MD_STD_CACHE when run from command line 2020-12-02 09:39:43 +01:00
35c0fe4723 Require cached contents in MD_STD_CACHE to be 3 chars long
An empty json array is 2 chars long
2020-12-01 00:05:59 +01:00
a38c3c6fae Let serve_page_through_redis_cache return string 2020-11-30 22:36:17 +01:00
57da808a6a Fix class variable comment 2020-11-30 19:19:44 +01:00
558ed729dc Add class MD_STD_CACHE 2020-11-30 19:08:20 +01:00
14c7ffb8d4 Fix class comment 2020-11-23 14:06:03 +01:00
a16619b78e Add option to set frame-ancestors CSP 2020-11-22 23:27:54 +01:00
90997e4eb5 Add function for sending complete CSP headers 2020-11-22 17:45:07 +01:00
c60932088d Add missing function comment 2020-11-22 15:42:56 +01:00
258781307d Fix reference to incorrect array part in MD_STD_SEC's brute force
protection
2020-11-22 14:18:08 +01:00
6 changed files with 383 additions and 46 deletions

View File

@ -5,7 +5,7 @@
declare(strict_types = 1);
/**
* A class that, once initialized, forces the programmer to make security instructions implicit.
* A class that, once initialized, forces the programmer to make security instructions explicit.
* 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
@ -178,7 +178,7 @@ final class MD_JAIL {
*
* @return void
*/
private function _apply_basedir_restriction():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.");
@ -187,7 +187,7 @@ final class MD_JAIL {
throw new Exception('Failed to set open_basedir restrictions');
}
}
}
/**
* Enforces security options previously set.
@ -208,6 +208,7 @@ final class MD_JAIL {
$this->_status = self::STATUS_SPECIFIED;
$this->__destruct();
return;
}
$this->_apply_memory_limit();
@ -233,6 +234,9 @@ final class MD_JAIL {
}
/**
* Destructor. Throws an exception if the settings have not been set.
*/
public function __destruct() {
if ($this->_status !== self::STATUS_SPECIFIED) {
@ -255,6 +259,5 @@ final class MD_JAIL {
}
}
}

View File

@ -44,11 +44,56 @@ final class MD_STD {
public static function realpath(string $path):string {
$output = \realpath($path);
if (!\is_string($output)) throw new MDFileDoesNotExist("The file {$path} does not exist or is not readable.");
if (!\is_string($output)) {
throw new MDFileDoesNotExist("The file {$path} does not exist or is not readable.");
}
return $output;
}
/**
* Wrapper around mkdir, that throws an exception, if the folder cannot be
* created.
*
* @see https://www.php.net/manual/en/function.mkdir.php
*
* @param string $pathname The directory path.
* @param integer $mode Permissions.
* @param boolean $recursive Allows the creation of nested directories
* specified in the pathname.
*
* @return void
*/
public static function mkdir(string $pathname, int $mode = 0775, bool $recursive = false):void {
// One reason, to throw an error, is that the folder already exists.
if (\is_dir($pathname)) {
return;
}
if (\mkdir($pathname, $mode, $recursive) === false) {
throw new Exception("Failed to create directory: $pathname");
}
}
/**
* Wrapper around unlink, that throws an exception if the file failed to be
* removed.
*
* @see https://www.php.net/manual/en/function.unlink.php
*
* @param string $filename File path.
*
* @return void
*/
public static function unlink(string $filename):void {
if (\unlink($filename) === false) {
throw new Exception("Failed to delete: $filename");
}
}
/**
* Gets contents of a folder.
*
@ -81,7 +126,9 @@ final class MD_STD {
public static function ob_get_clean():string {
$output = \ob_get_clean();
if ($output === false) throw new MDOutputBufferNotStarted("Output buffer was not started");
if ($output === false) {
throw new MDOutputBufferNotStarted("Output buffer was not started");
}
return $output;
}
@ -96,8 +143,12 @@ final class MD_STD {
*/
public static function startsWith(string $haystack, string $needle):bool {
if (substr($haystack, 0, \strlen($needle)) == $needle) return true;
else return false;
if (substr($haystack, 0, \strlen($needle)) === $needle) {
return true;
}
else {
return false;
}
}
@ -114,7 +165,9 @@ final class MD_STD {
$output = false;
foreach ($needles as $needle) {
$output = self::startsWith($haystack, $needle);
if ($output == true) return $output;
if ($output == true) {
return $output;
}
}
return $output;
@ -154,7 +207,9 @@ final class MD_STD {
public static function json_encode(array $value, int $options = 0, int $depth = 512):string {
$output = \json_encode($value, $options, $depth);
if ($output === false) throw new Exception("JSON output could not be generated");
if ($output === false) {
throw new Exception("JSON output could not be generated");
}
return $output;
}
@ -169,11 +224,45 @@ final class MD_STD {
public static function strtotime(string $datetime):int {
$output = \strtotime($datetime);
if ($output === false) throw new MDInvalidInputDate("Invalid input date {$datetime}.");
if ($output === false) {
throw new MDInvalidInputDate("Invalid input date {$datetime}.");
}
return $output;
}
/**
* Initializes a curl request with the given presets.
*
* @param string $url URL to query.
* @param integer $timeout Timeout in milliseconds.
*
* @return resource
*/
public static function curl_init(string $url, int $timeout) {
$curl = \curl_init();
\curl_setopt($curl, CURLOPT_URL, $url);
\curl_setopt($curl, CURLOPT_HEADER, false);
\curl_setopt($curl, CURLOPT_CONNECTTIMEOUT_MS, $timeout); //timeout in seconds
\curl_setopt($curl, CURLOPT_TIMEOUT_MS, $timeout); //timeout in seconds
\curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
// \curl_setopt($curl, CURLOPT_COOKIESESSION, true);
\curl_setopt($curl, CURLOPT_AUTOREFERER, true);
\curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:10.0.2) Gecko/20100101 Firefox/10.0.2');
/*
if (!file_exists(__DIR__ . '/../../curled.txt')) {
touch (__DIR__ . '/../../curled.txt');
}
file_put_contents(__DIR__ . '/../../curled.txt', $url . PHP_EOL, FILE_APPEND);
*/
return $curl;
}
/**
* Wrapper for curling contents from the web.
*
@ -184,28 +273,67 @@ final class MD_STD {
*/
public static function runCurl(string $url, int $timeout = 1200):string {
$curl = \curl_init();
\curl_setopt($curl, CURLOPT_URL, $url);
\curl_setopt($curl, CURLOPT_HEADER, false);
\curl_setopt($curl, CURLOPT_CONNECTTIMEOUT_MS, $timeout); //timeout in seconds
\curl_setopt($curl, CURLOPT_TIMEOUT_MS, $timeout); //timeout in seconds
$curl = self::curl_init($url, $timeout);
\curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
// \curl_setopt($curl, CURLOPT_COOKIESESSION, true);
\curl_setopt($curl, CURLOPT_AUTOREFERER, true);
\curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:10.0.2) Gecko/20100101 Firefox/10.0.2');
$result = \curl_exec($curl);
// if ($err = curl_errno($curl)) echo $err;
// if ($errmsg = curl_error($curl)) echo $errmsg;
\curl_close($curl);
if (\is_bool($result)) return "";
if (\is_bool($result)) {
return "";
}
return $result;
}
/**
* Wrapper for curling multiple pages from the web at ones and returning their contents.
* Adapted from hushuilong's comment at https://www.php.net/manual/de/function.curl-multi-init.php#105252.
*
* @param array<string> $urls URL to query.
* @param integer $timeout Timeout in milliseconds.
*
* @return array<string>
*/
public static function runCurlMulti(array $urls, int $timeout = 1200):array {
if (!($mh = curl_multi_init())) {
throw new exception("Failed to set up multi handle");
}
$curl_array = [];
foreach($urls as $i => $url) {
$curl_array[$i] = self::curl_init($url, $timeout);
curl_setopt($curl_array[$i], CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($mh, $curl_array[$i]);
}
$running = null;
do {
usleep(10000);
curl_multi_exec($mh, $running);
} while($running > 0);
$res = [];
foreach($urls as $i => $url) {
$res[$i] = curl_multi_getcontent($curl_array[$i]);
}
foreach($urls as $i => $url){
curl_multi_remove_handle($mh, $curl_array[$i]);
}
curl_multi_close($mh);
return $res;
}
/**
* Function lang_getfrombrowser gets the browser language based on HTTP headers.
*
@ -221,7 +349,9 @@ final class MD_STD {
// $_SERVER['HTTP_ACCEPT_LANGUAGE'] verwenden, wenn keine Sprachvariable mitgegeben wurde
if ($lang_variable === "") {
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) $lang_variable = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$lang_variable = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
}
}
// wurde irgendwelche Information mitgeschickt?
@ -232,7 +362,9 @@ final class MD_STD {
// Den Header auftrennen
$accepted_languages = preg_split('/,\s*/', $lang_variable);
if (!is_array($accepted_languages)) return $default_language;
if (!is_array($accepted_languages)) {
return $default_language;
}
// Die Standardwerte einstellen
$current_lang = $default_language;
@ -259,7 +391,8 @@ final class MD_STD {
if (isset($matches[2])) {
// die Qualität benutzen
$lang_quality = (float)$matches[2];
} else {
}
else {
// Kompabilitätsmodus: Qualität 1 annehmen
$lang_quality = 1.0;
}
@ -340,7 +473,9 @@ final class MD_STD {
public static function openssl_random_pseudo_bytes(int $length):string {
$output = \openssl_random_pseudo_bytes($length);
if ($output === false) throw new Exception("Failed generating random pseudo bytes using openssl_random_pseudo_bytes");
if ($output === false) {
throw new Exception("Failed generating random pseudo bytes using openssl_random_pseudo_bytes");
}
return $output;
}
@ -356,7 +491,9 @@ final class MD_STD {
$input = \explode(PHP_EOL, $input);
$output = "";
foreach ($input as $line) $output .= \trim($line) . PHP_EOL;
foreach ($input as $line) {
$output .= \trim($line) . PHP_EOL;
}
return $output;
}
@ -412,7 +549,7 @@ final class MD_STD {
*
* @return void
*/
public static function ensure_file(string $filepath, array $accepted_mimetype = []) {
public static function ensure_file(string $filepath, array $accepted_mimetype = []):void {
if (!\file_exists($filepath)) {
throw new MDFileDoesNotExist("File " . basename($filepath) . " does not exist");
@ -430,4 +567,57 @@ final class MD_STD {
}
}
/**
* Wrapper around exec, to be used with editing functions.
* Pipes STDERR to STDOUT and throws an Exception on any error.
*
* @param string $cmds Commands to run.
*
* @return void
*/
public static function exec_edit(string $cmds):void {
$error = \shell_exec($cmds . ' 2>&1 1>/dev/null');
if (!empty($error)) {
throw new \Exception('Shell error: ' . $error . PHP_EOL . PHP_EOL . 'Command was: ' . $cmds);
}
}
/**
* Wrapper around levenshtein(), that only compares the first 250
* characters (levenshtein can't handle more than 255).
*
* @param string $str1 First string.
* @param string $str2 Second string.
*
* @return integer
*/
public static function levenshtein(string $str1, string $str2):int {
if (\strlen($str1) > 250) {
$str1 = \substr($str1, 0, 250);
}
if (\strlen($str2) > 250) {
$str2 = \substr($str2, 0, 250);
}
return \levenshtein($str1, $str2);
}
/**
* Converts a string to color codes.
*
* @param string $str Input string.
*
* @return string
*/
public static function string_to_color_code(string $str):string {
$code = substr(dechex(crc32($str)), 0, 6);
return $code;
}
}

88
MD_STD_CACHE.php Normal file
View File

@ -0,0 +1,88 @@
<?PHP
/**
* Provides static functions for simple caching.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
/**
* Provides caching functions.
*/
final class MD_STD_CACHE {
/** @var string */
public static string $redis_host = '127.0.0.1';
/** @var integer */
public static int $redis_port = 6379;
/**
* Shutdown function for caching contents of output buffer.
*
* @param string $redisKey Key to cache by in redis.
* @param integer $expiry Expiration time in seconds.
*
* @return void
*/
public static function shutdown_cache_through_redis(string $redisKey, int $expiry = 3600):void {
$outputT = trim(MD_STD::minimizeHTMLString(MD_STD::ob_get_clean()));
echo $outputT;
$redis = new Redis();
$redis->connect(self::$redis_host, self::$redis_port, 1, null, 0, 0, ['auth' => [MD_CONF::$redis_pw]]);
$redis->set($redisKey, $outputT);
$redis->expire($redisKey, $expiry);
$redis->close();
}
/**
* Caches and serves a page through redis. Should be called at the start
* of the script generating a page.
*
* @param string $redisKey Key to cache by in redis.
* @param integer $expiry Expiration time in seconds.
*
* @return string
*/
public static function serve_page_through_redis_cache(string $redisKey, int $expiry = 3600):string {
if (PHP_SAPI === 'cli') {
return '';
}
$redis = new Redis();
$redis->connect(self::$redis_host, self::$redis_port, 1, null, 0, 0, ['auth' => [MD_CONF::$redis_pw]]);
if ($redis->ping() !== false) {
ob_start();
if (($redisResult = $redis->get($redisKey))) {
if (strlen($redisResult) > 3) {
$redis->close();
return $redisResult;
}
else {
register_shutdown_function(function(string $redisKey, int $expiry = 3600) :void {
self::shutdown_cache_through_redis($redisKey, $expiry);
}, $redisKey);
}
}
else {
register_shutdown_function(function(string $redisKey, int $expiry = 3600) :void {
self::shutdown_cache_through_redis($redisKey, $expiry);
}, $redisKey);
}
}
$redis->close();
return '';
}
}

View File

@ -9,7 +9,6 @@ declare(strict_types = 1);
* functions.
*/
final class MD_STD_IN {
/**
* Validates and sanitizes input integers to be in line with MySQL
* autoincrement IDs.
@ -45,7 +44,9 @@ final class MD_STD_IN {
*/
public static function sanitize_id_or_zero($input):int {
if ($input === "") return 0;
if ($input === "") {
return 0;
}
$input = \filter_var($input, \FILTER_VALIDATE_INT, [
'options' => [
@ -77,7 +78,9 @@ final class MD_STD_IN {
FILTER_SANITIZE_STRING,
FILTER_FLAG_NO_ENCODE_QUOTES);
if ($output === false) return "";
if ($output === false) {
return "";
}
while (strpos($output, " ") !== false) {
$output = str_replace(" ", " ", $output);
}
@ -86,6 +89,29 @@ final class MD_STD_IN {
}
/**
* String sanitization for 3 o4 6 characters RGB color codes (sans the leading #).
*
* @param mixed $input Input string.
*
* @return string
*/
public static function sanitize_rgb_color($input):string {
$output = \filter_var($input,
FILTER_SANITIZE_STRING,
FILTER_FLAG_NO_ENCODE_QUOTES);
if ($output === false
|| ((preg_match('/^[a-zA-Z0-9]{3}$/', $output)) === false && (preg_match('/^[a-zA-Z0-9]{6}$/', $output)) === false)
) {
throw new MDInvalidColorCode("Invalid color code provided: " . $output);
}
return $output;
}
/**
* Retrieves HTTP input texts from GET or POST variables, whatever is provided.
* If neither is given, returns a provided default.
@ -104,10 +130,12 @@ final class MD_STD_IN {
else if (isset($_POST[$var_name])) {
$output = self::sanitize_text($_POST[$var_name]);
}
else $output = self::sanitize_text($default);
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;
@ -129,10 +157,12 @@ final class MD_STD_IN {
if (isset($_POST[$var_name])) {
$output = self::sanitize_text($_POST[$var_name]);
}
else $output = self::sanitize_text($default);
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;
@ -148,7 +178,9 @@ final class MD_STD_IN {
*/
public static function sanitize_url($input):string {
if ($input === "") return "";
if ($input === "") {
return "";
}
$output = \filter_var($input, FILTER_SANITIZE_URL);
if (($output = \filter_var($output, FILTER_VALIDATE_URL)) === false) {
@ -168,7 +200,9 @@ final class MD_STD_IN {
*/
public static function sanitize_email($input):string {
if ($input === "") return "";
if ($input === "") {
return "";
}
$output = \filter_var($input, FILTER_SANITIZE_EMAIL);
if (($output = \filter_var($output, FILTER_VALIDATE_EMAIL)) === false) {
@ -205,7 +239,9 @@ final class MD_STD_IN {
*/
public static function validate_isbn(string $input):string {
if ($input === "") return "";
if ($input === "") {
return "";
}
// Remove hyphens
$input = trim(strtr($input, ["-" => "", "" => ""]));
@ -231,5 +267,4 @@ final class MD_STD_IN {
throw new MDgenericInvalidInputsException("ISBNs must be either 10 or 13 characters long.");
}
}

View File

@ -38,7 +38,8 @@ final class MD_STD_SEC {
) {
$validity = true;
}
$_SESSION['csrf-token'] = null; unset($_SESSION['csrf-token']);
$_SESSION['csrf-token'] = null;
unset($_SESSION['csrf-token']);
return $validity;
@ -46,8 +47,8 @@ final class MD_STD_SEC {
const BRUTE_FORCE_DELAY_DEFAULT = 2000; // 2000 microseconds = 2 milliseconds
const BRUTE_FORCE_DELAY_MULTIPLIER_COMMON = 1.08;
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER = 3;
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP = 8;
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER = 1.8;
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP = 4;
/**
* Prevent brute force attacks by delaying the login .
@ -67,7 +68,9 @@ final class MD_STD_SEC {
$logfile_common = \sys_get_temp_dir() . "/logins_{$tool_name}.json";
// Ensure the log files exist
if (!\file_exists($logfile_common)) \file_put_contents($logfile_common, "[]");
if (!\file_exists($logfile_common)) {
\file_put_contents($logfile_common, "[]");
}
// Hash entered username and IP to prevent malicious strings from
// entering the system.
@ -102,7 +105,7 @@ final class MD_STD_SEC {
// Translate counters into delay multipliers
$delay_multiplier_common = $loginLog['common']['count'];
$delay_multiplier_per_user = $loginLog['usr'][$hash_user]['count'];
$delay_multiplier_per_ip = $loginLog['usr'][$hash_ip]['count'];
$delay_multiplier_per_ip = $loginLog['ip'][$hash_ip]['count'];
// Calculate delay
$delay_micoseconds = \intval(self::BRUTE_FORCE_DELAY_DEFAULT *
@ -122,4 +125,24 @@ final class MD_STD_SEC {
return true;
}
/**
* Send CSP headers.
*
* @param array{default-src: string, connect-src: string, script-src: string, img-src: string, media-src: string, style-src: string, frame-src: string, object-src: string, base-uri: string, form-action: string, frame-ancestors?: string} $directives Directives to send. Font source is always set to 'self', and hence excluded.
* @param string $frame_ancestors Frame ancestors directive. Default is to not set it.
*
* @return void
*/
public static function sendContentSecurityPolicy(array $directives, string $frame_ancestors = ""):void {
$policy = 'Content-Security-Policy: default-src ' . $directives['default-src'] . '; connect-src ' . $directives['connect-src'] . '; script-src ' . $directives['script-src'] . '; img-src ' . $directives['img-src'] . '; media-src ' . $directives['media-src'] . '; style-src ' . $directives['style-src'] . '; font-src \'self\'; frame-src ' . $directives['frame-src'] . '; object-src ' . $directives['object-src'] . '; base-uri ' . $directives['base-uri'] . '; form-action ' . $directives['form-action'] . '; manifest-src \'self\';';
if (!empty($frame_ancestors)) {
$policy .= ' frame-ancestors ' . $frame_ancestors . ';';
}
header($policy);
}
}

View File

@ -5,7 +5,6 @@ declare(strict_types = 1);
* Exception thrown by MDJail if a required security option has not been set.
*/
final class MDJailSecurityOptionNotSetException extends Exception {
/**
* Error message.
*
@ -17,5 +16,4 @@ final class MDJailSecurityOptionNotSetException extends Exception {
return $errorMsg;
}
}