diff --git a/packages/bruno-cli/src/utils/cookies.js b/packages/bruno-cli/src/utils/cookies.js index acb58b505..01a82316b 100644 --- a/packages/bruno-cli/src/utils/cookies.js +++ b/packages/bruno-cli/src/utils/cookies.js @@ -1,5 +1,6 @@ const { Cookie, CookieJar } = require('tough-cookie'); const each = require('lodash/each'); +const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils; const cookieJar = new CookieJar(); @@ -11,7 +12,9 @@ const addCookieToJar = (setCookieHeader, requestUrl) => { }; const getCookiesForUrl = (url) => { - return cookieJar.getCookiesSync(url); + return cookieJar.getCookiesSync(url, { + secure: isPotentiallyTrustworthyOrigin(url) + }); }; const getCookieStringForUrl = (url) => { diff --git a/packages/bruno-electron/src/utils/cookies.js b/packages/bruno-electron/src/utils/cookies.js index 7585e9a8a..7f3751eaf 100644 --- a/packages/bruno-electron/src/utils/cookies.js +++ b/packages/bruno-electron/src/utils/cookies.js @@ -1,6 +1,7 @@ const { Cookie, CookieJar } = require('tough-cookie'); const each = require('lodash/each'); const moment = require('moment'); +const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils; const cookieJar = new CookieJar(); @@ -12,7 +13,9 @@ const addCookieToJar = (setCookieHeader, requestUrl) => { }; const getCookiesForUrl = (url) => { - return cookieJar.getCookiesSync(url); + return cookieJar.getCookiesSync(url, { + secure: isPotentiallyTrustworthyOrigin(url) + }); }; const getCookieStringForUrl = (url) => { diff --git a/packages/bruno-requests/src/index.ts b/packages/bruno-requests/src/index.ts index 5513916c5..01850f3e4 100644 --- a/packages/bruno-requests/src/index.ts +++ b/packages/bruno-requests/src/index.ts @@ -1 +1,3 @@ export { addDigestInterceptor, getOAuth2Token } from './auth'; + +export * as utils from './utils'; diff --git a/packages/bruno-requests/src/utils/cookie-utils.js b/packages/bruno-requests/src/utils/cookie-utils.js new file mode 100644 index 000000000..6a1a5ac57 --- /dev/null +++ b/packages/bruno-requests/src/utils/cookie-utils.js @@ -0,0 +1,105 @@ +const { URL } = require('node:url'); +const net = require('node:net'); + +const isLoopbackV4 = (address) => { + // 127.0.0.0/8: first octet = 127 + const octets = address.split('.'); + return ( + octets.length === 4 + ) && parseInt(octets[0], 10) === 127; +} + +const isLoopbackV6 = (address) => { + // new URL(...) follows the WHATWG URL Standard + // which compresses IPv6 addresses, therefore the IPv6 + // loopback address will always be compressed to '[::1]': + // https://url.spec.whatwg.org/#concept-ipv6-serializer + return (address === '::1'); +} + +const isIpLoopback = (address) => { + if (net.isIPv4(address)) { + return isLoopbackV4(address); + } + + if (net.isIPv6(address)) { + return isLoopbackV6(address); + } + + return false; +} + +const isNormalizedLocalhostTLD = (host) => { + return host.toLowerCase().endsWith('.localhost'); +} + +const isLocalHostname = (host) => { + return host.toLowerCase() === 'localhost' || + isNormalizedLocalhostTLD(host); +} + +/** + * Removes leading and trailing square brackets if present. + * Adapted from https://github.com/chromium/chromium/blob/main/url/gurl.cc#L440-L448 + * + * @param {string} host + * @returns {string} + */ +const hostNoBrackets = (host) => { + if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) { + return host.substring(1, host.length - 1); + } + return host; +} + +/** + * Determines if a URL string represents a potentially trustworthy origin. + * + * A URL is considered potentially trustworthy if it: + * - Uses HTTPS, WSS or file schemes + * - Points to a loopback address (IPv4 127.0.0.0/8 or IPv6 ::1) + * - Uses localhost or *.localhost hostnames + * + * @param {string} urlString - The URL to check + * @returns {boolean} + * @see {@link https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin W3C Spec} + */ +const isPotentiallyTrustworthyOrigin = (urlString) => { + let url; + + // try ... catch doubles as an opaque origin check + try { + url = new URL(urlString); + } catch (e) { + if (e instanceof TypeError && e.code === 'ERR_INVALID_URL') { + return false; + } else throw e; + } + + const scheme = url.protocol.replace(':', '').toLowerCase(); + const hostname = hostNoBrackets( + url.hostname + ).replace(/\.+$/, ''); + + if ( + scheme === 'https' || + scheme === 'wss' || + scheme === 'file' // https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin + ) { + return true; + } + + // If it's already an IP literal, check if it's a loopback address + if (net.isIP(hostname)) { + return isIpLoopback(hostname); + } + + // RFC 6761 states that localhost names will always resolve + // to the respective IP loopback address: + // https://datatracker.ietf.org/doc/html/rfc6761#section-6.3 + return isLocalHostname(hostname); +} + +module.exports = { + isPotentiallyTrustworthyOrigin +}; \ No newline at end of file diff --git a/packages/bruno-requests/src/utils/index.ts b/packages/bruno-requests/src/utils/index.ts new file mode 100644 index 000000000..dd94dd186 --- /dev/null +++ b/packages/bruno-requests/src/utils/index.ts @@ -0,0 +1 @@ +export * from './cookie-utils';