The three values that change
- session.use_strict_mode goes from 0 to 1. PHP rejects a session ID that matches no existing session in storage and issues a new one. This is the defense against session fixation (the attacker plants a session ID they know on the victim, then waits for them to log in).
- session.cookie_httponly goes from 0 to 1. JavaScript on the page can no longer read the session cookie through document.cookie. The browser still sends it with requests.
- session.cookie_samesite goes from empty to Lax. The browser no longer sends the cookie on requests coming from another site, except top-level navigations with a safe method (clicking a link, a GET redirect).
A default only wins if nobody else decides first
Ini
; php.ini-production, PHP-8.5 branch
session.use_strict_mode = 0
session.cookie_httponly =
session.cookie_samesite =
Ini
; php.ini-production, PHP-8.6 branch
session.use_strict_mode = 1
session.cookie_httponly = 1
session.cookie_samesite = "Lax"
Ini
; docker/php/conf.d/zz-session.ini
session.use_strict_mode = 1
session.cookie_httponly = 1
session.cookie_samesite = "Lax"
; only if all traffic is HTTPS
session.cookie_secure = 1
Dockerfile
FROM php:8.5-fpm
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY docker/php/conf.d/zz-session.ini "$PHP_INI_DIR/conf.d/zz-session.ini"
Symfony: already aligned, unless you turned it off
- NativeSessionStorage forces use_strict_mode to 1 in its constructor, whatever php.ini says.
- FrameworkBundle declares cookie_httponly: true, cookie_samesite: lax and cookie_secure: auto as the defaults of framework.session, and applies them with ini_set() when the session storage is created.
Bash
php bin/console debug:config framework session
What breaks for those who never set anything
- SameSite=Lax: a POST coming from another site arrives without the session cookie. Typical cases are a payment provider returning the user with a POST, SP-initiated SAML SSO (mentioned by the RFC) and forms submitted from another domain. No error shows up; the user is just logged out. As Marco Orta puts it: "They fail silently".
- HttpOnly: any script that reads the session ID from document.cookie gets a string without it. The RFC recommends a separate, non-HttpOnly CSRF token for whatever the front end needs to read.
- Strict mode: an ID supplied from outside (handing a session over between subdomains, for example) is rejected. PHP 8.6's UPGRADING file states that a custom session handler must then implement validateId() and create_sid(), or explicitly set the option back to 0. Passing session_set_save_handler() an object without those two methods is also deprecated in 8.6.
Php
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Payment\PaymentReturnVerifier;
use App\Repository\OrderRepository;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
final class PaymentReturnController
{
public function __construct(
private readonly PaymentReturnVerifier $verifier,
private readonly OrderRepository $orders,
private readonly UrlGeneratorInterface $urls,
) {
}
#[Route('/payment/return', name: 'payment_return', methods: ['POST'])]
public function __invoke(Request $request): RedirectResponse
{
// Cross-site POST: with SameSite=Lax, no session cookie here.
// So this action never touches the session.
if (!$this->verifier->isValid($request)) {
throw new AccessDeniedHttpException('Invalid payment return signature.');
}
$order = $this->orders->getByReference($request->request->getString('reference'));
// 303: the browser follows with a GET, a top-level navigation,
// so the session cookie is sent again.
return new RedirectResponse(
$this->urls->generate('order_confirmation', ['reference' => $order->getReference()]),
Response::HTTP_SEE_OTHER,
);
}
}
Check before the stable release
Bash
# Compiled defaults, without any php.ini
php -n -r 'foreach (["use_strict_mode", "cookie_httponly", "cookie_samesite"] as $k) { echo $k, "=", ini_get("session.$k"), PHP_EOL; }'
# Effective CLI values (often a different php.ini than FPM)
php -i | grep -E 'session\.(use_strict_mode|cookie_httponly|cookie_samesite)'
# What the browser actually receives, on a page that starts the session
curl -sS -o /dev/null -D - https://staging.example.com/login | grep -i '^set-cookie'
The traps
- Keeping the old php.ini and thinking you're covered. The explicit lines in 8.5's php.ini-production cancel the new defaults without any message.
- Switching everything to SameSite=None for a single callback. It reopens cookie sending on every cross-site request, and None requires session.cookie_secure = 1. Fix the flow that needs it instead.
- Starting a session in the POST callback. With no cookie received, PHP creates a new session and sends back a new cookie, which can replace the user's own and log them out when they return from payment.
Sources
- PHP 8.6 Session Defaults: SameSite, HttpOnly, Strict Mode — PHP News, Eric L. Barnes, August 19, 2026
- What's New in PHP 8.6 — Laravel News, Paul Redmond, September 15, 2026 (updated September 24)
- PHP 8.6: What Actually Breaks When You Upgrade — Marco Orta, August 29, 2026 (updated October 15)
- PHP RFC: Secure Session Configuration Defaults — RFC, Jorg Sowa (values, votes, compatibility)
- UPGRADING, PHP-8.6 branch — php-src (session defaults, handlers, deprecations)
- php.ini-production PHP-8.5 and php.ini-production PHP-8.6 — php-src
- PHP 8.6 release timetable and php.net announcements — RC5 and GA schedule
- Session Runtime Configuration — PHP manual
- NativeSessionStorage.php and FrameworkBundle Configuration.php — Symfony 8.1 source code
- Official php Docker image — Configuration section