The EU General Data Protection Regulation (GDPR) specifies that a person may object to data processing “by automated means using technical specifications”. (GDPR Article 21(5).)

In the context of web browsing, two such technical specifications are the Do Not Track (DNT) and Global Privacy Control (Sec-GPC) HTTP headers, which browsers can be configured to send. Germany’s Berlin Regional Court has recently ruled that under the GDPR, a web browser sending a DNT header provides sufficient notice of such data processing objection by the individual using that browser.

Cookies are widely used across the web to enable session-like behavior over an inherently sessionless protocol (HTTP). Every HTTP cookie optionally has a maximum lifetime and/or an expiration point in time, after which the cookie is considered expired. A cookie where the server specifies neither of these is considered a “session” cookie and will typically be deleted either when the browser is closed normally, or sooner; a cookie where either is specified is requested by the server to be kept until that time has expired, although the actual cookie eviction policy is up to the client.

Cookies are also often used to enable persistent tracking of individual users, which has earned them quite a bad name. This is a particular issue with persistent (non-session) cookies, as those generally have a longer lifetime. Of course, not all cookies are used for tracking purposes, but especially for long-lived cookies it can be very difficult to tell from the outside whether this is the case. (The same is, naturally, true also for session cookies; but since session cookies by their very nature are not persistent, they cannot by themselves be used to track an individual between browser sessions.)

To turn a persistent cookie into a session cookie, the Set-Cookie HTTP response header must be edited to remove both the Expires and the Max-Age settings.

With the Apache web server, this can be done conditional on whether the client sends either the DNT or Sec-GPC headers by using mod_headers together with the If directive. First enable the mod_headers module, and then add to the HTTP server configuration:

<If "req_novary('Sec-GPC') == '1' || req_novary('DNT') == '1'>
  Header always edit Set-Cookie "^([^;]+; *)(Expires=[^;]+;?)(.+)?$" "$1$3"
  Header always edit Set-Cookie "^([^;]+; *)(Max-Age=[0-9]+;?)(.+)?$" "$1$3"
  Header always edit* Set-Cookie "; +" "; "
  Header always edit Set-Cookie "^([^;]+;)* *$" "$1"
  Header always edit Set-Cookie ";$" ""
</If>

(Please do mind the whitespace; it is important. You probably want to copy and paste the above, not re-type it.)

This will, just before the HTTP response is sent over the network (because the early directive is not specified and thus late processing is requested), if in the request the user’s browser sent either a DNT or GPC header indicating a preference not to be tracked, rewrite all Set-Cookie headers in the response to turn any persistent cookies into session cookies by deleting the Expires and Max-Age specifications.

The last three edit statements in the above snippet collapse any double whitespace between fields and delete any lingering whitespace or field separators at the end of the header. The formal Set-Cookie header syntax requires that any semicolons within the cookie value are percent-encoded and that the last attribute’s value is not terminated with a semicolon.

Do note that this can break functionality which relies on the server indicating that a cookie should be evicted by setting it to already having expired, but without also simultaneously clearing the value of the cookie. At least some applications use Max-Age=0 to indicate a request for cookie eviction, which technically is against the formal header syntax (the value for Max-Age is required to be one non-zero digit followed by any number of digits). To special-case that and more closely match only values for Max-Age which are valid according to the header syntax, you can replace the Max-Age line in the snippet above with:

Header always edit Set-Cookie "^([^;]+; *)(Max-Age=[1-9][0-9]*;?)(.+)?$" "$1$3"

Doing so will let Set-Cookie headers through with Max-Age=0, but delete any non-zero Max-Age value specification.