Skip to content

Commit

Permalink
Build response headers before sending them out
Browse files Browse the repository at this point in the history
Signed-off-by: Maurício Meneghini Fauth <mauricio@fauth.dev>
  • Loading branch information
MauricioFauth committed Jul 1, 2021
1 parent 9adaa91 commit c985faf
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 150 deletions.
75 changes: 52 additions & 23 deletions libraries/classes/Core.php
Expand Up @@ -600,13 +600,20 @@ public static function headerJSON(): void
}

// No caching
self::noCacheHeader();
// MIME type
header('Content-Type: application/json; charset=UTF-8');
// Disable content sniffing in browser
// This is needed in case we include HTML in JSON, browser might assume it's
// html to display
header('X-Content-Type-Options: nosniff');
$headers = self::getNoCacheHeaders();

// Media type
$headers['Content-Type'] = 'application/json; charset=UTF-8';

/**
* Disable content sniffing in browser.
* This is needed in case we include HTML in JSON, browser might assume it's html to display.
*/
$headers['X-Content-Type-Options'] = 'nosniff';

foreach ($headers as $name => $value) {
header(sprintf('%s: %s', $name, $value));
}
}

/**
Expand All @@ -618,19 +625,36 @@ public static function noCacheHeader(): void
return;
}

$headers = self::getNoCacheHeaders();

foreach ($headers as $name => $value) {
header(sprintf('%s: %s', $name, $value));
}
}

/**
* @return array<string, string>
*/
public static function getNoCacheHeaders(): array
{
$headers = [];
$date = (string) gmdate(DATE_RFC1123);

// rfc2616 - Section 14.21
header('Expires: ' . gmdate(DATE_RFC1123));
$headers['Expires'] = $date;

// HTTP/1.1
header(
'Cache-Control: no-store, no-cache, must-revalidate,'
. ' pre-check=0, post-check=0, max-age=0'
);
$headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0';

// HTTP/1.0
$headers['Pragma'] = 'no-cache';

header('Pragma: no-cache'); // HTTP/1.0
// test case: exporting a database into a .gz file with Safari
// would produce files not having the current time
// (added this header for Safari but should not harm other browsers)
header('Last-Modified: ' . gmdate(DATE_RFC1123));
$headers['Last-Modified'] = $date;

return $headers;
}

/**
Expand All @@ -648,32 +672,37 @@ public static function downloadHeader(
int $length = 0,
bool $no_cache = true
): void {
$headers = [];

if ($no_cache) {
self::noCacheHeader();
$headers = self::getNoCacheHeaders();
}

/* Replace all possibly dangerous chars in filename */
$filename = Sanitize::sanitizeFilename($filename);
if (! empty($filename)) {
header('Content-Description: File Transfer');
header('Content-Disposition: attachment; filename="' . $filename . '"');
$headers['Content-Description'] = 'File Transfer';
$headers['Content-Disposition'] = 'attachment; filename="' . $filename . '"';
}

header('Content-Type: ' . $mimetype);
$headers['Content-Type'] = $mimetype;
// inform the server that compression has been done,
// to avoid a double compression (for example with Apache + mod_deflate)
$notChromeOrLessThan43 = PMA_USR_BROWSER_AGENT != 'CHROME' // see bug #4942
|| (PMA_USR_BROWSER_AGENT == 'CHROME' && PMA_USR_BROWSER_VER < 43);
if (strpos($mimetype, 'gzip') !== false && $notChromeOrLessThan43) {
header('Content-Encoding: gzip');
$headers['Content-Encoding'] = 'gzip';
}

header('Content-Transfer-Encoding: binary');
if ($length <= 0) {
return;
$headers['Content-Transfer-Encoding'] = 'binary';

if ($length > 0) {
$headers['Content-Length'] = (string) $length;
}

header('Content-Length: ' . $length);
foreach ($headers as $name => $value) {
header(sprintf('%s: %s', $name, $value));
}
}

/**
Expand Down
190 changes: 100 additions & 90 deletions libraries/classes/Header.php
Expand Up @@ -11,13 +11,15 @@
use PhpMyAdmin\Navigation\Navigation;

use function __;
use function array_merge;
use function defined;
use function gmdate;
use function header;
use function htmlspecialchars;
use function implode;
use function ini_get;
use function is_bool;
use function sprintf;
use function strlen;
use function strtolower;
use function urlencode;
Expand Down Expand Up @@ -517,54 +519,70 @@ public function sendHttpHeaders(): void
*/
$GLOBALS['now'] = gmdate('D, d M Y H:i:s') . ' GMT';

$headers = $this->getHttpHeaders();

foreach ($headers as $name => $value) {
header(sprintf('%s: %s', $name, $value));
}

$this->headerIsSent = true;
}

/**
* @return array<string, string>
*/
private function getHttpHeaders(): array
{
$headers = [];

/* Prevent against ClickJacking by disabling framing */
if (strtolower((string) $GLOBALS['cfg']['AllowThirdPartyFraming']) === 'sameorigin') {
header(
'X-Frame-Options: SAMEORIGIN'
);
$headers['X-Frame-Options'] = 'SAMEORIGIN';
} elseif ($GLOBALS['cfg']['AllowThirdPartyFraming'] !== true) {
header(
'X-Frame-Options: DENY'
);
$headers['X-Frame-Options'] = 'DENY';
}

header(
'Referrer-Policy: no-referrer'
);
$headers['Referrer-Policy'] = 'no-referrer';

$cspHeaders = $this->getCspHeaders();
foreach ($cspHeaders as $cspHeader) {
header($cspHeader);
}
$headers = array_merge($headers, $this->getCspHeaders());

/**
* Re-enable possible disabled XSS filters.
*
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
*/
$headers['X-XSS-Protection'] = '1; mode=block';

/**
* "nosniff", prevents Internet Explorer and Google Chrome from MIME-sniffing
* a response away from the declared content-type.
*
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
*/
$headers['X-Content-Type-Options'] = 'nosniff';

/**
* Adobe cross-domain-policies.
*
* @see https://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html
*/
$headers['X-Permitted-Cross-Domain-Policies'] = 'none';

/**
* Robots meta tag.
*
* @see https://developers.google.com/webmasters/control-crawl-index/docs/robots_meta_tag
*/
$headers['X-Robots-Tag'] = 'noindex, nofollow';

$headers = array_merge($headers, Core::getNoCacheHeaders());

// Re-enable possible disabled XSS filters
// see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
header(
'X-XSS-Protection: 1; mode=block'
);
// "nosniff", prevents Internet Explorer and Google Chrome from MIME-sniffing
// a response away from the declared content-type
// see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
header(
'X-Content-Type-Options: nosniff'
);
// Adobe cross-domain-policies
// see https://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html
header(
'X-Permitted-Cross-Domain-Policies: none'
);
// Robots meta tag
// see https://developers.google.com/webmasters/control-crawl-index/docs/robots_meta_tag
header(
'X-Robots-Tag: noindex, nofollow'
);
Core::noCacheHeader();
if (! defined('IS_TRANSFORMATION_WRAPPER')) {
// Define the charset to be used
header('Content-Type: text/html; charset=utf-8');
$headers['Content-Type'] = 'text/html; charset=utf-8';
}

$this->headerIsSent = true;
return $headers;
}

/**
Expand Down Expand Up @@ -599,7 +617,7 @@ public function getPageTitle(): string
/**
* Get all the CSP allow policy headers
*
* @return string[]
* @return array<string, string>
*/
private function getCspHeaders(): array
{
Expand All @@ -610,64 +628,56 @@ private function getCspHeaders(): array
$cspAllow = $cfg['CSPAllow'];

if (
! empty($cfg['CaptchaApi'])
! empty($cfg['CaptchaLoginPrivateKey'])
&& ! empty($cfg['CaptchaLoginPublicKey'])
&& ! empty($cfg['CaptchaApi'])
&& ! empty($cfg['CaptchaRequestParam'])
&& ! empty($cfg['CaptchaResponseParam'])
&& ! empty($cfg['CaptchaLoginPrivateKey'])
&& ! empty($cfg['CaptchaLoginPublicKey'])
) {
$captchaUrl = ' ' . $cfg['CaptchaCsp'] . ' ';
}

return [

"Content-Security-Policy: default-src 'self' "
. $captchaUrl
. $cspAllow . ';'
. "script-src 'self' 'unsafe-inline' 'unsafe-eval' "
. $captchaUrl
. $cspAllow . ';'
. "style-src 'self' 'unsafe-inline' "
. $captchaUrl
. $cspAllow
. ';'
. "img-src 'self' data: "
. $cspAllow
. $mapTileUrls
. $captchaUrl
. ';'
. "object-src 'none';",

"X-Content-Security-Policy: default-src 'self' "
. $captchaUrl
. $cspAllow . ';'
. 'options inline-script eval-script;'
. 'referrer no-referrer;'
. "img-src 'self' data: "
. $cspAllow
. $mapTileUrls
. $captchaUrl
. ';'
. "object-src 'none';",

"X-WebKit-CSP: default-src 'self' "
. $captchaUrl
. $cspAllow . ';'
. "script-src 'self' "
. $captchaUrl
. $cspAllow
. " 'unsafe-inline' 'unsafe-eval';"
. 'referrer no-referrer;'
. "style-src 'self' 'unsafe-inline' "
. $captchaUrl
. ';'
. "img-src 'self' data: "
. $cspAllow
. $mapTileUrls
. $captchaUrl
. ';'
. "object-src 'none';",
];
$headers = [];

$headers['Content-Security-Policy'] = sprintf(
'default-src \'self\' %s%s;script-src \'self\' \'unsafe-inline\' \'unsafe-eval\' %s%s;'
. 'style-src \'self\' \'unsafe-inline\' %s%s;img-src \'self\' data: %s%s%s;object-src \'none\';',
$captchaUrl,
$cspAllow,
$captchaUrl,
$cspAllow,
$captchaUrl,
$cspAllow,
$cspAllow,
$mapTileUrls,
$captchaUrl
);

$headers['X-Content-Security-Policy'] = sprintf(
'default-src \'self\' %s%s;options inline-script eval-script;'
. 'referrer no-referrer;img-src \'self\' data: %s%s%s;object-src \'none\';',
$captchaUrl,
$cspAllow,
$cspAllow,
$mapTileUrls,
$captchaUrl
);

$headers['X-WebKit-CSP'] = sprintf(
'default-src \'self\' %s%s;script-src \'self\' %s%s \'unsafe-inline\' \'unsafe-eval\';'
. 'referrer no-referrer;style-src \'self\' \'unsafe-inline\' %s;'
. 'img-src \'self\' data: %s%s%s;object-src \'none\';',
$captchaUrl,
$cspAllow,
$captchaUrl,
$cspAllow,
$captchaUrl,
$cspAllow,
$mapTileUrls,
$captchaUrl
);

return $headers;
}

/**
Expand Down
14 changes: 11 additions & 3 deletions libraries/classes/OutputBuffering.php
Expand Up @@ -20,6 +20,7 @@
use function ob_get_status;
use function ob_start;
use function register_shutdown_function;
use function sprintf;

/**
* Output buffering wrapper class
Expand Down Expand Up @@ -110,9 +111,7 @@ public function start()
}

ob_start();
if (! defined('TESTSUITE')) {
header('X-ob_mode: ' . $this->mode);
}
$this->sendHeader('X-ob_mode', (string) $this->mode);

register_shutdown_function(
[
Expand All @@ -123,6 +122,15 @@ public function start()
$this->on = true;
}

private function sendHeader(string $name, string $value): void
{
if (defined('TESTSUITE')) {
return;
}

header(sprintf('%s: %s', $name, $value));
}

/**
* This function will need to run at the bottom of all pages if output
* buffering is turned on. It also needs to be passed $mode from the
Expand Down

0 comments on commit c985faf

Please sign in to comment.