From 399f91f8b96746eaeda972f0bb98fe38027c8dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0ime=C4=8Dek?= Date: Mon, 21 Oct 2024 14:33:26 +0200 Subject: [PATCH 1/6] Fixed cookie parse for multiple cookies on server --- .changeset/brown-rivers-confess.md | 5 +++++ packages/core/src/storage/CookieStorage.ts | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .changeset/brown-rivers-confess.md diff --git a/.changeset/brown-rivers-confess.md b/.changeset/brown-rivers-confess.md new file mode 100644 index 0000000000..797924bfbe --- /dev/null +++ b/.changeset/brown-rivers-confess.md @@ -0,0 +1,5 @@ +--- +"@ima/core": patch +--- + +Fixed cookie parsing from setCookie header when multiple cookies were sent to server. Previously only the first cookie was parsed while multiple set-cookies could alter the cookie settings diff --git a/packages/core/src/storage/CookieStorage.ts b/packages/core/src/storage/CookieStorage.ts index e0d28ad5d5..aa27bd36fe 100644 --- a/packages/core/src/storage/CookieStorage.ts +++ b/packages/core/src/storage/CookieStorage.ts @@ -266,10 +266,14 @@ export class CookieStorage extends Storage { * header. */ parseFromSetCookieHeader(setCookieHeader: string): void { - const cookie = this.#extractCookie(setCookieHeader); + const cookiesArray = setCookieHeader ? setCookieHeader.split(', ') : []; - if (typeof cookie.name === 'string') { - this.set(cookie.name, cookie.value, cookie.options); + for (const cookie of cookiesArray) { + const cookieItem = this.#extractCookie(cookie); + + if (typeof cookieItem.name === 'string') { + this.set(cookieItem.name, cookieItem.value, cookieItem.options); + } } } From 8759221a282a28ca1721f8c22153656e9c26ce24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0ime=C4=8Dek?= Date: Tue, 22 Oct 2024 14:34:11 +0200 Subject: [PATCH 2/6] Added cookie validity check --- packages/core/src/http/HttpAgentImpl.ts | 12 ++- packages/core/src/storage/CookieStorage.ts | 97 +++++++++++++++++++++- 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/packages/core/src/http/HttpAgentImpl.ts b/packages/core/src/http/HttpAgentImpl.ts index e980a17ebb..24e2107f83 100644 --- a/packages/core/src/http/HttpAgentImpl.ts +++ b/packages/core/src/http/HttpAgentImpl.ts @@ -246,7 +246,7 @@ export class HttpAgentImpl extends HttpAgent { data?: UnknownParameters, options?: Partial ): Promise> { - const optionsWithDefault = this._prepareOptions(options); + const optionsWithDefault = this._prepareOptions(options, url); if (optionsWithDefault.cache) { const cachedData = this._getCachedData(method, url, data); @@ -433,7 +433,8 @@ export class HttpAgentImpl extends HttpAgent { * internally. */ _prepareOptions( - options: Partial = {} + options: Partial = {}, + url: string ): HttpAgentRequestOptions { const composedOptions = { ...this._defaultRequestOptions, @@ -455,7 +456,7 @@ export class HttpAgentImpl extends HttpAgent { if (composedOptions.fetchOptions?.credentials === 'include') { // mock default browser behavior for server-side (sending cookie with a fetch request) composedOptions.fetchOptions.headers.Cookie = - this._cookie.getCookiesStringForCookieHeader(); + this._cookie.getCookiesStringForCookieHeader(url); } return composedOptions; @@ -500,7 +501,10 @@ export class HttpAgentImpl extends HttpAgent { const receivedCookies = agentResponse.headersRaw.get('set-cookie'); if (receivedCookies) { - this._cookie.parseFromSetCookieHeader(receivedCookies); + this._cookie.parseFromSetCookieHeader( + receivedCookies, + agentResponse.params.url + ); } } } diff --git a/packages/core/src/storage/CookieStorage.ts b/packages/core/src/storage/CookieStorage.ts index aa27bd36fe..2e50652c54 100644 --- a/packages/core/src/storage/CookieStorage.ts +++ b/packages/core/src/storage/CookieStorage.ts @@ -19,6 +19,12 @@ const MAX_EXPIRE_DATE = new Date('Fri, 31 Dec 9999 23:59:59 UTC'); */ const COOKIE_SEPARATOR = '; '; +/** + * Separator used to separate cookie declarations in the `Set-Cookie` HTTP + * header. + */ +const SERVER_COOKIE_SEPARATOR = ', '; + export type CookieOptions = { domain?: string; expires?: Date; @@ -96,6 +102,61 @@ export class CookieStorage extends Storage { return [Window, Request, Response]; } + /** + * Filters invalid cookies based on the provided validate options. + * We try to check validity of the domain based on secure, path and + * domain definitions. + */ + static validateCookieSecurity(cookie: Cookie, url: string): boolean { + const { pathname, hostname, protocol } = new URL(url); + const secure = protocol === 'https:'; + + /** + * Don't allow setting secure cookies without the secure + * defined in the validate options. + */ + if ( + typeof cookie.options.secure === 'boolean' && + secure !== cookie.options.secure + ) { + return false; + } + + /** + * Don't allow setting cookies with a path that doesn't start with + * the path defined in the validate options. + */ + if ( + typeof cookie.options.path === 'string' && + !cookie.options.path.endsWith(pathname) + ) { + return false; + } + + /** + * Final domain check, since this check uses guard clauses, it HAS + * TO BE THE LAST CHECK!. + */ + if (cookie.options.domain) { + const cookieDomain = cookie.options.domain.toLowerCase(); + + // Check subdomain match + if (cookieDomain === hostname) { + return true; + } + + // Check if cookieDomain is a subdomain of hostname + if (cookieDomain.endsWith(hostname)) { + return true; + } + + // Domains don't match + return false; + } + + return true; + } + /** * Initializes the cookie storage. * @@ -238,15 +299,29 @@ export class CookieStorage extends Storage { * Returns all cookies in this storage serialized to a string compatible * with the `Cookie` HTTP header. * + * When `url` is provided, the method validates the cookie security based on + * the `url` and the cookie's domain, path, and secure attributes. + * * @return All cookies in this storage serialized to a string * compatible with the `Cookie` HTTP header. */ - getCookiesStringForCookieHeader(): string { + getCookiesStringForCookieHeader(url?: string): string { const cookieStrings = []; for (const cookieName of this._storage.keys()) { const cookieItem = this._storage.get(cookieName); + // Validate cookie security + if ( + url && + cookieItem && + !CookieStorage.validateCookieSecurity(cookieItem, url) + ) { + // eslint-disable-next-line no-console + console.log('cookie not valid', cookieItem); + continue; + } + cookieStrings.push( this.#generateCookieString(cookieName, cookieItem!.value, {}) ); @@ -258,6 +333,9 @@ export class CookieStorage extends Storage { /** * Parses cookies from the provided `Set-Cookie` HTTP header value. * + * When `url` is provided, the method validates the cookie security based on + * the `url` and the cookie's domain, path, and secure attributes. + * * The parsed cookies will be set to the internal storage, and the current * HTTP response (via the `Set-Cookie` HTTP header) if at the server * side, or the browser (via the `document.cookie` property). @@ -265,12 +343,25 @@ export class CookieStorage extends Storage { * @param setCookieHeader The value of the `Set-Cookie` HTTP * header. */ - parseFromSetCookieHeader(setCookieHeader: string): void { - const cookiesArray = setCookieHeader ? setCookieHeader.split(', ') : []; + parseFromSetCookieHeader(setCookieHeader: string, url?: string): void { + const cookiesArray = setCookieHeader + ? setCookieHeader.split(SERVER_COOKIE_SEPARATOR) + : []; for (const cookie of cookiesArray) { const cookieItem = this.#extractCookie(cookie); + // Validate cookie security + if ( + url && + cookieItem && + !CookieStorage.validateCookieSecurity(cookieItem, url) + ) { + // eslint-disable-next-line no-console + console.log('cookie not valid', cookieItem); + continue; + } + if (typeof cookieItem.name === 'string') { this.set(cookieItem.name, cookieItem.value, cookieItem.options); } From b82b8b516c68131447f34eb9f0747d5c6f2b93d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0ime=C4=8Dek?= Date: Wed, 23 Oct 2024 14:35:29 +0200 Subject: [PATCH 3/6] Added tests and fixed validation --- packages/core/src/storage/CookieStorage.ts | 66 ++-- .../storage/__tests__/CookieStorageSpec.ts | 317 ++++++++++++++++++ 2 files changed, 358 insertions(+), 25 deletions(-) diff --git a/packages/core/src/storage/CookieStorage.ts b/packages/core/src/storage/CookieStorage.ts index 2e50652c54..8236f53791 100644 --- a/packages/core/src/storage/CookieStorage.ts +++ b/packages/core/src/storage/CookieStorage.ts @@ -103,7 +103,7 @@ export class CookieStorage extends Storage { } /** - * Filters invalid cookies based on the provided validate options. + * Filters invalid cookies based on the provided url. * We try to check validity of the domain based on secure, path and * domain definitions. */ @@ -124,34 +124,50 @@ export class CookieStorage extends Storage { /** * Don't allow setting cookies with a path that doesn't start with - * the path defined in the validate options. + * the path defined in the validate options. Root path is always valid. */ if ( typeof cookie.options.path === 'string' && - !cookie.options.path.endsWith(pathname) + cookie.options.path !== '/' ) { - return false; + const pathChunks = pathname.split('/'); + const cookiePathChunks = cookie.options.path.split('/'); + + /** + * Compare the path chunks of the request path and the cookie path. + */ + for (let i = 0; i < cookiePathChunks.length; i++) { + /** + * There are no more path chunks to compare, so the cookie path is a + * prefix of the request path. + */ + if (cookiePathChunks[i] === undefined) { + break; + } + + /** + * The path chunks don't match, the cookie path is not a prefix of + * the request path. + */ + if (pathChunks[i] !== cookiePathChunks[i]) { + return false; + } + } } /** - * Final domain check, since this check uses guard clauses, it HAS - * TO BE THE LAST CHECK!. + * Domain security check, we also check for subdomain match. */ if (cookie.options.domain) { const cookieDomain = cookie.options.domain.toLowerCase(); - - // Check subdomain match - if (cookieDomain === hostname) { - return true; - } - - // Check if cookieDomain is a subdomain of hostname - if (cookieDomain.endsWith(hostname)) { - return true; - } - - // Domains don't match - return false; + const normalizedCookieDomain = cookieDomain.startsWith('.') + ? cookieDomain.slice(1) + : cookieDomain; + + return normalizedCookieDomain === hostname || + hostname.endsWith(normalizedCookieDomain) + ? true + : false; } return true; @@ -311,14 +327,14 @@ export class CookieStorage extends Storage { for (const cookieName of this._storage.keys()) { const cookieItem = this._storage.get(cookieName); - // Validate cookie security + /** + * Skip cookies that are not secure for the provided url. + */ if ( url && cookieItem && !CookieStorage.validateCookieSecurity(cookieItem, url) ) { - // eslint-disable-next-line no-console - console.log('cookie not valid', cookieItem); continue; } @@ -351,14 +367,14 @@ export class CookieStorage extends Storage { for (const cookie of cookiesArray) { const cookieItem = this.#extractCookie(cookie); - // Validate cookie security + /** + * Skip cookies that are not secure for the provided url. + */ if ( url && cookieItem && !CookieStorage.validateCookieSecurity(cookieItem, url) ) { - // eslint-disable-next-line no-console - console.log('cookie not valid', cookieItem); continue; } diff --git a/packages/core/src/storage/__tests__/CookieStorageSpec.ts b/packages/core/src/storage/__tests__/CookieStorageSpec.ts index 3ba0e52a15..794dc8273a 100644 --- a/packages/core/src/storage/__tests__/CookieStorageSpec.ts +++ b/packages/core/src/storage/__tests__/CookieStorageSpec.ts @@ -342,4 +342,321 @@ describe('ima.storage.CookieStorage', () => { expect(cookie['_storage'].size).toBe(2); }); }); + + describe('validateCookieSecurity', () => { + it('should return true for a cookie with matching domain and path', () => { + const cookie = { + value: 'test', + options: { domain: 'example.com', path: '/' }, + }; + const url = 'http://example.com/'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + }); + + it('should return true for a cookie with matching subdomain', () => { + const cookie = { + value: 'test', + options: { domain: '.example.com', path: '/' }, + }; + const url = 'http://sub.example.com/'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + }); + + it('should return false for a cookie with non-matching domain', () => { + const cookie = { + value: 'test', + options: { domain: 'example.com', path: '/' }, + }; + const url = 'http://otherdomain.com/'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(false); + }); + + it('should return false for a secure cookie on non-https URL', () => { + const cookie = { + value: 'test', + options: { domain: 'example.com', path: '/', secure: true }, + }; + const url = 'http://example.com/'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(false); + }); + + it('should return true for a secure cookie on https URL', () => { + const cookie = { + value: 'test', + options: { domain: 'example.com', path: '/', secure: true }, + }; + const url = 'https://example.com/'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + }); + + it('should return false for a cookie with non-matching path', () => { + const cookie = { + value: 'test', + options: { domain: 'example.com', path: '/app' }, + }; + const url = 'http://example.com/'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(false); + }); + + it('should return true for a cookie with matching path prefix', () => { + const cookie = { + value: 'test', + options: { domain: 'example.com', path: '/app' }, + }; + const url = 'http://example.com/app/subpath'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + }); + + it('should return true for a cookie without domain restriction', () => { + const cookie = { + value: 'test', + options: { path: '/' }, + }; + const url = 'http://example.com/'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + }); + + it('should return false for a cookie with ip address domain on non-matching ip', () => { + const cookie = { + value: 'test', + options: { domain: '192.168.1.1', path: '/' }, + }; + const url = 'http://192.168.1.2/'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(false); + }); + + it('should return true for a cookie with ip address domain on matching ip', () => { + const cookie = { + value: 'test', + options: { domain: '192.168.1.1', path: '/' }, + }; + const url = 'http://192.168.1.1/'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + }); + + describe('options that do not affect validation', () => { + it('should return true regardless of httpOnly flag', () => { + const cookie: Cookie = { + value: 'test', + options: { + domain: 'example.com', + path: '/', + httpOnly: true, + sameSite: 'strict', + }, + }; + const url = 'http://example.com/'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + + cookie.options.httpOnly = false; + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + }); + + it('should return true regardless of sameSite value', () => { + const cookie: Cookie = { + value: 'test', + options: { + domain: 'example.com', + path: '/', + sameSite: 'strict', + }, + }; + const url = 'http://example.com/'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + + cookie.options.sameSite = 'lax'; + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + + cookie.options.sameSite = 'none'; + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + }); + + it('should return true regardless of partitioned flag', () => { + const cookie = { + value: 'test', + options: { + domain: 'example.com', + path: '/', + partitioned: true, + }, + }; + const url = 'http://example.com/'; + + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + + cookie.options.partitioned = false; + expect(CookieStorage.validateCookieSecurity(cookie, url)).toBe(true); + }); + }); + + describe('complex combinations', () => { + it('should validate multiple security attributes together', () => { + const cookie: Cookie = { + value: 'test', + options: { + domain: 'example.com', + path: '/app', + secure: true, + httpOnly: true, + sameSite: 'strict', + }, + }; + + // Should fail - wrong protocol + expect( + CookieStorage.validateCookieSecurity(cookie, 'http://example.com/app') + ).toBe(false); + + // Should fail - wrong path + expect( + CookieStorage.validateCookieSecurity( + cookie, + 'https://example.com/other' + ) + ).toBe(false); + + // Should fail - wrong domain + expect( + CookieStorage.validateCookieSecurity(cookie, 'https://other.com/app') + ).toBe(false); + + // Should pass - all conditions met + expect( + CookieStorage.validateCookieSecurity( + cookie, + 'https://example.com/app' + ) + ).toBe(true); + }); + + it('should handle subdomain with path and secure combinations', () => { + const cookie: Cookie = { + value: 'test', + options: { + domain: '.example.com', + path: '/api', + secure: true, + httpOnly: true, + sameSite: 'strict', + }, + }; + + // Should fail - not secure + expect( + CookieStorage.validateCookieSecurity( + cookie, + 'http://sub.example.com/api' + ) + ).toBe(false); + + // Should fail - wrong path + expect( + CookieStorage.validateCookieSecurity( + cookie, + 'https://sub.example.com/app' + ) + ).toBe(false); + + // Should pass - subdomain with correct path and protocol + expect( + CookieStorage.validateCookieSecurity( + cookie, + 'https://sub.example.com/api' + ) + ).toBe(true); + + // Should pass - main domain with correct path and protocol + expect( + CookieStorage.validateCookieSecurity( + cookie, + 'https://example.com/api' + ) + ).toBe(true); + }); + + it('should validate path hierarchy correctly', () => { + const cookie = { + value: 'test', + options: { + domain: 'example.com', + path: '/api', + }, + }; + + // Should pass - exact path match + expect( + CookieStorage.validateCookieSecurity(cookie, 'http://example.com/api') + ).toBe(true); + + // Should pass - subpath + expect( + CookieStorage.validateCookieSecurity( + cookie, + 'http://example.com/api/users' + ) + ).toBe(true); + + // Should fail - different path + expect( + CookieStorage.validateCookieSecurity(cookie, 'http://example.com/app') + ).toBe(false); + + // Should fail - partial path match but not at boundary + expect( + CookieStorage.validateCookieSecurity( + cookie, + 'http://example.com/api-docs' + ) + ).toBe(false); + }); + + it('should handle root path with various domains', () => { + const cookie = { + value: 'test', + options: { + domain: '.example.com', + path: '/', + }, + }; + + // Should pass - root domain + expect( + CookieStorage.validateCookieSecurity(cookie, 'http://example.com/') + ).toBe(true); + + // Should pass - subdomain with any path + expect( + CookieStorage.validateCookieSecurity( + cookie, + 'http://sub.example.com/any/path' + ) + ).toBe(true); + + // Should pass - deeper subdomain + expect( + CookieStorage.validateCookieSecurity( + cookie, + 'http://deep.sub.example.com/' + ) + ).toBe(true); + + // Should fail - different domain + expect( + CookieStorage.validateCookieSecurity(cookie, 'http://example.org/') + ).toBe(false); + }); + }); + }); }); From 2f30ad3bd8b51b9a11266f945bf6c6ceda3555b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0ime=C4=8Dek?= Date: Wed, 23 Oct 2024 14:49:16 +0200 Subject: [PATCH 4/6] Fixed tests --- packages/core/src/http/HttpAgentImpl.ts | 4 ++-- .../src/http/__tests__/HttpAgentImplSpec.ts | 8 +++---- packages/core/src/storage/CookieStorage.ts | 22 +++++++++---------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/core/src/http/HttpAgentImpl.ts b/packages/core/src/http/HttpAgentImpl.ts index 24e2107f83..af715f877b 100644 --- a/packages/core/src/http/HttpAgentImpl.ts +++ b/packages/core/src/http/HttpAgentImpl.ts @@ -498,9 +498,9 @@ export class HttpAgentImpl extends HttpAgent { */ _setCookiesFromResponse(agentResponse: HttpAgentResponse): void { if (agentResponse.headersRaw) { - const receivedCookies = agentResponse.headersRaw.get('set-cookie'); + const receivedCookies = agentResponse.headersRaw.getSetCookie(); - if (receivedCookies) { + if (receivedCookies.length > 0) { this._cookie.parseFromSetCookieHeader( receivedCookies, agentResponse.params.url diff --git a/packages/core/src/http/__tests__/HttpAgentImplSpec.ts b/packages/core/src/http/__tests__/HttpAgentImplSpec.ts index 0377a6115e..005a3c2455 100644 --- a/packages/core/src/http/__tests__/HttpAgentImplSpec.ts +++ b/packages/core/src/http/__tests__/HttpAgentImplSpec.ts @@ -71,11 +71,9 @@ describe('ima.core.http.HttpAgentImpl', () => { 'set-cookie': ['cookie1=cookie1', 'cookie2=cookie2'], }, // @ts-ignore - headersRaw: new Map( - Object.entries({ - 'set-cookie': ['cookie1=cookie1', 'cookie2=cookie2'], - }) - ), + headersRaw: new Headers({ + 'set-cookie': ['cookie1=cookie1', 'cookie2=cookie2'], + }), }; }); diff --git a/packages/core/src/storage/CookieStorage.ts b/packages/core/src/storage/CookieStorage.ts index 8236f53791..7889172a58 100644 --- a/packages/core/src/storage/CookieStorage.ts +++ b/packages/core/src/storage/CookieStorage.ts @@ -19,12 +19,6 @@ const MAX_EXPIRE_DATE = new Date('Fri, 31 Dec 9999 23:59:59 UTC'); */ const COOKIE_SEPARATOR = '; '; -/** - * Separator used to separate cookie declarations in the `Set-Cookie` HTTP - * header. - */ -const SERVER_COOKIE_SEPARATOR = ', '; - export type CookieOptions = { domain?: string; expires?: Date; @@ -356,13 +350,17 @@ export class CookieStorage extends Storage { * HTTP response (via the `Set-Cookie` HTTP header) if at the server * side, or the browser (via the `document.cookie` property). * - * @param setCookieHeader The value of the `Set-Cookie` HTTP - * header. + * @param cookiesString The value of the `Set-Cookie` HTTP + * header. When there are multiple cookies, the value can be + * provided as an array of strings. */ - parseFromSetCookieHeader(setCookieHeader: string, url?: string): void { - const cookiesArray = setCookieHeader - ? setCookieHeader.split(SERVER_COOKIE_SEPARATOR) - : []; + parseFromSetCookieHeader( + cookiesString: string | string[], + url?: string + ): void { + const cookiesArray = Array.isArray(cookiesString) + ? cookiesString + : [cookiesString]; for (const cookie of cookiesArray) { const cookieItem = this.#extractCookie(cookie); From 7da74772748c862d14338d992b220fa7e6527375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0ime=C4=8Dek?= Date: Wed, 23 Oct 2024 16:31:23 +0200 Subject: [PATCH 5/6] Added validation settings --- packages/core/src/http/HttpAgent.ts | 1 + packages/core/src/http/HttpAgentImpl.ts | 8 ++++++-- .../create-ima-app/template/ts/app/config/settings.ts | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/src/http/HttpAgent.ts b/packages/core/src/http/HttpAgent.ts index 709196b777..6b67c3fb68 100644 --- a/packages/core/src/http/HttpAgent.ts +++ b/packages/core/src/http/HttpAgent.ts @@ -39,6 +39,7 @@ export interface HttpAgentRequestOptions { ) => HttpAgentResponse)[]; abortController?: AbortController; keepSensitiveHeaders?: boolean; + validateCookies?: boolean; } /** diff --git a/packages/core/src/http/HttpAgentImpl.ts b/packages/core/src/http/HttpAgentImpl.ts index af715f877b..5b6184207b 100644 --- a/packages/core/src/http/HttpAgentImpl.ts +++ b/packages/core/src/http/HttpAgentImpl.ts @@ -456,7 +456,9 @@ export class HttpAgentImpl extends HttpAgent { if (composedOptions.fetchOptions?.credentials === 'include') { // mock default browser behavior for server-side (sending cookie with a fetch request) composedOptions.fetchOptions.headers.Cookie = - this._cookie.getCookiesStringForCookieHeader(url); + this._cookie.getCookiesStringForCookieHeader( + options.validateCookies ? url : undefined + ); } return composedOptions; @@ -503,7 +505,9 @@ export class HttpAgentImpl extends HttpAgent { if (receivedCookies.length > 0) { this._cookie.parseFromSetCookieHeader( receivedCookies, - agentResponse.params.url + this._defaultRequestOptions.validateCookies + ? agentResponse.params.url + : undefined ); } } diff --git a/packages/create-ima-app/template/ts/app/config/settings.ts b/packages/create-ima-app/template/ts/app/config/settings.ts index 48759cda2f..af942f3799 100644 --- a/packages/create-ima-app/template/ts/app/config/settings.ts +++ b/packages/create-ima-app/template/ts/app/config/settings.ts @@ -24,6 +24,7 @@ export const initSettings: InitSettingsFunction = (ns, oc, config) => { 'Accept-Language': config.$Language, }, }, + validateCookies: true, // Validate cookies when parsing from Set-Cookie header and when sending cookies from the server. cache: true, // if value exists in cache then returned it else make request to remote server. }, cacheOptions: { From f5f4be879269141a2bee1f18fe2086ecd056c9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0ime=C4=8Dek?= Date: Fri, 25 Oct 2024 09:57:33 +0200 Subject: [PATCH 6/6] Added changeset --- .changeset/flat-beds-travel.md | 6 ++++++ packages/create-ima-app/template/ts/app/config/settings.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/flat-beds-travel.md diff --git a/.changeset/flat-beds-travel.md b/.changeset/flat-beds-travel.md new file mode 100644 index 0000000000..4167889fae --- /dev/null +++ b/.changeset/flat-beds-travel.md @@ -0,0 +1,6 @@ +--- +"create-ima-app": minor +"@ima/core": minor +--- + +Added new settings `validateCookies` to enable/disable cookie validation. It validates cookie options and request url before saving cookie or sending it to the server. This means that path, subdomain and secure options must match between the request url and the cookie, otherwise the cookie is not saved or sent. diff --git a/packages/create-ima-app/template/ts/app/config/settings.ts b/packages/create-ima-app/template/ts/app/config/settings.ts index af942f3799..6a927f4f34 100644 --- a/packages/create-ima-app/template/ts/app/config/settings.ts +++ b/packages/create-ima-app/template/ts/app/config/settings.ts @@ -24,7 +24,7 @@ export const initSettings: InitSettingsFunction = (ns, oc, config) => { 'Accept-Language': config.$Language, }, }, - validateCookies: true, // Validate cookies when parsing from Set-Cookie header and when sending cookies from the server. + validateCookies: false, // Validate cookies when parsing from Set-Cookie header and when sending cookies from the server. cache: true, // if value exists in cache then returned it else make request to remote server. }, cacheOptions: {