Serve uploaded SVGs under their CSP, and identify the image fetcher
The Content-Security-Policy meant for stored SVGs never applied: in Caddy a `*` in the middle of a path does not cross a `/`, so `path /storage/*.svg` only matched files directly under /storage, and every upload lives in a directory below it. Two matchers, which Caddy ANDs, cover any depth. RemoteImage fetched with Guzzle's anonymous default User-Agent, which Wikimedia answers with 403. It now sends `<app>/1.0 (+<app url>; media fetcher)`, the identification such hosts ask of automated clients. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ namespace App\Services\Media;
|
||||
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
@@ -25,6 +26,10 @@ use Illuminate\Validation\ValidationException;
|
||||
* - Redirects are not followed: the target of one would need all of the above
|
||||
* again, and "use the final address" is an easy thing to ask for.
|
||||
* - The body is read to a ceiling and no further, whatever the headers claim.
|
||||
*
|
||||
* It says who is asking. Hosts such as Wikimedia refuse Guzzle's anonymous
|
||||
* default outright, and a User-Agent with a contact address is what their
|
||||
* policies ask of anything that fetches automatically.
|
||||
*/
|
||||
class RemoteImage
|
||||
{
|
||||
@@ -63,6 +68,7 @@ class RemoteImage
|
||||
)]],
|
||||
])
|
||||
->withHeaders(['Accept' => 'image/*'])
|
||||
->withUserAgent($this->userAgent())
|
||||
->get($url);
|
||||
} catch (ConnectionException) {
|
||||
$this->fail('unreachable');
|
||||
@@ -94,6 +100,19 @@ class RemoteImage
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* `pireon-admin/1.0 (+https://admin.example.org; media fetcher)`: a product
|
||||
* token without spaces, and the installation's own address to reach it by.
|
||||
*/
|
||||
protected function userAgent(): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s/1.0 (+%s; media fetcher)',
|
||||
Str::slug((string) config('app.name')) ?: 'landing',
|
||||
rtrim((string) config('app.url'), '/'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The address to connect to, once every address the host has is known to
|
||||
* be public.
|
||||
|
||||
Reference in New Issue
Block a user