1111use App \Enum \StorageDiskType ;
1212use App \Exceptions \SecurePaths \InvalidPayloadException ;
1313use App \Exceptions \SecurePaths \InvalidSignatureException ;
14+ use App \Exceptions \SecurePaths \SignatureExpiredException ;
1415use App \Exceptions \SecurePaths \WrongPathException ;
1516use App \Models \Configs ;
17+ use App \Models \Extensions \HasUrlGenerator ;
1618use Illuminate \Contracts \Encryption \DecryptException ;
1719use Illuminate \Http \Request ;
1820use Illuminate \Routing \Controller ;
21+ use Illuminate \Support \Carbon ;
22+ use Illuminate \Support \Collection ;
1923use Illuminate \Support \Facades \Crypt ;
24+ use Illuminate \Support \Facades \Log ;
2025use Illuminate \Support \Facades \Storage ;
2126
2227/**
2328 * Controller responsible for serving files securely.
2429 */
2530class SecurePathController extends Controller
2631{
32+ use HasUrlGenerator;
33+
2734 public function __invoke (Request $ request , ?string $ path )
2835 {
29- if (!$ request ->hasValidSignature ()) {
36+ // In theory we should use the `$request->hasCorrectSignature()` method here.
37+ // However, for some stupid unknown reason, the path value is added to the server Query String.
38+ // This completely invalidates the signature check.
39+ // For example the url http://localhost:8000/image/small2x/c3/3d/c661c594a5a781cd44db06828783.png?expires=1748380289
40+ // will be verified as :
41+ // http://localhost:8000/image/small2x/c3/3d/c661c594a5a781cd44db06828783.png?/image/small2x/c3/3d/c661c594a5a781cd44db06828783.png&expires=1748380289
42+ // which makes the signature check fail as the hmac does not match.
43+ if (!self ::shouldNotUseSignedUrl () && !$ this ->hasCorrectSignature ($ request )) {
44+ Log::error ('Invalid signature for secure path request. Verify that the url generated for the image match. ' , [
45+ 'candidate url ' => $ this ->getUrl ($ request ),
46+ ]);
3047 throw new InvalidSignatureException ();
3148 }
3249
50+ // On the bright side, we can now differentiate between a missing/failed signature and an expired one.
51+ if (!self ::shouldNotUseSignedUrl () && !$ this ->signatureHasNotExpired ($ request )) {
52+ throw new SignatureExpiredException ();
53+ }
54+
3355 if (is_null ($ path )) {
3456 throw new WrongPathException ();
3557 }
@@ -49,4 +71,55 @@ public function __invoke(Request $request, ?string $path)
4971
5072 return response ()->file ($ file );
5173 }
74+
75+ private function getUrl (Request $ request , bool $ absolute = true ): string
76+ {
77+ $ ignore_query = ['signature ' ];
78+
79+ $ url = $ absolute ? $ request ->url () : ('/ ' . $ request ->path ());
80+
81+ $ query_string = '' ;
82+ $ query_string = (new Collection (explode ('& ' , (string ) $ request ->server ->get ('QUERY_STRING ' ))))
83+ ->reject (fn ($ parameter ) => in_array (\Str::before ($ parameter , '= ' ), $ ignore_query , true ))
84+ ->reject (fn ($ parameter ) => count (explode ('= ' , $ parameter )) === 1 ) // Ignore parameters without value => avoid problem above mentionned.
85+ ->join ('& ' );
86+
87+ return rtrim ($ url . '? ' . $ query_string , '? ' );
88+ }
89+
90+ /**
91+ * Determine if the signature from the given request matches the URL.
92+ *
93+ * @param \Illuminate\Http\Request $request
94+ * @param bool $absolute
95+ *
96+ * @return bool
97+ */
98+ private function hasCorrectSignature (Request $ request , bool $ absolute = true ): bool
99+ {
100+ $ original = $ this ->getUrl ($ request , $ absolute );
101+ $ key = new \SensitiveParameterValue (config ('app.key ' ));
102+ if (hash_equals (
103+ hash_hmac ('sha256 ' , $ original , $ key ->getValue ()),
104+ $ request ->query ('signature ' , '' )
105+ )) {
106+ return true ;
107+ }
108+
109+ return false ;
110+ }
111+
112+ /**
113+ * Determine if the expires timestamp from the given request is not from the past.
114+ *
115+ * @param \Illuminate\Http\Request $request
116+ *
117+ * @return bool
118+ */
119+ private function signatureHasNotExpired (Request $ request )
120+ {
121+ $ expires = $ request ->query ('expires ' );
122+
123+ return !($ expires !== null && $ expires !== '' && Carbon::now ()->getTimestamp () > $ expires );
124+ }
52125}
0 commit comments