2020/08/14

The SameSite attribute of the Set-Cookie in WebBroker Response

前陣子在陪好友練習 Cookie 操作時,無意間發現瀏覽器跳出一個奇怪的警告訊息:

一查才發現原來是瀏覽器為了防止儲存的 Cookie 被送到其它網站服務器所作的安全機制。

若現在不理它,未來可能會被瀏覽器無視,所以先來了解要如何解決。

SameSite 定義

MDN 中對 sameSite 說明如下:

Values

The SameSite attribute accepts three values:

Lax(一般;預設值)

Cookies are allowed to be sent with top-level navigations and will be sent along with GET request initiated by third party website. This is the default value in modern browsers.

Strict(最嚴格)

Cookies will only be sent in a first-party context and not be sent along with requests initiated by third party websites.

None(不限制)

Cookies will be sent in all contexts, i.e sending cross-origin is allowed.

None used to be the default value, but recent browser versions made Lax the default value to have reasonably robust defense against some classes of cross-site request forgery (CSRF) attacks.

None requires the Secure attribute in latest browser versions. See below for more information.

雖然沒有說明是否為必須設定,而且預設值是 Lax,現在不處理好像也沒關係。

程式解法

依照 iTHome 【Chrome 80將採用新的Cookie安全模型預設無法跨站存取Cookie】報導指出,不是每一家函式庫和語言都有支援 SameSite,像 Delphi 的 WebBroker 就沒有……

PHP 則是到 7.3 版之後有內建支援:

PHP 7.2 以前 =  header(‘Set-Cookie: cross-site-cookie=name; SameSite=None; Secure’);

PHP 7.3 = setcookie(‘cross-site-cookie’, ‘name’, [‘samesite’ => ‘None’, ‘secure’ => true]);

PHP 都有這種問題,那 Delphi 應該也不例外,也就是說,PHP 7.2 以前的寫法很值得參考。

回到 Delphi,和 Cookie 有關的設定在 HTTPApp 單元裡的 TCookie 類別:

其中的 GetHeaderValue 就是 TCookie 兜出等同 PHP 7.2 Response 自訂 header 的內容:

function TCookie.GetHeaderValue: AnsiString; var S: string; begin S := Format('%s=%s; ', [HTTPEncode(FName), HTTPEncode(FValue)]); if Domain <> '' then S := S + Format('domain=%s; ', [Domain]); { do not localize } if Path <> '' then S := S + Format('path=%s; ', [Path]); { do not localize } if Expires > -1 then S := S + Format(FormatDateTime('"expires="' + sDateFormat + ' "GMT; "', Expires), { do not localize } [DayOfWeekStr(Expires), MonthStr(Expires)]); if Secure then S := S + 'secure'; { do not localize } if Copy(S, Length(S) - 1, MaxInt) = '; ' then SetLength(S, Length(S) - 2); Result := AnsiString(S); end;

以這個基礎,改寫如下:

function GetHeaderValue(FName, FValue:string; Domain:string=''; Path:string=''; SameSite: string='Lax'; Expires: TDateTime=-1; Secure: Boolean=False): string; var S: string; begin S := Format('%s=%s; ', [HTTPEncode(FName), HTTPEncode(FValue)]); if Domain <> '' then S := S + Format('domain=%s; ', [Domain]); { do not localize } if Path <> '' then S := S + Format('path=%s; ', [Path]); { do not localize } if Expires > -1 then S := S + Format(FormatDateTime('"expires="' + sDateFormat + ' "GMT; "', Expires), { do not localize } [DayOfWeekStr(Expires), MonthStr(Expires)]); if SameSite='' then S := S + 'SameSite=None;' else S := S + Format('SameSite=%s;', [SameSite]); if Secure then S := S + 'secure'; { do not localize } if Copy(S, Length(S) - 1, MaxInt) = '; ' then SetLength(S, Length(S) - 2); Result := (S); end;

增加了對 SameSite 的支援,對現代的瀏覽器支援又更進一步。

See also

沒有留言:

張貼留言