Skip to content

Commit 51f36b1

Browse files
authored
Merge pull request #4038 from Chriss4123/feature/localhost-secure-context
feat: Add RFC 6761–compliant localhost loopback checks so `secure` cookies work on localhost (fixes: #1676)
2 parents 6b122d7 + 2c3d2ff commit 51f36b1

File tree

5 files changed

+116
-2
lines changed

5 files changed

+116
-2
lines changed

packages/bruno-cli/src/utils/cookies.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const { Cookie, CookieJar } = require('tough-cookie');
22
const each = require('lodash/each');
3+
const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils;
34

45
const cookieJar = new CookieJar();
56

@@ -11,7 +12,9 @@ const addCookieToJar = (setCookieHeader, requestUrl) => {
1112
};
1213

1314
const getCookiesForUrl = (url) => {
14-
return cookieJar.getCookiesSync(url);
15+
return cookieJar.getCookiesSync(url, {
16+
secure: isPotentiallyTrustworthyOrigin(url)
17+
});
1518
};
1619

1720
const getCookieStringForUrl = (url) => {

packages/bruno-electron/src/utils/cookies.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const { Cookie, CookieJar } = require('tough-cookie');
22
const each = require('lodash/each');
33
const moment = require('moment');
4+
const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils;
45

56
const cookieJar = new CookieJar();
67

@@ -12,7 +13,9 @@ const addCookieToJar = (setCookieHeader, requestUrl) => {
1213
};
1314

1415
const getCookiesForUrl = (url) => {
15-
return cookieJar.getCookiesSync(url);
16+
return cookieJar.getCookiesSync(url, {
17+
secure: isPotentiallyTrustworthyOrigin(url)
18+
});
1619
};
1720

1821
const getCookieStringForUrl = (url) => {

packages/bruno-requests/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export { addDigestInterceptor, getOAuth2Token } from './auth';
2+
3+
export * as utils from './utils';
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
const { URL } = require('node:url');
2+
const net = require('node:net');
3+
4+
const isLoopbackV4 = (address) => {
5+
// 127.0.0.0/8: first octet = 127
6+
const octets = address.split('.');
7+
return (
8+
octets.length === 4
9+
) && parseInt(octets[0], 10) === 127;
10+
}
11+
12+
const isLoopbackV6 = (address) => {
13+
// new URL(...) follows the WHATWG URL Standard
14+
// which compresses IPv6 addresses, therefore the IPv6
15+
// loopback address will always be compressed to '[::1]':
16+
// https://url.spec.whatwg.org/#concept-ipv6-serializer
17+
return (address === '::1');
18+
}
19+
20+
const isIpLoopback = (address) => {
21+
if (net.isIPv4(address)) {
22+
return isLoopbackV4(address);
23+
}
24+
25+
if (net.isIPv6(address)) {
26+
return isLoopbackV6(address);
27+
}
28+
29+
return false;
30+
}
31+
32+
const isNormalizedLocalhostTLD = (host) => {
33+
return host.toLowerCase().endsWith('.localhost');
34+
}
35+
36+
const isLocalHostname = (host) => {
37+
return host.toLowerCase() === 'localhost' ||
38+
isNormalizedLocalhostTLD(host);
39+
}
40+
41+
/**
42+
* Removes leading and trailing square brackets if present.
43+
* Adapted from https://github.com/chromium/chromium/blob/main/url/gurl.cc#L440-L448
44+
*
45+
* @param {string} host
46+
* @returns {string}
47+
*/
48+
const hostNoBrackets = (host) => {
49+
if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) {
50+
return host.substring(1, host.length - 1);
51+
}
52+
return host;
53+
}
54+
55+
/**
56+
* Determines if a URL string represents a potentially trustworthy origin.
57+
*
58+
* A URL is considered potentially trustworthy if it:
59+
* - Uses HTTPS, WSS or file schemes
60+
* - Points to a loopback address (IPv4 127.0.0.0/8 or IPv6 ::1)
61+
* - Uses localhost or *.localhost hostnames
62+
*
63+
* @param {string} urlString - The URL to check
64+
* @returns {boolean}
65+
* @see {@link https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin W3C Spec}
66+
*/
67+
const isPotentiallyTrustworthyOrigin = (urlString) => {
68+
let url;
69+
70+
// try ... catch doubles as an opaque origin check
71+
try {
72+
url = new URL(urlString);
73+
} catch (e) {
74+
if (e instanceof TypeError && e.code === 'ERR_INVALID_URL') {
75+
return false;
76+
} else throw e;
77+
}
78+
79+
const scheme = url.protocol.replace(':', '').toLowerCase();
80+
const hostname = hostNoBrackets(
81+
url.hostname
82+
).replace(/\.+$/, '');
83+
84+
if (
85+
scheme === 'https' ||
86+
scheme === 'wss' ||
87+
scheme === 'file' // https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin
88+
) {
89+
return true;
90+
}
91+
92+
// If it's already an IP literal, check if it's a loopback address
93+
if (net.isIP(hostname)) {
94+
return isIpLoopback(hostname);
95+
}
96+
97+
// RFC 6761 states that localhost names will always resolve
98+
// to the respective IP loopback address:
99+
// https://datatracker.ietf.org/doc/html/rfc6761#section-6.3
100+
return isLocalHostname(hostname);
101+
}
102+
103+
module.exports = {
104+
isPotentiallyTrustworthyOrigin
105+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './cookie-utils';

0 commit comments

Comments
 (0)