diff --git a/app/Http/Controllers/Admin/AdminSettingsController.php b/app/Http/Controllers/Admin/AdminSettingsController.php
index 98e16bbc..f1c2ca3a 100644
--- a/app/Http/Controllers/Admin/AdminSettingsController.php
+++ b/app/Http/Controllers/Admin/AdminSettingsController.php
@@ -531,6 +531,7 @@ trait AdminSettingsController
'registration_status' => 'required|in:open,filtered,closed',
'cloud_storage' => 'required',
'activitypub_enabled' => 'required',
+ 'authorized_fetch' => 'required',
'account_migration' => 'required',
'mobile_apis' => 'required',
'stories' => 'required',
@@ -555,6 +556,7 @@ trait AdminSettingsController
}
}
}
+ ConfigCacheService::put('federation.activitypub.authorized_fetch', $request->boolean('authorized_fetch'));
ConfigCacheService::put('federation.activitypub.enabled', $request->boolean('activitypub_enabled'));
ConfigCacheService::put('federation.migration', $request->boolean('account_migration'));
ConfigCacheService::put('pixelfed.oauth_enabled', $request->boolean('mobile_apis'));
diff --git a/app/Services/AdminSettingsService.php b/app/Services/AdminSettingsService.php
index 57fb6e96..6a261f5a 100644
--- a/app/Services/AdminSettingsService.php
+++ b/app/Services/AdminSettingsService.php
@@ -37,6 +37,7 @@ class AdminSettingsService
'registration_status' => $regState,
'cloud_storage' => $cloud_ready && $cloud_storage,
'activitypub_enabled' => (bool) config_cache('federation.activitypub.enabled'),
+ 'authorized_fetch' => (bool) config_cache('federation.activitypub.authorized_fetch'),
'account_migration' => (bool) config_cache('federation.migration'),
'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'),
'stories' => (bool) config_cache('instance.stories.enabled'),
diff --git a/app/Services/ConfigCacheService.php b/app/Services/ConfigCacheService.php
index 4f2b006c..527c8602 100644
--- a/app/Services/ConfigCacheService.php
+++ b/app/Services/ConfigCacheService.php
@@ -46,6 +46,7 @@ class ConfigCacheService
'pixelfed.oauth_enabled',
'pixelfed.import.instagram.enabled',
'pixelfed.bouncer.enabled',
+ 'federation.activitypub.authorized_fetch',
'pixelfed.enforce_email_verification',
'pixelfed.max_account_size',
diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php
index 9e03beef..fe82eb2e 100644
--- a/app/Util/ActivityPub/Helpers.php
+++ b/app/Util/ActivityPub/Helpers.php
@@ -25,6 +25,8 @@ use Cache;
use Carbon\Carbon;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
+use League\Uri\Exceptions\UriException;
+use League\Uri\Uri;
use Purify;
use Validator;
@@ -153,61 +155,74 @@ class Helpers
return in_array($url, $audience['to']) || in_array($url, $audience['cc']);
}
- public static function validateUrl($url)
+ public static function validateUrl($url = null, $disableDNSCheck = false)
{
- if (is_array($url)) {
+ if (is_array($url) && ! empty($url)) {
$url = $url[0];
}
+ if (! $url || strlen($url) === 0) {
+ return false;
+ }
+ try {
+ $uri = Uri::new($url);
- $hash = hash('sha256', $url);
- $key = "helpers:url:valid:sha256-{$hash}";
+ if (! $uri) {
+ return false;
+ }
+
+ if ($uri->getScheme() !== 'https') {
+ return false;
+ }
+
+ $host = $uri->getHost();
+
+ if (! $host || $host === '') {
+ return false;
+ }
+
+ if (! filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
+ return false;
+ }
+
+ if (! str_contains($host, '.')) {
+ return false;
+ }
- $valid = Cache::remember($key, 900, function () use ($url) {
$localhosts = [
- '127.0.0.1', 'localhost', '::1',
+ 'localhost',
+ '127.0.0.1',
+ '::1',
+ 'broadcasthost',
+ 'ip6-localhost',
+ 'ip6-loopback',
];
- if (strtolower(mb_substr($url, 0, 8)) !== 'https://') {
- return false;
- }
-
- if (substr_count($url, '://') !== 1) {
- return false;
- }
-
- if (mb_substr($url, 0, 8) !== 'https://') {
- $url = 'https://'.substr($url, 8);
- }
-
- $valid = filter_var($url, FILTER_VALIDATE_URL);
-
- if (! $valid) {
- return false;
- }
-
- $host = parse_url($valid, PHP_URL_HOST);
-
if (in_array($host, $localhosts)) {
return false;
}
- if (config('security.url.verify_dns')) {
- if (DomainService::hasValidDns($host) === false) {
+ if ($disableDNSCheck !== true && app()->environment() === 'production' && (bool) config('security.url.verify_dns')) {
+ $hash = hash('sha256', $host);
+ $key = "helpers:url:valid-dns:sha256-{$hash}";
+ $domainValidDns = Cache::remember($key, 14440, function () use ($host) {
+ return DomainService::hasValidDns($host);
+ });
+ if (! $domainValidDns) {
return false;
}
}
- if (app()->environment() === 'production') {
+ if ($disableDNSCheck !== true && app()->environment() === 'production') {
$bannedInstances = InstanceService::getBannedDomains();
if (in_array($host, $bannedInstances)) {
return false;
}
}
- return $url;
- });
-
- return $valid;
+ return $uri->toString();
+ } catch (UriException $e) {
+ return false;
+ }
}
public static function validateLocalUrl($url)
@@ -215,7 +230,12 @@ class Helpers
$url = self::validateUrl($url);
if ($url == true) {
$domain = config('pixelfed.domain.app');
- $host = parse_url($url, PHP_URL_HOST);
+
+ $uri = Uri::new($url);
+ $host = $uri->getHost();
+ if (! $host || empty($host)) {
+ return false;
+ }
$url = strtolower($domain) === strtolower($host) ? $url : false;
return $url;
diff --git a/app/Util/ActivityPub/HttpSignature.php b/app/Util/ActivityPub/HttpSignature.php
index 5bfdcac0..35facb82 100644
--- a/app/Util/ActivityPub/HttpSignature.php
+++ b/app/Util/ActivityPub/HttpSignature.php
@@ -2,146 +2,159 @@
namespace App\Util\ActivityPub;
-use Cache, Log;
use App\Models\InstanceActor;
use App\Profile;
-use \DateTime;
+use Cache;
+use DateTime;
-class HttpSignature {
+class HttpSignature
+{
+ /*
+ * source: https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php
+ * thanks aaronpk!
+ */
- /*
- * source: https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php
- * thanks aaronpk!
- */
+ public static function sign(Profile $profile, $url, $body = false, $addlHeaders = [])
+ {
+ if ($body) {
+ $digest = self::_digest($body);
+ }
+ $user = $profile;
+ $headers = self::_headersToSign($url, $body ? $digest : false);
+ $headers = array_merge($headers, $addlHeaders);
+ $stringToSign = self::_headersToSigningString($headers);
+ $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
+ $key = openssl_pkey_get_private($user->private_key);
+ openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
+ $signature = base64_encode($signature);
+ $signatureHeader = 'keyId="'.$user->keyId().'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
+ unset($headers['(request-target)']);
+ $headers['Signature'] = $signatureHeader;
- public static function sign(Profile $profile, $url, $body = false, $addlHeaders = []) {
- if($body) {
- $digest = self::_digest($body);
- }
- $user = $profile;
- $headers = self::_headersToSign($url, $body ? $digest : false);
- $headers = array_merge($headers, $addlHeaders);
- $stringToSign = self::_headersToSigningString($headers);
- $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
- $key = openssl_pkey_get_private($user->private_key);
- openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
- $signature = base64_encode($signature);
- $signatureHeader = 'keyId="'.$user->keyId().'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
- unset($headers['(request-target)']);
- $headers['Signature'] = $signatureHeader;
-
- return self::_headersToCurlArray($headers);
- }
-
- public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post')
- {
- $keyId = config('app.url') . '/i/actor#main-key';
- $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function() {
- return InstanceActor::first()->private_key;
- });
- if($body) {
- $digest = self::_digest($body);
- }
- $headers = self::_headersToSign($url, $body ? $digest : false, $method);
- $headers = array_merge($headers, $addlHeaders);
- $stringToSign = self::_headersToSigningString($headers);
- $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
- $key = openssl_pkey_get_private($privateKey);
- openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
- $signature = base64_encode($signature);
- $signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
- unset($headers['(request-target)']);
- $headers['Signature'] = $signatureHeader;
-
- return $headers;
- }
-
- public static function parseSignatureHeader($signature) {
- $parts = explode(',', $signature);
- $signatureData = [];
-
- foreach($parts as $part) {
- if(preg_match('/(.+)="(.+)"/', $part, $match)) {
- $signatureData[$match[1]] = $match[2];
- }
+ return self::_headersToCurlArray($headers);
}
- if(!isset($signatureData['keyId'])) {
- return [
- 'error' => 'No keyId was found in the signature header. Found: '.implode(', ', array_keys($signatureData))
- ];
+ public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post')
+ {
+ $keyId = config('app.url').'/i/actor#main-key';
+ $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () {
+ return InstanceActor::first()->private_key;
+ });
+ if ($body) {
+ $digest = self::_digest($body);
+ }
+ $headers = self::_headersToSign($url, $body ? $digest : false, $method);
+ $headers = array_merge($headers, $addlHeaders);
+ $stringToSign = self::_headersToSigningString($headers);
+ $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
+ $key = openssl_pkey_get_private($privateKey);
+ openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
+ $signature = base64_encode($signature);
+ $signatureHeader = 'keyId="'.$keyId.'",headers="'.$signedHeaders.'",algorithm="rsa-sha256",signature="'.$signature.'"';
+ unset($headers['(request-target)']);
+ $headers['Signature'] = $signatureHeader;
+
+ return $headers;
}
- if(!filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) {
- return [
- 'error' => 'keyId is not a URL: '.$signatureData['keyId']
- ];
+ public static function parseSignatureHeader($signature)
+ {
+ $parts = explode(',', $signature);
+ $signatureData = [];
+
+ foreach ($parts as $part) {
+ if (preg_match('/(.+)="(.+)"/', $part, $match)) {
+ $signatureData[$match[1]] = $match[2];
+ }
+ }
+
+ if (! isset($signatureData['keyId'])) {
+ return [
+ 'error' => 'No keyId was found in the signature header. Found: '.implode(', ', array_keys($signatureData)),
+ ];
+ }
+
+ if (! filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) {
+ return [
+ 'error' => 'keyId is not a URL: '.$signatureData['keyId'],
+ ];
+ }
+
+ if (! Helpers::validateUrl($signatureData['keyId'])) {
+ return [
+ 'error' => 'keyId is not a URL: '.$signatureData['keyId'],
+ ];
+ }
+
+ if (! isset($signatureData['headers']) || ! isset($signatureData['signature'])) {
+ return [
+ 'error' => 'Signature is missing headers or signature parts',
+ ];
+ }
+
+ return $signatureData;
}
- if(!isset($signatureData['headers']) || !isset($signatureData['signature'])) {
- return [
- 'error' => 'Signature is missing headers or signature parts'
- ];
+ public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body)
+ {
+ $digest = 'SHA-256='.base64_encode(hash('sha256', $body, true));
+ $headersToSign = [];
+ foreach (explode(' ', $signatureData['headers']) as $h) {
+ if ($h == '(request-target)') {
+ $headersToSign[$h] = 'post '.$path;
+ } elseif ($h == 'digest') {
+ $headersToSign[$h] = $digest;
+ } elseif (isset($inputHeaders[$h][0])) {
+ $headersToSign[$h] = $inputHeaders[$h][0];
+ }
+ }
+ $signingString = self::_headersToSigningString($headersToSign);
+
+ $verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256);
+
+ return [$verified, $signingString];
}
- return $signatureData;
- }
-
- public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body) {
- $digest = 'SHA-256='.base64_encode(hash('sha256', $body, true));
- $headersToSign = [];
- foreach(explode(' ',$signatureData['headers']) as $h) {
- if($h == '(request-target)') {
- $headersToSign[$h] = 'post '.$path;
- } elseif($h == 'digest') {
- $headersToSign[$h] = $digest;
- } elseif(isset($inputHeaders[$h][0])) {
- $headersToSign[$h] = $inputHeaders[$h][0];
- }
- }
- $signingString = self::_headersToSigningString($headersToSign);
-
- $verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256);
-
- return [$verified, $signingString];
- }
-
- private static function _headersToSigningString($headers) {
- return implode("\n", array_map(function($k, $v){
- return strtolower($k).': '.$v;
- }, array_keys($headers), $headers));
- }
-
- private static function _headersToCurlArray($headers) {
- return array_map(function($k, $v){
- return "$k: $v";
- }, array_keys($headers), $headers);
- }
-
- private static function _digest($body) {
- if(is_array($body)) {
- $body = json_encode($body);
- }
- return base64_encode(hash('sha256', $body, true));
- }
-
- protected static function _headersToSign($url, $digest = false, $method = 'post') {
- $date = new DateTime('UTC');
-
- if(!in_array($method, ['post', 'get'])) {
- throw new \Exception('Invalid method used to sign headers in HttpSignature');
- }
- $headers = [
- '(request-target)' => $method . ' '.parse_url($url, PHP_URL_PATH),
- 'Host' => parse_url($url, PHP_URL_HOST),
- 'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
- ];
-
- if($digest) {
- $headers['Digest'] = 'SHA-256='.$digest;
+ private static function _headersToSigningString($headers)
+ {
+ return implode("\n", array_map(function ($k, $v) {
+ return strtolower($k).': '.$v;
+ }, array_keys($headers), $headers));
}
- return $headers;
- }
+ private static function _headersToCurlArray($headers)
+ {
+ return array_map(function ($k, $v) {
+ return "$k: $v";
+ }, array_keys($headers), $headers);
+ }
+ private static function _digest($body)
+ {
+ if (is_array($body)) {
+ $body = json_encode($body);
+ }
+
+ return base64_encode(hash('sha256', $body, true));
+ }
+
+ protected static function _headersToSign($url, $digest = false, $method = 'post')
+ {
+ $date = new DateTime('UTC');
+
+ if (! in_array($method, ['post', 'get'])) {
+ throw new \Exception('Invalid method used to sign headers in HttpSignature');
+ }
+ $headers = [
+ '(request-target)' => $method.' '.parse_url($url, PHP_URL_PATH),
+ 'Host' => parse_url($url, PHP_URL_HOST),
+ 'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
+ ];
+
+ if ($digest) {
+ $headers['Digest'] = 'SHA-256='.$digest;
+ }
+
+ return $headers;
+ }
}
diff --git a/composer.json b/composer.json
index b97ec118..29fd2232 100644
--- a/composer.json
+++ b/composer.json
@@ -29,6 +29,7 @@
"laravel/ui": "^4.2",
"league/flysystem-aws-s3-v3": "^3.0",
"league/iso3166": "^2.1|^4.0",
+ "league/uri": "^7.4",
"pbmedia/laravel-ffmpeg": "^8.0",
"phpseclib/phpseclib": "~2.0",
"pixelfed/fractal": "^0.18.0",
diff --git a/composer.lock b/composer.lock
index e6fd309e..370627f1 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "c055c4b1ba26004ab6951e9dba4b4508",
+ "content-hash": "fecc0efcc40880a422690feefedef584",
"packages": [
{
"name": "aws/aws-crt-php",
diff --git a/config/federation.php b/config/federation.php
index 3d7a7bb3..124935ec 100644
--- a/config/federation.php
+++ b/config/federation.php
@@ -30,6 +30,8 @@ return [
'ingest' => [
'store_notes_without_followers' => env('AP_INGEST_STORE_NOTES_WITHOUT_FOLLOWERS', false),
],
+
+ 'authorized_fetch' => env('AUTHORIZED_FETCH', false),
],
'atom' => [
diff --git a/resources/assets/components/admin/AdminSettings.vue b/resources/assets/components/admin/AdminSettings.vue
index 78ffd1b1..8693f05b 100644
--- a/resources/assets/components/admin/AdminSettings.vue
+++ b/resources/assets/components/admin/AdminSettings.vue
@@ -63,6 +63,13 @@
@change="handleChange($event, 'features', 'activitypub_enabled')"
/>
+
+
+
+
{
this.isSubmitting = false;
this.isSubmittingTimeout = true;