Skip to content

Commit

Permalink
Prevent access to snapshots except through Amber iframe
Browse files Browse the repository at this point in the history
If a snapshot is loaded directly, without a surrounding iframe that
puts it in a sandbox, the viewer could be at risk of an XSS attack.
Prevent this by (a) checking that views of cached content have a
referrer header that matches that of the expected containing iframe;
and (b) restricting access to the wp-content/uploads/amber directory
by placing a restrictive .htaccess fiels in that directory. (#34)
  • Loading branch information
jlicht committed Mar 23, 2016
1 parent 4615bcd commit 22e0a39
Showing 1 changed file with 70 additions and 12 deletions.
82 changes: 70 additions & 12 deletions amber.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public static function get_storage_instance($backend) {
$base_dir = wp_upload_dir();
$subdir = Amber::get_option('amber_storage_location', 'amber');
$file_path = join(DIRECTORY_SEPARATOR, array($base_dir['basedir'], $subdir));
Amber::secure_local_storage($file_path);
$storage = new AmberStorage($file_path);
break;
case AMBER_BACKEND_PERMA:
Expand Down Expand Up @@ -664,6 +665,7 @@ public static function format_memento_date($date_string) {
return $result;
}


/**
* Request handling function to display cached content and assets
*/
Expand Down Expand Up @@ -710,19 +712,27 @@ public static function display_cached_content ($wp) {
die();
}
if (!empty($cache_frame_id)) {
status_header( 200 ); /* This must be set BEFORE any content is printed */
if (empty($asset_id)) {
/* This is the root item */
$data = Amber::retrieve_cache_item($cache_frame_id);
$status = Amber::get_status();
$status->save_view($cache_frame_id);
print $data['data'];
die();
/* Check to make sure the cached item is being displayed in an iframe that
protects the user from XSS attacks from malicious javascript that might
exist in a snapshot */
if (!Amber::validate_cache_iframe_referrer()) {
status_header(403);
die();
} else {
/* This is an asset */
$data = Amber::retrieve_cache_asset($cache_frame_id, $asset_id);
print($data['data']);
die();
status_header( 200 ); /* This must be set BEFORE any content is printed */
if (empty($asset_id)) {
/* This is the root item */
$data = Amber::retrieve_cache_item($cache_frame_id);
$status = Amber::get_status();
$status->save_view($cache_frame_id);
print $data['data'];
die();
} else {
/* This is an asset */
$data = Amber::retrieve_cache_asset($cache_frame_id, $asset_id);
print($data['data']);
die();
}
}
}
}
Expand Down Expand Up @@ -772,6 +782,38 @@ public static function filter_cached_content_headers($headers)
return $headers;
}

/**
* Called when displaying cached content that's expected to be in an iframe
* Validates that the iframe that's enclosing the cached content is being served
* from the expected URL on the same server. We know that requests from this
* URL provide the proper sandbox attributes for the iframe to prevent XSS
* attacks.
* @return true if access is allowed
*/
public static function validate_cache_iframe_referrer() {
$headers = getallheaders();
if (isset($headers['Referer'])) {
/* The Referer URL should be the same as the current URL, except with
'cache' instead of 'cacheframe' in the URL. */
$referer_uri = $headers['Referer'];
$requested_uri = "http"
. (isset($_SERVER['HTTPS']) ? 's' : '')
. "://{$_SERVER['HTTP_HOST']}"
. $_SERVER['REQUEST_URI'];

/* The value that should be in the HTTP Referer header */
$expected_referer = str_replace("/cacheframe/", "/cache/", $requested_uri);
if (strcmp($expected_referer, $referer_uri) === 0) {
return TRUE;
}
else {
return FALSE;
}
}
/* No Referer header, so not allowed */
return FALSE;
}

/* Respond to an ajax call to cache links on a specific page immediately
*/
public static function ajax_cache_now() {
Expand Down Expand Up @@ -983,6 +1025,22 @@ public static function ajax_get_memento() {
die();
}

/* Ensure that the directory on the local file system that contains cached content
has an htaccess file to prevent the content from being loaded directly (to
protect against XSS attacks from malicious saved javascript) */
public static function secure_local_storage($dir) {
$filename = join(DIRECTORY_SEPARATOR,array($dir,'.htaccess'));
if (!file_exists($filename)) {
$htaccess = <<<EOF
<Files "*">
Order Deny,Allow
Deny from all
</Files>
EOF;
file_put_contents($filename, $htaccess);
}
}

}

include_once dirname( __FILE__ ) . '/amber-install.php';
Expand Down

0 comments on commit 22e0a39

Please sign in to comment.