From 3b581d35c1ef480f8b10b76672057300b7ebc826 Mon Sep 17 00:00:00 2001 From: Dmitry Seregin Date: Fri, 15 Mar 2024 19:07:00 +0300 Subject: [PATCH] AG-28767: added domain value for setting cookie scriptlets Merge in ADGUARD-FILTERS/scriptlets from feature/AG-28767 to master Squashed commit of the following: commit b19341e353f8e91a394699b3e6e6186aec3d2be5 Author: Dmitriy Seregin Date: Fri Mar 15 18:05:54 2024 +0300 fixes commit 3492998224307015177f350dcae9ea6d49a474e1 Author: Dmitriy Seregin Date: Fri Mar 15 13:15:49 2024 +0300 fixed doc commit e55f0c23dd75c0702ec86274673dea368d8ba2fa Author: Dmitriy Seregin Date: Thu Mar 14 15:55:48 2024 +0300 AG-28767: added domain value for setting cookie scriptlets --- CHANGELOG.md | 4 ++- src/helpers/cookie-utils.ts | 8 +++++- src/scriptlets/set-cookie-reload.js | 21 ++++++++++---- src/scriptlets/set-cookie.js | 21 ++++++++++---- src/scriptlets/trusted-set-cookie-reload.js | 27 ++++++++++++++---- src/scriptlets/trusted-set-cookie.js | 26 +++++++++++++---- tests/helpers/cookie-utils.spec.js | 25 ++++++++++------- .../scriptlets/trusted-click-element.test.js | 28 +++++++++---------- 8 files changed, 110 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0211db..51ed70d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic - `json-prune-fetch-response` scriptlet [#361] - `href-sanitizer` scriptlet [#327] - `no-protected-audience` scriptlet [#395] -- multiple redirects can be used as scriptlets [#300]: +- Domain value for setting cookie scriptlets [#389] +- Multiple redirects can be used as scriptlets [#300]: - `amazon-apstag` - `didomi-loader` - `fingerprintjs2` @@ -48,6 +49,7 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic [#404]: https://github.com/AdguardTeam/Scriptlets/issues/404 [#403]: https://github.com/AdguardTeam/Scriptlets/issues/403 [#395]: https://github.com/AdguardTeam/Scriptlets/issues/395 +[#389]: https://github.com/AdguardTeam/Scriptlets/issues/389 [#377]: https://github.com/AdguardTeam/Scriptlets/issues/377 [#361]: https://github.com/AdguardTeam/Scriptlets/issues/361 [#327]: https://github.com/AdguardTeam/Scriptlets/issues/327 diff --git a/src/helpers/cookie-utils.ts b/src/helpers/cookie-utils.ts index 73ace1f4..cc630743 100644 --- a/src/helpers/cookie-utils.ts +++ b/src/helpers/cookie-utils.ts @@ -30,14 +30,16 @@ export const getCookiePath = (rawPath: string): string => { * @param name name argument of *set-cookie-* scriptlets * @param rawValue value argument of *set-cookie-* scriptlets * @param rawPath path argument of *set-cookie-* scriptlets + * @param domainValue domain argument of *set-cookie-* scriptlets * @param shouldEncodeValue if cookie value should be encoded. Default is `true` * * @returns string OR `null` if name or value is invalid */ -export const concatCookieNameValuePath = ( +export const serializeCookie = ( name: string, rawValue: string, rawPath: string, + domainValue = '', shouldEncodeValue = true, ) => { const COOKIE_BREAKER = ';'; @@ -57,6 +59,10 @@ export const concatCookieNameValuePath = ( resultCookie += `; ${path}`; } + if (domainValue) { + resultCookie += `; domain=${domainValue}`; + } + return resultCookie; }; diff --git a/src/scriptlets/set-cookie-reload.js b/src/scriptlets/set-cookie-reload.js index 7afed35c..3562f0cd 100644 --- a/src/scriptlets/set-cookie-reload.js +++ b/src/scriptlets/set-cookie-reload.js @@ -4,7 +4,7 @@ import { nativeIsNaN, isCookieSetWithValue, getLimitedCookieValue, - concatCookieNameValuePath, + serializeCookie, isValidCookiePath, // following helpers should be imported and injected // because they are used by helpers above @@ -15,14 +15,14 @@ import { * @scriptlet set-cookie-reload * * @description - * Sets a cookie with the specified name and value, and path, + * Sets a cookie with the specified name and value, path, and domain, * and reloads the current page after the cookie setting. * If reloading option is not needed, use [set-cookie](#set-cookie) scriptlet. * * ### Syntax * * ```text - * example.org#%#//scriptlet('set-cookie-reload', name, value[, path]) + * example.org#%#//scriptlet('set-cookie-reload', name, value[, path[, domain]]) * ``` * * - `name` — required, cookie name to be set @@ -45,6 +45,8 @@ import { * - `path` — optional, cookie path, defaults to `/`; possible values: * - `/` — root path * - `none` — to set no path at all + * - `domain` — optional, cookie domain, if not set origin will be set as domain, + * if the domain does not match the origin, the cookie will not be set * * > Note that the scriptlet does not encode a cookie name, * > e.g. name 'a:b' will be set as 'a:b' and not as 'a%3Ab'. @@ -59,11 +61,13 @@ import { * example.org#%#//scriptlet('set-cookie-reload', 'gdpr-settings-cookie', '1') * * example.org#%#//scriptlet('set-cookie-reload', 'cookie-set', 'true', 'none') + * + * example.org#%#//scriptlet('set-cookie-reload', 'test', '1', 'none', 'example.org') * ``` * * @added v1.3.14. */ -export function setCookieReload(source, name, value, path = '/') { +export function setCookieReload(source, name, value, path = '/', domain = '') { if (isCookieSetWithValue(document.cookie, name, value)) { return; } @@ -79,7 +83,12 @@ export function setCookieReload(source, name, value, path = '/') { return; } - const cookieToSet = concatCookieNameValuePath(name, validValue, path); + if (!document.location.origin.includes(domain)) { + logMessage(source, `Cookie domain not matched by origin: '${domain}'`); + return; + } + + const cookieToSet = serializeCookie(name, validValue, path, domain); if (!cookieToSet) { logMessage(source, 'Invalid cookie name or value'); return; @@ -109,7 +118,7 @@ setCookieReload.injections = [ nativeIsNaN, isCookieSetWithValue, getLimitedCookieValue, - concatCookieNameValuePath, + serializeCookie, isValidCookiePath, getCookiePath, ]; diff --git a/src/scriptlets/set-cookie.js b/src/scriptlets/set-cookie.js index fb600894..de7f94b1 100644 --- a/src/scriptlets/set-cookie.js +++ b/src/scriptlets/set-cookie.js @@ -4,7 +4,7 @@ import { nativeIsNaN, isCookieSetWithValue, getLimitedCookieValue, - concatCookieNameValuePath, + serializeCookie, isValidCookiePath, // following helpers should be imported and injected // because they are used by helpers above @@ -16,7 +16,7 @@ import { * @scriptlet set-cookie * * @description - * Sets a cookie with the specified name, value, and path. + * Sets a cookie with the specified name, value, path, and domain. * * Related UBO scriptlet: * https://github.com/gorhill/uBlock/wiki/Resources-Library#set-cookiejs- @@ -24,7 +24,7 @@ import { * ### Syntax * * ```text - * example.org#%#//scriptlet('set-cookie', name, value[, path]) + * example.org#%#//scriptlet('set-cookie', name, value[, path[, domain]]) * ``` * * - `name` — required, cookie name to be set @@ -47,6 +47,8 @@ import { * - `path` — optional, cookie path, defaults to `/`; possible values: * - `/` — root path * - `none` — to set no path at all + * - `domain` — optional, cookie domain, if not set origin will be set as domain, + * if the domain does not match the origin, the cookie will not be set * * > Note that the scriptlet does not encode a cookie name, * > e.g. name 'a:b' will be set as 'a:b' and not as 'a%3Ab'. @@ -61,12 +63,14 @@ import { * example.org#%#//scriptlet('set-cookie', 'gdpr-settings-cookie', 'true') * * example.org#%#//scriptlet('set-cookie', 'cookie_consent', 'ok', 'none') + * + * example.org#%#//scriptlet('set-cookie-reload', 'test', '1', 'none', 'example.org') * ``` * * @added v1.2.3. */ /* eslint-enable max-len */ -export function setCookie(source, name, value, path = '/') { +export function setCookie(source, name, value, path = '/', domain = '') { const validValue = getLimitedCookieValue(value); if (validValue === null) { logMessage(source, `Invalid cookie value: '${validValue}'`); @@ -78,7 +82,12 @@ export function setCookie(source, name, value, path = '/') { return; } - const cookieToSet = concatCookieNameValuePath(name, validValue, path); + if (!document.location.origin.includes(domain)) { + logMessage(source, `Cookie domain not matched by origin: '${domain}'`); + return; + } + + const cookieToSet = serializeCookie(name, validValue, path, domain); if (!cookieToSet) { logMessage(source, 'Invalid cookie name or value'); return; @@ -102,7 +111,7 @@ setCookie.injections = [ nativeIsNaN, isCookieSetWithValue, getLimitedCookieValue, - concatCookieNameValuePath, + serializeCookie, isValidCookiePath, getCookiePath, ]; diff --git a/src/scriptlets/trusted-set-cookie-reload.js b/src/scriptlets/trusted-set-cookie-reload.js index ab97f99d..153a063a 100644 --- a/src/scriptlets/trusted-set-cookie-reload.js +++ b/src/scriptlets/trusted-set-cookie-reload.js @@ -3,7 +3,7 @@ import { logMessage, nativeIsNaN, isCookieSetWithValue, - concatCookieNameValuePath, + serializeCookie, isValidCookiePath, parseKeywordValue, getTrustedCookieOffsetMs, @@ -19,14 +19,15 @@ import { * * @description * Sets a cookie with arbitrary name and value, - * and with optional ability to offset cookie attribute 'expires' and set path. + * and with optional ability to offset cookie attribute 'expires', set path + * and set domain. * Also reloads the current page after the cookie setting. * If reloading option is not needed, use the [`trusted-set-cookie` scriptlet](#trusted-set-cookie). * * ### Syntax * * ```text - * example.org#%#//scriptlet('trusted-set-cookie-reload', name, value[, offsetExpiresSec[, path]]) + * example.org#%#//scriptlet('trusted-set-cookie-reload', name, value[, offsetExpiresSec[, path[, domain]]]) * ``` * * - `name` — required, cookie name to be set @@ -43,6 +44,8 @@ import { * - `path` — optional, argument for setting cookie path, defaults to `/`; possible values: * - `/` — root path * - `none` — to set no path at all + * - `domain` — optional, cookie domain, if not set origin will be set as domain, + * if the domain does not match the origin, the cookie will not be set * * > Note that the scriptlet does not encode cookie names and values. * > As a result, if a cookie's name or value includes `;`, @@ -80,11 +83,17 @@ import { * example.org#%#//scriptlet('trusted-set-cookie-reload', 'cmpconsent', 'decline', '', 'none') * ``` * + * 1. Set cookie with domain + * + * ```adblock + * example.org#%#//scriptlet('trusted-set-cookie-reload', 'cmpconsent', 'decline', '', 'none', 'example.org') + * ``` + * * @added v1.7.10. */ /* eslint-enable max-len */ -export function trustedSetCookieReload(source, name, value, offsetExpiresSec = '', path = '/') { +export function trustedSetCookieReload(source, name, value, offsetExpiresSec = '', path = '/', domain = '') { if (typeof name === 'undefined') { logMessage(source, 'Cookie name should be specified'); return; @@ -107,12 +116,18 @@ export function trustedSetCookieReload(source, name, value, offsetExpiresSec = ' return; } - let cookieToSet = concatCookieNameValuePath(name, parsedValue, path, false); + if (!document.location.origin.includes(domain)) { + logMessage(source, `Cookie domain not matched by origin: '${domain}'`); + return; + } + + let cookieToSet = serializeCookie(name, parsedValue, path, domain, false); if (!cookieToSet) { logMessage(source, 'Invalid cookie name or value'); return; } + // TODO: Move this concat to serializeCookie if (offsetExpiresSec) { const parsedOffsetMs = getTrustedCookieOffsetMs(offsetExpiresSec); @@ -150,7 +165,7 @@ trustedSetCookieReload.injections = [ logMessage, nativeIsNaN, isCookieSetWithValue, - concatCookieNameValuePath, + serializeCookie, isValidCookiePath, getTrustedCookieOffsetMs, parseKeywordValue, diff --git a/src/scriptlets/trusted-set-cookie.js b/src/scriptlets/trusted-set-cookie.js index b216703b..591e5cee 100644 --- a/src/scriptlets/trusted-set-cookie.js +++ b/src/scriptlets/trusted-set-cookie.js @@ -3,7 +3,7 @@ import { logMessage, nativeIsNaN, isCookieSetWithValue, - concatCookieNameValuePath, + serializeCookie, isValidCookiePath, parseKeywordValue, getTrustedCookieOffsetMs, @@ -18,12 +18,13 @@ import { * * @description * Sets a cookie with arbitrary name and value, - * and with optional ability to offset cookie attribute 'expires' and set path. + * and with optional ability to offset cookie attribute 'expires', set path + * and set domain. * * ### Syntax * * ```text - * example.org#%#//scriptlet('trusted-set-cookie', name, value[, offsetExpiresSec[, path]]) + * example.org#%#//scriptlet('trusted-set-cookie', name, value[, offsetExpiresSec[, path[, domain]]]) * ``` * * - `name` — required, cookie name to be set @@ -40,6 +41,8 @@ import { * - `path` — optional, argument for setting cookie path, defaults to `/`; possible values: * - `/` — root path * - `none` — to set no path at all + * - `domain` — optional, cookie domain, if not set origin will be set as domain, + * if the domain does not match the origin, the cookie will not be set * * > Note that the scriptlet does not encode cookie names and values. * > As a result, if a cookie's name or value includes `;`, @@ -76,13 +79,18 @@ import { * * ```adblock * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', 'decline', '', 'none') + * + * 1. Set cookie with domain + * + * ```adblock + * example.org#%#//scriptlet('trusted-set-cookie', 'cmpconsent', 'decline', '', 'none', 'example.org') * ``` * * @added v1.7.3. */ /* eslint-enable max-len */ -export function trustedSetCookie(source, name, value, offsetExpiresSec = '', path = '/') { +export function trustedSetCookie(source, name, value, offsetExpiresSec = '', path = '/', domain = '') { if (typeof name === 'undefined') { logMessage(source, 'Cookie name should be specified'); return; @@ -99,12 +107,18 @@ export function trustedSetCookie(source, name, value, offsetExpiresSec = '', pat return; } - let cookieToSet = concatCookieNameValuePath(name, parsedValue, path, false); + if (!document.location.origin.includes(domain)) { + logMessage(source, `Cookie domain not matched by origin: '${domain}'`); + return; + } + + let cookieToSet = serializeCookie(name, parsedValue, path, domain, false); if (!cookieToSet) { logMessage(source, 'Invalid cookie name or value'); return; } + // TODO: Move this concat to serializeCookie if (offsetExpiresSec) { const parsedOffsetMs = getTrustedCookieOffsetMs(offsetExpiresSec); @@ -131,7 +145,7 @@ trustedSetCookie.injections = [ logMessage, nativeIsNaN, isCookieSetWithValue, - concatCookieNameValuePath, + serializeCookie, isValidCookiePath, getTrustedCookieOffsetMs, parseKeywordValue, diff --git a/tests/helpers/cookie-utils.spec.js b/tests/helpers/cookie-utils.spec.js index 70f82438..602e3c4d 100644 --- a/tests/helpers/cookie-utils.spec.js +++ b/tests/helpers/cookie-utils.spec.js @@ -1,6 +1,6 @@ -import { concatCookieNameValuePath } from '../../src/helpers/cookie-utils'; +import { serializeCookie } from '../../src/helpers/cookie-utils'; -describe('concatCookieNameValuePath', () => { +describe('serializeCookie', () => { describe('encode cookie value', () => { test.each([ { @@ -37,44 +37,49 @@ describe('concatCookieNameValuePath', () => { actual: ['abc', 'de;f', ''], expected: 'abc=de%3Bf', }, + // set domain + { + actual: ['test', '1', '', 'example.com'], + expected: 'test=1; domain=example.com', + }, ])('$actual -> $expected', ({ actual, expected }) => { - expect(concatCookieNameValuePath(...actual)).toBe(expected); + expect(serializeCookie(...actual)).toBe(expected); }); }); describe('no cookie value encoding', () => { test.each([ { - actual: ['name', 'value', ''], + actual: ['name', 'value', '', '', false], expected: 'name=value', }, { - actual: ['__test-cookie_expires', 'expires', '/'], + actual: ['__test-cookie_expires', 'expires', '/', '', false], expected: '__test-cookie_expires=expires; path=/', }, { - actual: ['aa::bb::cc', '1', ''], + actual: ['aa::bb::cc', '1', '', '', false], expected: 'aa::bb::cc=1', }, { - actual: ['__w_cc11', '{%22cookies_statistical%22:false%2C%22cookies_ad%22:true}', ''], + actual: ['__w_cc11', '{%22cookies_statistical%22:false%2C%22cookies_ad%22:true}', '', '', false], // do not encode cookie value // https://github.com/AdguardTeam/Scriptlets/issues/311 expected: '__w_cc11={%22cookies_statistical%22:false%2C%22cookies_ad%22:true}', }, // invalid name because of ';' { - actual: ['a;bc', 'def', ''], + actual: ['a;bc', 'def', '', '', false], expected: null, }, // invalid value because of ';' and it is not being encoded { - actual: ['abc', 'de;f', ''], + actual: ['abc', 'de;f', '', '', false], expected: null, }, ])('$actual -> $expected', ({ actual, expected }) => { // explicit 'false' to disable encoding - expect(concatCookieNameValuePath(...actual, false)).toBe(expected); + expect(serializeCookie(...actual)).toBe(expected); }); }); }); diff --git a/tests/scriptlets/trusted-click-element.test.js b/tests/scriptlets/trusted-click-element.test.js index 66838835..252ae505 100644 --- a/tests/scriptlets/trusted-click-element.test.js +++ b/tests/scriptlets/trusted-click-element.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-underscore-dangle, no-console */ import { runScriptlet, clearGlobalProps } from '../helpers'; -import { concatCookieNameValuePath } from '../../src/helpers'; +import { serializeCookie } from '../../src/helpers'; const { test, module } = QUnit; const name = 'trusted-click-element'; @@ -204,7 +204,7 @@ test('Multiple elements clicked, non-ordered render', (assert) => { test('extraMatch - single cookie match, matched', (assert) => { const cookieKey1 = 'first'; - const cookieData = concatCookieNameValuePath(cookieKey1, 'true', '/'); + const cookieData = serializeCookie(cookieKey1, 'true', '/'); document.cookie = cookieData; const EXTRA_MATCH_STR = `cookie:${cookieKey1}`; @@ -232,7 +232,7 @@ test('extraMatch - single cookie match, matched', (assert) => { test('extraMatch - single cookie match, not matched', (assert) => { const cookieKey1 = 'first'; const cookieKey2 = 'second'; - const cookieData = concatCookieNameValuePath(cookieKey1, 'true', '/'); + const cookieData = serializeCookie(cookieKey1, 'true', '/'); document.cookie = cookieData; const EXTRA_MATCH_STR = `cookie:${cookieKey2}`; @@ -260,7 +260,7 @@ test('extraMatch - single cookie match, not matched', (assert) => { test('extraMatch - string+regex cookie input, matched', (assert) => { const cookieKey1 = 'first'; const cookieVal1 = 'true'; - const cookieData1 = concatCookieNameValuePath(cookieKey1, cookieVal1, '/'); + const cookieData1 = serializeCookie(cookieKey1, cookieVal1, '/'); document.cookie = cookieData1; const EXTRA_MATCH_STR = 'cookie:/firs/=true'; @@ -341,13 +341,13 @@ test('extraMatch - single localStorage match, not matched', (assert) => { test('extraMatch - complex string+regex cookie input & whitespaces & comma in regex, matched', (assert) => { const cookieKey1 = 'first'; const cookieVal1 = 'true'; - const cookieData1 = concatCookieNameValuePath(cookieKey1, cookieVal1, '/'); + const cookieData1 = serializeCookie(cookieKey1, cookieVal1, '/'); const cookieKey2 = 'sec'; const cookieVal2 = '1-1'; - const cookieData2 = concatCookieNameValuePath(cookieKey2, cookieVal2, '/'); + const cookieData2 = serializeCookie(cookieKey2, cookieVal2, '/'); const cookieKey3 = 'third'; const cookieVal3 = 'true'; - const cookieData3 = concatCookieNameValuePath(cookieKey3, cookieVal3, '/'); + const cookieData3 = serializeCookie(cookieKey3, cookieVal3, '/'); document.cookie = cookieData1; document.cookie = cookieData2; @@ -378,7 +378,7 @@ test('extraMatch - complex string+regex cookie input & whitespaces & comma in re test('extraMatch - single cookie match + single localStorage match, matched', (assert) => { const cookieKey1 = 'cookieMatch'; - const cookieData = concatCookieNameValuePath(cookieKey1, 'true', '/'); + const cookieData = serializeCookie(cookieKey1, 'true', '/'); document.cookie = cookieData; const itemName = 'itemMatch'; window.localStorage.setItem(itemName, 'value'); @@ -433,7 +433,7 @@ test('extraMatch - single cookie revert, click', (assert) => { test('extraMatch - single cookie with value revert match, should click', (assert) => { const cookieKey = 'clickValue'; const cookieVal = 'true'; - const cookieData = concatCookieNameValuePath(cookieKey, cookieVal, '/'); + const cookieData = serializeCookie(cookieKey, cookieVal, '/'); document.cookie = cookieData; const EXTRA_MATCH_STR = `!cookie:${cookieKey}=false`; @@ -460,7 +460,7 @@ test('extraMatch - single cookie with value revert match, should click', (assert test('extraMatch - single cookie revert match, should not click', (assert) => { const cookieKey = 'doNotClick'; - const cookieData = concatCookieNameValuePath(cookieKey, 'true', '/'); + const cookieData = serializeCookie(cookieKey, 'true', '/'); document.cookie = cookieData; const EXTRA_MATCH_STR = `!cookie:${cookieKey}`; @@ -488,7 +488,7 @@ test('extraMatch - single cookie revert match, should not click', (assert) => { test('extraMatch - single cookie with value revert match, should not click', (assert) => { const cookieKey = 'doNotClickValue'; const cookieVal = 'true'; - const cookieData = concatCookieNameValuePath(cookieKey, cookieVal, '/'); + const cookieData = serializeCookie(cookieKey, cookieVal, '/'); document.cookie = cookieData; const EXTRA_MATCH_STR = `!cookie:${cookieKey}=${cookieVal}`; @@ -591,13 +591,13 @@ test('extraMatch - single cookie match + single localStorage match, revert - cli test('extraMatch - complex string+regex cookie input&whitespaces&comma in regex, revert should not click', (assert) => { const cookieKey1 = 'first'; const cookieVal1 = 'true'; - const cookieData1 = concatCookieNameValuePath(cookieKey1, cookieVal1, '/'); + const cookieData1 = serializeCookie(cookieKey1, cookieVal1, '/'); const cookieKey2 = 'sec'; const cookieVal2 = '1-1'; - const cookieData2 = concatCookieNameValuePath(cookieKey2, cookieVal2, '/'); + const cookieData2 = serializeCookie(cookieKey2, cookieVal2, '/'); const cookieKey3 = 'third'; const cookieVal3 = 'true'; - const cookieData3 = concatCookieNameValuePath(cookieKey3, cookieVal3, '/'); + const cookieData3 = serializeCookie(cookieKey3, cookieVal3, '/'); document.cookie = cookieData1; document.cookie = cookieData2;