Repository: salesforce/tough-cookie
Pull request #498 (2025-02-16 19:08:26)

feat: Add RFC 6761–compliant localhost loopback checks so secure cookies work on localhost (fixes: #382)
fixes: [#382](https://github.com/salesforce/tough-cookie/issues/382)

# Description

This commit extends how cookies are treated in secure contexts by fully recognizing `localhost` and loopback IPs as trustworthy origins, matching the de facto behavior of all modern browsers and RFC 6761. Previously, `tough-cookie` defaulted `secure` to `true` only for `https:` and `wss:` URLs, causing cookies with `secure` to not work with `localhost`.

### What Changed

1. **New `secureContext.ts`**  
   - Implements `isPotentiallyTrustworthy(url)` by checking:
     - Schemes: `https` or `wss`
     - Loopback IPs: `127.0.0.1/8` and `::1`  
     - Hostnames: `localhost` and `*.localhost`  
   - Adapted from Chromium’s `IsLocalhost`, `IsLoopback` and `HostNoBracketsPiece`, located at:
     - [`IsLocalhost`](https://github.com/chromium/chromium/blob/main/net/base/url_util.cc#L457-L551)
     - [`IsLoopback`](https://github.com/chromium/chromium/blob/main/net/base/ip_address.cc#L228-L243)
     - [`HostNoBracketsPiece`](https://github.com/chromium/chromium/blob/main/url/gurl.cc#L440-L448)

2. **`cookieJar.ts` Update**
  - Replaced the old check which only accounts for `https` and `wss` schemes:
```ts
const secure =
  context.protocol &&
  (context.protocol == 'https:' || context.protocol == 'wss:')
```
   - with the new `isPotentiallyTrustworthy` function which accounts for secure schemes, `localhost` addresses and loopback addresses.

No existing functionality is removed. Where the old check would return true, so does the new check. The only added functionality is treating `localhost` addresses and loopback addresses as secure contexts which was previously not present.

All modern browsers (Chrome et. al) support secure cookies on localhost so it only makes sense `tough-cookie` supports this too.

Chriss4123: @colincasey I've completed all the requested changes and also added a comment linking a potentially trustworthy origin and the notion of a "secure" connection in [draft-ietf-httpbis-rfc6265bis-19](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-19#section-5.8.3-2.1.2.3).

I'm ready for you to re-review the PR.
Chriss4123: Will look at the integration tests later and find what is causing these issues. Will let you know once all is good.
colincasey: Thanks @Chriss4123, I'll hold off on approval until it's clearer why `jsdom` tests would fail with this code included. 
Chriss4123: @colincasey The reason `jsdom` tests fail is because they expect `Secure` cookies to **not** work on localhost, as seen [here](https://github.com/jsdom/jsdom/blob/8b62a4f474664ccd6d84ea63f1697a6bd90e638a/test/api/cookies.js#L261-L287) and in other cookie tests.

The `testHost + "/TestPath/set-cookie-from-server"` path sets multiple cookies, one of which being a cookie with the `Secure` attribute (`testHost` is `http://127.0.0.1:...`) and the assertion expects the `Secure` cookie to not be present:
```js
assertCookies(cookieHeader, [
  "Test1=Basic",
  "Test2=PathMatch",
  "Test6=HttpOnly",
  // "Test7=Secure; expires=Wed, 13-Jan-2051 22:23:01 GMT; path=/; Secure", (This cookie is present)
  "Test9=Duplicate",
  "Test10={\"prop1\":5,\"prop2\":\"value\"}",
  "Malformed"
]);
```

due to the scheme being `http` instead of `https`.

This is unfortunately something out of our control, but as their goal states:
> the goal of the project is to emulate enough of a subset of a web browser to be useful for testing and scraping real-world web applications.

I do believe that part of that should be accepting `Secure` cookies on `localhost` as many browsers (Firefox, Chromium, etc) do.

I'll open a PR in `jsdom` attempting to change that but in case users don't want this functionality we can add a new parameter in the get cookies function that explicitly controls whether we should treat localhost/loopback addresses as trustworthy, something like `allowSecureOnLocal`.

As it stands `jsdom` isn't automatically using the latest version of `tough-cookie`, thus we can add this parameter, merge the PR and raise attention to the `jsdom` devs that if they'd like to keep old cookie trustworthy functionality they need to set `allowSecureOnLocal` to `true`.

What do you think of this?
Chriss4123: By the way, when I run `npm run prepare-pr` it replaces all `CRLF`'s with `LF`'s in the docs:
> In Unix systems the end of a line is represented with a line feed (LF). In windows a line is represented with a carriage return (CR) and a line feed (LF) thus (CRLF). when you get code from git that was uploaded from a unix system they will only have an LF.

(taken from https://stackoverflow.com/a/5834094)

This is because I am on Linux and the last person to run `prepare-pr` likely did it on Windows. I am going to commit it, but if this is an issue we can revert this commit and make a plan to use `CRLF` instead.
colincasey: > By the way, when I run npm run prepare-pr it replaces all CRLF's with LF's in the docs

This is expected @Chriss4123. There's a bug in the [api-extractor](https://github.com/microsoft/rushstack/issues/1467) that seems to be at the root of this. Our automation doc checks should catch if there's any problems here so if that passes you should be all good.

As for the code, I have one question for you on the addition of `allowSecureOnLocal` - do you think that option should be  available on the [`CookieJar` constructor](https://github.com/salesforce/tough-cookie/blob/master/api/docs/tough-cookie.createcookiejaroptions.md) instead of as an option on the `getCookies` method? The RFC states this should be at the discretion of the user agent during both [storage](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-19#section-5.7-3.13.1) and [retrieval](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-19#section-5.8.3-2.1.2.3.1) so I find it a bit odd that it's only exposed as configuration when retrieving cookies. 
Chriss4123: That is a great point @colincasey. The only reason I didn't do this is because the cookie jar _doesn't_ actually follow this step in the algorithm:
```js
> jar.setCookie('ipv6cookie=ipv6val; Secure; Path=/', 'http://[::ffff:7f00:1]:5000/')
Promise {
  Cookie="ipv6cookie=ipv6val; Path=/; Secure; hostOnly=true; aAge=0ms; cAge=0ms",
  [Symbol(async_id_symbol)]: 137,
  [Symbol(trigger_async_id_symbol)]: 6
}

> jar.store.idx
[Object: null prototype] {
  '::ffff:7f00:1': [Object: null prototype] {
    '/': [Object: null prototype] {
      ipv6cookie: Cookie="ipv6cookie=ipv6val; Path=/; Secure; hostOnly=true; aAge=142897ms; cAge=142897ms"
    }
  }
}
```
If it did, this cookie wouldn't be present in the store at all. Testing with this code:
```py
from flask import Flask, make_response, request

app = Flask(__name__)

@app.route('/setcookie')
def set_cookie():
    resp = make_response('')
    resp.set_cookie('example', '', secure=True)
    return resp

@app.route('/getcookie')
def get_cookie():
    return 'Cookie is set' if 'example' in request.cookies else 'No cookie set'

if __name__ == '__main__':
    app.run()
```

and Firefox on `[::ffff:7f00:1]:5000`, I can see that the `example` cookie is **not** set when I visit `/setcookie`, however this library **will** set, albeit it wont be returned so in the end it's **as if it was never set** if you only use the `getCookies` functions and don't access the store directly.

If you think this is a flaw too, let me know and I can open a new PR for it.
I'll also adjust the logic within this PR to reflect this rule in the algorithm by moving the `allowSecureOnLocal` to the constructor of `CookieJar`.
colincasey: > this library will set, albeit it wont be returned so in the end it's as if it was never set

This is what I was hinting at. I noticed in the tests added to support the `allowSecureOnLocal ` option you were able to set the cookie in the store but not retrieve it. While I don't think that's technically wrong, I also don't think it's technically right 😅 

What do you think of the following alternatives?

#### Expose the `allowSecureOnLocal` option on the `CookieStore` 

This would allow `setCookie` and `getCookies` to enforce the option and cover what's described in the storage and retrieval algorithms of the RFC. Then, a `CookieJar` with the `allowSecureOnLocal` option disabled would never allow **new** secure `localhost` cookies to be set while also preventing **existing** secure `localhost` cookies that may already be in the store from being returned.

Testing the retrieval case is a bit more complex but can be done if `setCookie` is bypassed and a secure `localhost` cookie is inserted directly into the store.

#### Expose the `allowSecureOnLocal` option on `setCookie` and `getCookies`

Same as the above, this would allow `setCookie` and `getCookies` to enforce the option and cover what's described in the storage and retrieval algorithms of the RFC. This finer degree of control comes with the trade-off of increased configuration complexity for the API with little benefit. Other then testing, I can't imagine a scenario where you'd want to use different values for `allowSecureOnLocal` when storing vs. retrieving.

Chriss4123: I prefer having the option in the constructor because while I like granular control I don't see a scenario where you'd want localhost as both trustworthy and not trustworthy. You'd also have to specifically say that localhost isn't trustworthy each time you set/get a cookie which would be a nuisance.

What's your thoughts on the issue I pointed out regarding setting `Secure` cookies on a non trustworthy address?

It is definitely more technically correct to **abort** as defined in the algorithm but the final call is obviously up to you.
Chriss4123: > This would allow `setCookie` and `getCookies` to enforce the option and cover what's described in the storage and retrieval algorithms of the RFC. Then, a `CookieJar` with the `allowSecureOnLocal` option disabled would never allow **new** secure `localhost` cookies to be set while also preventing **existing** secure `localhost` cookies that may already be in the store from being returned.

@colincasey Sorry, didn't realize you actually answered my question, guess I was too tired 😅.
Anyways, I've gone ahead and implemented that parameter and also the logic to abort if we're setting a secure cookie on a non-secure URL.

In the process, I had to modify multiple current and legacy tests to reflect that setting a cookie with the `Secure` attribute on a non-secure URL now throws an error instead of succeeding.

Previously, those tests expected no error when setting the cookie and instead not retrieving the cookie.
colincasey: @Chriss4123 discussed this PR with the other maintainers today. We're looking at shipping this in a v6 release. @awaterma is also going to give it a review and we're just figuring out if there are other work items that we'd like to include in a new major release so don't be surprised if it takes a while longer for it to be merged. Thanks so much for this contribution!
Chriss4123: @colincasey got it. For now I can just use my branch in my projects where I need this functionality.

If you need any further changes let me know.

Do you have a broad ETA in mind for this being merged (i.e. v6 release)?
colincasey: @Chriss4123 the ETA would definitely be a broad one 😅  My guess is somewhere between 2 - 4 weeks. 
Chriss4123: @colincasey that ETA is definitely quicker than I anticipated which is good to hear!

I realized I made a small mistake on a test case:

Pre `Secure` cookie fail when not a potentially trustworthy origin:
```ts
it('should fail silently when no Domain or Path', async () => {
  await cookieJar.setCookie('__Host-SID=12345; Secure', insecureUrl, {})
  const cookies = await cookieJar.getCookies(insecureUrl)
  expect(cookies).toEqual([])
})
```
Post `Secure` cookie fail when not a potentially trustworthy origin:
```ts
it('should fail silently when no Domain or Path', async () => {
  await cookieJar.setCookie('__Host-SID=12345', insecureUrl, {})
  const cookies = await cookieJar.getCookies(insecureUrl)
  expect(cookies).toEqual([])
})
```

I think there was an issue with the initial test case: `__Host-` prefix requires the `Secure` attribute and thus a secure URL so I'm not 100% sure why `insecureUrl` was being used, which led to me just getting rid of the `Secure` attribute altogether as an error is now raised when a `Secure` cookie is paired with an unsecure URL. I've updated it to use the `Secure` attribute with `secureUrl` (so it actually tests that it silently fails when no Domain or Path is set).

Patch:
diff --git a/api/docs/tough-cookie.cookiejar.setcookie.md b/api/docs/tough-cookie.cookiejar.setcookie.md
index b54334f..947aa68 100644
--- a/api/docs/tough-cookie.cookiejar.setcookie.md
+++ b/api/docs/tough-cookie.cookiejar.setcookie.md
@@ -87,5 +87,5 @@ void
 
 - If successfully persisted, the [Cookie](./tough-cookie.cookie.md) will have updated [Cookie.creation](./tough-cookie.cookie.creation.md)<!-- -->, [Cookie.lastAccessed](./tough-cookie.cookie.lastaccessed.md) and [Cookie.hostOnly](./tough-cookie.cookie.hostonly.md) properties.
 
-- As per the RFC, the [Cookie.hostOnly](./tough-cookie.cookie.hostonly.md) flag is set if there was no `Domain={value}` atttribute on the cookie string. The [Cookie.domain](./tough-cookie.cookie.domain.md) property is set to the fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an exact hostname match (not a [domainMatch()](./tough-cookie.domainmatch.md) as per usual)
+- As per the RFC, the [Cookie.hostOnly](./tough-cookie.cookie.hostonly.md) flag is set if there was no `Domain={value}` attribute on the cookie string. The [Cookie.domain](./tough-cookie.cookie.domain.md) property is set to the fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an exact hostname match (not a [domainMatch()](./tough-cookie.domainmatch.md) as per usual)
 
diff --git a/api/docs/tough-cookie.cookiejar.setcookie_1.md b/api/docs/tough-cookie.cookiejar.setcookie_1.md
index 22df45b..8d48cb4 100644
--- a/api/docs/tough-cookie.cookiejar.setcookie_1.md
+++ b/api/docs/tough-cookie.cookiejar.setcookie_1.md
@@ -103,5 +103,5 @@ void
 
 - If successfully persisted, the [Cookie](./tough-cookie.cookie.md) will have updated [Cookie.creation](./tough-cookie.cookie.creation.md)<!-- -->, [Cookie.lastAccessed](./tough-cookie.cookie.lastaccessed.md) and [Cookie.hostOnly](./tough-cookie.cookie.hostonly.md) properties.
 
-- As per the RFC, the [Cookie.hostOnly](./tough-cookie.cookie.hostonly.md) flag is set if there was no `Domain={value}` atttribute on the cookie string. The [Cookie.domain](./tough-cookie.cookie.domain.md) property is set to the fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an exact hostname match (not a [domainMatch()](./tough-cookie.domainmatch.md) as per usual)
+- As per the RFC, the [Cookie.hostOnly](./tough-cookie.cookie.hostonly.md) flag is set if there was no `Domain={value}` attribute on the cookie string. The [Cookie.domain](./tough-cookie.cookie.domain.md) property is set to the fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an exact hostname match (not a [domainMatch()](./tough-cookie.domainmatch.md) as per usual)
 
diff --git a/api/docs/tough-cookie.cookiejar.setcookie_2.md b/api/docs/tough-cookie.cookiejar.setcookie_2.md
index c2424bc..2daa80b 100644
--- a/api/docs/tough-cookie.cookiejar.setcookie_2.md
+++ b/api/docs/tough-cookie.cookiejar.setcookie_2.md
@@ -87,5 +87,5 @@ Promise&lt;[Cookie](./tough-cookie.cookie.md) \| undefined&gt;
 
 - If successfully persisted, the [Cookie](./tough-cookie.cookie.md) will have updated [Cookie.creation](./tough-cookie.cookie.creation.md)<!-- -->, [Cookie.lastAccessed](./tough-cookie.cookie.lastaccessed.md) and [Cookie.hostOnly](./tough-cookie.cookie.hostonly.md) properties.
 
-- As per the RFC, the [Cookie.hostOnly](./tough-cookie.cookie.hostonly.md) flag is set if there was no `Domain={value}` atttribute on the cookie string. The [Cookie.domain](./tough-cookie.cookie.domain.md) property is set to the fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an exact hostname match (not a [domainMatch()](./tough-cookie.domainmatch.md) as per usual)
+- As per the RFC, the [Cookie.hostOnly](./tough-cookie.cookie.hostonly.md) flag is set if there was no `Domain={value}` attribute on the cookie string. The [Cookie.domain](./tough-cookie.cookie.domain.md) property is set to the fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an exact hostname match (not a [domainMatch()](./tough-cookie.domainmatch.md) as per usual)
 
diff --git a/api/docs/tough-cookie.cookiejar.setcookiesync.md b/api/docs/tough-cookie.cookiejar.setcookiesync.md
index 1cce537..49a4635 100644
--- a/api/docs/tough-cookie.cookiejar.setcookiesync.md
+++ b/api/docs/tough-cookie.cookiejar.setcookiesync.md
@@ -89,5 +89,5 @@ _(Optional)_ Configuration settings to use when storing the cookie.
 
 - If successfully persisted, the [Cookie](./tough-cookie.cookie.md) will have updated [Cookie.creation](./tough-cookie.cookie.creation.md)<!-- -->, [Cookie.lastAccessed](./tough-cookie.cookie.lastaccessed.md) and [Cookie.hostOnly](./tough-cookie.cookie.hostonly.md) properties.
 
-- As per the RFC, the [Cookie.hostOnly](./tough-cookie.cookie.hostonly.md) flag is set if there was no `Domain={value}` atttribute on the cookie string. The [Cookie.domain](./tough-cookie.cookie.domain.md) property is set to the fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an exact hostname match (not a [domainMatch()](./tough-cookie.domainmatch.md) as per usual)
+- As per the RFC, the [Cookie.hostOnly](./tough-cookie.cookie.hostonly.md) flag is set if there was no `Domain={value}` attribute on the cookie string. The [Cookie.domain](./tough-cookie.cookie.domain.md) property is set to the fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an exact hostname match (not a [domainMatch()](./tough-cookie.domainmatch.md) as per usual)
 
diff --git a/api/docs/tough-cookie.createcookiejaroptions.allowsecureonlocal.md b/api/docs/tough-cookie.createcookiejaroptions.allowsecureonlocal.md
new file mode 100644
index 0000000..d242c2b
--- /dev/null
+++ b/api/docs/tough-cookie.createcookiejaroptions.allowsecureonlocal.md
@@ -0,0 +1,22 @@
+<!-- Do not edit this file. It is automatically generated by API Documenter. -->
+
+[Home](./index.md) &gt; [tough-cookie](./tough-cookie.md) &gt; [CreateCookieJarOptions](./tough-cookie.createcookiejaroptions.md) &gt; [allowSecureOnLocal](./tough-cookie.createcookiejaroptions.allowsecureonlocal.md)
+
+## CreateCookieJarOptions.allowSecureOnLocal property
+
+Flag to indicate if localhost and loopback addresses with an unsecure scheme should store and retrieve `Secure` cookies.
+
+If `true`<!-- -->, localhost, loopback addresses or similarly local addresses are treated as secure contexts and thus will store and retrieve `Secure` cookies even with an unsecure scheme.
+
+If `false`<!-- -->, only secure schemes (`https` and `wss`<!-- -->) will store and retrieve `Secure` cookies.
+
+**Signature:**
+
+```typescript
+allowSecureOnLocal?: boolean | undefined;
+```
+
+## Remarks
+
+When set to `true`<!-- -->, the [potentially trustworthy](https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin) algorithm is followed to determine if a URL is considered a secure context.
+
diff --git a/api/docs/tough-cookie.createcookiejaroptions.md b/api/docs/tough-cookie.createcookiejaroptions.md
index 8b28a69..92a7e2a 100644
--- a/api/docs/tough-cookie.createcookiejaroptions.md
+++ b/api/docs/tough-cookie.createcookiejaroptions.md
@@ -37,6 +37,29 @@ Description
 </th></tr></thead>
 <tbody><tr><td>
 
+[allowSecureOnLocal?](./tough-cookie.createcookiejaroptions.allowsecureonlocal.md)
+
+
+</td><td>
+
+
+</td><td>
+
+boolean \| undefined
+
+
+</td><td>
+
+_(Optional)_ Flag to indicate if localhost and loopback addresses with an unsecure scheme should store and retrieve `Secure` cookies.
+
+If `true`<!-- -->, localhost, loopback addresses or similarly local addresses are treated as secure contexts and thus will store and retrieve `Secure` cookies even with an unsecure scheme.
+
+If `false`<!-- -->, only secure schemes (`https` and `wss`<!-- -->) will store and retrieve `Secure` cookies.
+
+
+</td></tr>
+<tr><td>
+
 [allowSpecialUseDomain?](./tough-cookie.createcookiejaroptions.allowspecialusedomain.md)
 
 
diff --git a/api/docs/tough-cookie.getcookiesoptions.expire.md b/api/docs/tough-cookie.getcookiesoptions.expire.md
index 76373aa..c111839 100644
--- a/api/docs/tough-cookie.getcookiesoptions.expire.md
+++ b/api/docs/tough-cookie.getcookiesoptions.expire.md
@@ -14,7 +14,7 @@ expire?: boolean | undefined;
 
 ## Remarks
 
-- Using `false` returns expired cookies and does not remove them from the store which is potentially useful for replaying `Set-Cookie` headers.
+- Using `false` returns expired cookies and does not remove them from the store, which is potentially useful for replaying `Set-Cookie` headers.
 
 Defaults to `true` if not provided.
 
diff --git a/api/docs/tough-cookie.getcookiesoptions.md b/api/docs/tough-cookie.getcookiesoptions.md
index ae8d2db..68f613b 100644
--- a/api/docs/tough-cookie.getcookiesoptions.md
+++ b/api/docs/tough-cookie.getcookiesoptions.md
@@ -117,7 +117,7 @@ _(Optional)_ Set this to 'none', 'lax', or 'strict' to enforce SameSite cookies
 
 - `'none'` - This indicates a cross-origin request.
 
-- `undefined` - SameSite is not be enforced! This can be a valid use-case for when CSRF isn't in the threat model of the system being built.
+- `undefined` - SameSite is not enforced! This can be a valid use-case for when CSRF isn't in the threat model of the system being built.
 
 Defaults to `undefined` if not provided.
 
diff --git a/api/docs/tough-cookie.getcookiesoptions.samesitecontext.md b/api/docs/tough-cookie.getcookiesoptions.samesitecontext.md
index d28ecf7..210c82d 100644
--- a/api/docs/tough-cookie.getcookiesoptions.samesitecontext.md
+++ b/api/docs/tough-cookie.getcookiesoptions.samesitecontext.md
@@ -12,7 +12,7 @@ Set this to 'none', 'lax', or 'strict' to enforce SameSite cookies upon retrieva
 
 - `'none'` - This indicates a cross-origin request.
 
-- `undefined` - SameSite is not be enforced! This can be a valid use-case for when CSRF isn't in the threat model of the system being built.
+- `undefined` - SameSite is not enforced! This can be a valid use-case for when CSRF isn't in the threat model of the system being built.
 
 Defaults to `undefined` if not provided.
 
diff --git a/api/docs/tough-cookie.setcookieoptions.md b/api/docs/tough-cookie.setcookieoptions.md
index 3622f56..7a1ebf8 100644
--- a/api/docs/tough-cookie.setcookieoptions.md
+++ b/api/docs/tough-cookie.setcookieoptions.md
@@ -142,7 +142,7 @@ _(Optional)_ Set this to 'none', 'lax', or 'strict' to enforce SameSite cookies
 
 - `'none'` - This indicates a cross-origin request.
 
-- `undefined` - SameSite is not be enforced! This can be a valid use-case for when CSRF isn't in the threat model of the system being built.
+- `undefined` - SameSite is not enforced! This can be a valid use-case for when CSRF isn't in the threat model of the system being built.
 
 Defaults to `undefined` if not provided.
 
diff --git a/api/docs/tough-cookie.setcookieoptions.samesitecontext.md b/api/docs/tough-cookie.setcookieoptions.samesitecontext.md
index 3a88292..785d7e9 100644
--- a/api/docs/tough-cookie.setcookieoptions.samesitecontext.md
+++ b/api/docs/tough-cookie.setcookieoptions.samesitecontext.md
@@ -12,7 +12,7 @@ Set this to 'none', 'lax', or 'strict' to enforce SameSite cookies upon storage.
 
 - `'none'` - This indicates a cross-origin request.
 
-- `undefined` - SameSite is not be enforced! This can be a valid use-case for when CSRF isn't in the threat model of the system being built.
+- `undefined` - SameSite is not enforced! This can be a valid use-case for when CSRF isn't in the threat model of the system being built.
 
 Defaults to `undefined` if not provided.
 
diff --git a/api/tough-cookie.api.md b/api/tough-cookie.api.md
index 8bddb25..00abd33 100644
--- a/api/tough-cookie.api.md
+++ b/api/tough-cookie.api.md
@@ -124,6 +124,7 @@ export class CookieJar {
 
 // @public
 export interface CreateCookieJarOptions {
+    allowSecureOnLocal?: boolean | undefined;
     allowSpecialUseDomain?: boolean | undefined;
     looseMode?: boolean | undefined;
     prefixSecurity?: 'strict' | 'silent' | 'unsafe-disabled' | undefined;
diff --git a/lib/cookie/constants.ts b/lib/cookie/constants.ts
index c2753c0..49d8de5 100644
--- a/lib/cookie/constants.ts
+++ b/lib/cookie/constants.ts
@@ -35,6 +35,9 @@ const IP_V6_REGEX = `
   .trim()
 export const IP_V6_REGEX_OBJECT: RegExp = new RegExp(`^${IP_V6_REGEX}$`)
 
+const IP_V4_REGEX = `(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])`
+export const IP_V4_REGEX_OBJECT: RegExp = new RegExp(`^${IP_V4_REGEX}$`)
+
 /**
  * A JSON representation of a {@link CookieJar}.
  * @public
diff --git a/lib/cookie/cookieJar.ts b/lib/cookie/cookieJar.ts
index 2e7ee49..de6c8e7 100644
--- a/lib/cookie/cookieJar.ts
+++ b/lib/cookie/cookieJar.ts
@@ -23,6 +23,7 @@ import { defaultPath } from './defaultPath'
 import { domainMatch } from './domainMatch'
 import { cookieCompare } from './cookieCompare'
 import { version } from '../version'
+import { isPotentiallyTrustworthy } from './secureContext'
 
 const defaultSetCookieOptions: SetCookieOptions = {
   loose: false,
@@ -54,7 +55,7 @@ export interface SetCookieOptions {
    *
    * - `'none'` - This indicates a cross-origin request.
    *
-   * - `undefined` - SameSite is not be enforced! This can be a valid use-case for when
+   * - `undefined` - SameSite is not enforced! This can be a valid use-case for when
    *     CSRF isn't in the threat model of the system being built.
    *
    * Defaults to `undefined` if not provided.
@@ -109,7 +110,7 @@ export interface GetCookiesOptions {
    *
    * @remarks
    * - Using `false` returns expired cookies and does not remove them from the
-   *     store which is potentially useful for replaying `Set-Cookie` headers.
+   *     store, which is potentially useful for replaying `Set-Cookie` headers.
    *
    * Defaults to `true` if not provided.
    */
@@ -134,7 +135,7 @@ export interface GetCookiesOptions {
    *
    * - `'none'` - This indicates a cross-origin request.
    *
-   * - `undefined` - SameSite is not be enforced! This can be a valid use-case for when
+   * - `undefined` - SameSite is not enforced! This can be a valid use-case for when
    *     CSRF isn't in the threat model of the system being built.
    *
    * Defaults to `undefined` if not provided.
@@ -183,6 +184,19 @@ export interface CreateCookieJarOptions {
    * Defaults to `true` if not specified.
    */
   allowSpecialUseDomain?: boolean | undefined
+  /**
+   * Flag to indicate if localhost and loopback addresses with an unsecure scheme should store and retrieve `Secure` cookies.
+   *
+   * If `true`, localhost, loopback addresses or similarly local addresses are treated as secure contexts
+   * and thus will store and retrieve `Secure` cookies even with an unsecure scheme.
+   *
+   * If `false`, only secure schemes (`https` and `wss`) will store and retrieve `Secure` cookies.
+   *
+   * @remarks
+   * When set to `true`, the {@link https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin | potentially trustworthy}
+   *  algorithm is followed to determine if a URL is considered a secure context.
+   */
+  allowSecureOnLocal?: boolean | undefined
 }
 
 const SAME_SITE_CONTEXT_VAL_ERR =
@@ -297,6 +311,7 @@ export class CookieJar {
   private readonly rejectPublicSuffixes: boolean
   private readonly enableLooseMode: boolean
   private readonly allowSpecialUseDomain: boolean
+  private readonly allowSecureOnLocal: boolean
 
   /**
    * The configured {@link Store} for the {@link CookieJar}.
@@ -328,6 +343,7 @@ export class CookieJar {
     this.rejectPublicSuffixes = options?.rejectPublicSuffixes ?? true
     this.enableLooseMode = options?.looseMode ?? false
     this.allowSpecialUseDomain = options?.allowSpecialUseDomain ?? true
+    this.allowSecureOnLocal = options?.allowSecureOnLocal ?? true
     this.prefixSecurity = getNormalizedPrefixSecurity(
       options?.prefixSecurity ?? 'silent',
     )
@@ -368,7 +384,7 @@ export class CookieJar {
    *     properties.
    *
    * - As per the RFC, the {@link Cookie.hostOnly} flag is set if there was no `Domain={value}`
-   *     atttribute on the cookie string. The {@link Cookie.domain} property is set to the
+   *     attribute on the cookie string. The {@link Cookie.domain} property is set to the
    *     fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an
    *     exact hostname match (not a {@link domainMatch} as per usual)
    *
@@ -391,7 +407,7 @@ export class CookieJar {
    *     properties.
    *
    * - As per the RFC, the {@link Cookie.hostOnly} flag is set if there was no `Domain={value}`
-   *     atttribute on the cookie string. The {@link Cookie.domain} property is set to the
+   *     attribute on the cookie string. The {@link Cookie.domain} property is set to the
    *     fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an
    *     exact hostname match (not a {@link domainMatch} as per usual)
    *
@@ -416,7 +432,7 @@ export class CookieJar {
    *     properties.
    *
    * - As per the RFC, the {@link Cookie.hostOnly} flag is set if there was no `Domain={value}`
-   *     atttribute on the cookie string. The {@link Cookie.domain} property is set to the
+   *     attribute on the cookie string. The {@link Cookie.domain} property is set to the
    *     fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an
    *     exact hostname match (not a {@link domainMatch} as per usual)
    *
@@ -592,7 +608,23 @@ export class CookieJar {
       cookie.pathIsDefault = true
     }
 
-    // S5.3 step 8: NOOP; secure attribute
+    // S5.3 step 8: secure attribute:
+    // "If the request-uri does not denote a "secure" connection
+    // (as defined by the user agent), and the cookie's secure-only-flag
+    // is true, then abort these steps and ignore the cookie entirely."
+    const potentiallyTrustworthy = isPotentiallyTrustworthy(
+      url,
+      this.allowSecureOnLocal,
+    )
+    if (!potentiallyTrustworthy && cookie.secure) {
+      const err = new Error(
+        'Cookie is Secure but this is not a secure connection',
+      )
+      return options?.ignoreError
+        ? promiseCallback.resolve(undefined)
+        : promiseCallback.reject(err)
+    }
+
     // S5.3 step 9: NOOP; httpOnly attribute
 
     // S5.3 step 10
@@ -734,7 +766,7 @@ export class CookieJar {
    *     properties.
    *
    * - As per the RFC, the {@link Cookie.hostOnly} flag is set if there was no `Domain={value}`
-   *     atttribute on the cookie string. The {@link Cookie.domain} property is set to the
+   *     attribute on the cookie string. The {@link Cookie.domain} property is set to the
    *     fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an
    *     exact hostname match (not a {@link domainMatch} as per usual)
    *
@@ -858,9 +890,14 @@ export class CookieJar {
     const host = canonicalDomain(context.hostname)
     const path = context.pathname || '/'
 
-    const secure =
-      context.protocol &&
-      (context.protocol == 'https:' || context.protocol == 'wss:')
+    // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-19#section-5.8.3-2.1.2.3.2
+    // deliberately expects the user agent to determine the notion of a "secure" connection,
+    // and in practice this converges to a "potentially trustworthy origin" as defined in:
+    // https://www.w3.org/TR/secure-contexts/#is-origin-trustworthy
+    const potentiallyTrustworthy = isPotentiallyTrustworthy(
+      url,
+      this.allowSecureOnLocal,
+    )
 
     let sameSiteLevel = 0
     if (options.sameSiteContext) {
@@ -905,7 +942,7 @@ export class CookieJar {
 
       // "If the cookie's secure-only-flag is true, then the request-uri's
       // scheme must denote a "secure" protocol"
-      if (c.secure && !secure) {
+      if (c.secure && !potentiallyTrustworthy) {
         return false
       }
 
diff --git a/lib/cookie/secureContext.ts b/lib/cookie/secureContext.ts
new file mode 100644
index 0000000..0191884
--- /dev/null
+++ b/lib/cookie/secureContext.ts
@@ -0,0 +1,96 @@
+import { IP_V4_REGEX_OBJECT, IP_V6_REGEX_OBJECT } from './constants'
+
+function isLoopbackV4(address: string): boolean {
+  // 127.0.0.0/8: first octet = 127
+  const octets = address.split('.')
+  return (
+    octets.length === 4 &&
+    octets[0] !== undefined &&
+    parseInt(octets[0], 10) === 127
+  )
+}
+
+function isLoopbackV6(address: string): boolean {
+  // 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'
+}
+
+function isNormalizedLocalhostTLD(lowerHost: string): boolean {
+  return lowerHost.endsWith('.localhost')
+}
+
+function isLocalHostname(host: string): boolean {
+  const lowerHost = host.toLowerCase()
+  return lowerHost === 'localhost' || isNormalizedLocalhostTLD(lowerHost)
+}
+
+// Adapted from https://github.com/chromium/chromium/blob/main/url/gurl.cc#L440-L448
+function hostNoBrackets(host: string): string {
+  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 or WSS schemes
+ * - If `allowSecureOnLocal` is `true`:
+ *   - Points to a loopback address (IPv4 127.0.0.0/8 or IPv6 ::1)
+ *   - Uses localhost or *.localhost hostnames
+ *
+ * @param inputUrl - The URL string or URL object to check.
+ * @param allowSecureOnLocal - Whether to treat localhost and loopback addresses as trustworthy.
+ * @returns `true` if the URL is potentially trustworthy, otherwise `false`.
+ * @see {@link https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin | Potentially Trustworthy Origin algorithm}
+ */
+export function isPotentiallyTrustworthy(
+  inputUrl: string | URL,
+  allowSecureOnLocal: boolean = true,
+): boolean {
+  let url: URL
+
+  // try ... catch doubles as an opaque origin check
+  if (typeof inputUrl === 'string') {
+    try {
+      url = new URL(inputUrl)
+    } catch {
+      return false
+    }
+  } else {
+    url = inputUrl
+  }
+
+  const scheme = url.protocol.replace(':', '').toLowerCase()
+  const hostname = hostNoBrackets(url.hostname).replace(/\.+$/, '')
+
+  if (
+    scheme === 'https' ||
+    scheme === 'wss' // https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin
+  ) {
+    return true
+  }
+
+  if (!allowSecureOnLocal) {
+    return false
+  }
+
+  // If it's already an IP literal, check if it's a loopback address
+  if (IP_V4_REGEX_OBJECT.test(hostname)) {
+    return isLoopbackV4(hostname)
+  }
+
+  if (IP_V6_REGEX_OBJECT.test(hostname)) {
+    return isLoopbackV6(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)
+}


Test patch:
diff --git a/lib/__tests__/cookieJar.spec.ts b/lib/__tests__/cookieJar.spec.ts
index 131281a..883f3f8 100644
--- a/lib/__tests__/cookieJar.spec.ts
+++ b/lib/__tests__/cookieJar.spec.ts
@@ -451,29 +451,36 @@ describe('CookieJar', () => {
 
     describe('retrieving cookies', () => {
       beforeEach(async () => {
-        const url = 'http://example.com/index.html'
+        const insecureUrl = 'http://example.com/index.html'
+        const secureUrl = 'https://example.com/index.html'
 
-        await cookieJar.setCookie('a=1; Domain=example.com; Path=/', url)
+        await cookieJar.setCookie(
+          'a=1; Domain=example.com; Path=/',
+          insecureUrl,
+        )
         jest.advanceTimersByTime(1000)
 
         await cookieJar.setCookie(
           'b=2; Domain=example.com; Path=/; HttpOnly',
-          url,
+          insecureUrl,
         )
         jest.advanceTimersByTime(1000)
 
         await cookieJar.setCookie(
           'c=3; Domain=example.com; Path=/; Secure',
-          url,
+          secureUrl,
         )
         jest.advanceTimersByTime(1000)
 
         // path
-        await cookieJar.setCookie('d=4; Domain=example.com; Path=/foo', url)
+        await cookieJar.setCookie(
+          'd=4; Domain=example.com; Path=/foo',
+          insecureUrl,
+        )
         jest.advanceTimersByTime(1000)
 
         // host only
-        await cookieJar.setCookie('e=5', url)
+        await cookieJar.setCookie('e=5', insecureUrl)
         jest.advanceTimersByTime(1000)
 
         // other domain
@@ -486,14 +493,14 @@ describe('CookieJar', () => {
         // expired
         await cookieJar.setCookie(
           'g=7; Domain=example.com; Path=/; Expires=Tue, 18 Oct 2011 00:00:00 GMT',
-          url,
+          insecureUrl,
         )
         jest.advanceTimersByTime(1000)
 
         // expired via Max-Age
         await cookieJar.setCookie(
           'h=8; Domain=example.com; Path=/; Max-Age=1',
-          url,
+          insecureUrl,
         )
         jest.advanceTimersByTime(2000) // so that 'h=8' expires
       })
@@ -740,32 +747,37 @@ describe('CookieJar', () => {
 
     describe('retrieving cookie strings', () => {
       beforeEach(async () => {
-        const url = 'http://example.com/index.html'
+        const insecureUrl = 'http://example.com/index.html'
+        const secureUrl = 'https://example.com/index.html'
 
         const at = (timeFromNow: number): { now: Date } => ({
           now: new Date(Date.now() + timeFromNow),
         })
 
         const cookies = await Promise.all([
-          cookieJar.setCookie('a=1; Domain=example.com; Path=/', url, at(0)),
+          cookieJar.setCookie(
+            'a=1; Domain=example.com; Path=/',
+            insecureUrl,
+            at(0),
+          ),
           cookieJar.setCookie(
             'b=2; Domain=example.com; Path=/; HttpOnly',
-            url,
+            insecureUrl,
             at(1000),
           ),
           cookieJar.setCookie(
             'c=3; Domain=example.com; Path=/; Secure',
-            url,
+            secureUrl,
             at(2000),
           ),
           // path
           cookieJar.setCookie(
             'd=4; Domain=example.com; Path=/foo',
-            url,
+            insecureUrl,
             at(3000),
           ),
           // host only
-          cookieJar.setCookie('e=5', url, at(4000)),
+          cookieJar.setCookie('e=5', insecureUrl, at(4000)),
           // other domain
           cookieJar.setCookie(
             'f=6; Domain=nodejs.org; Path=/',
@@ -775,13 +787,13 @@ describe('CookieJar', () => {
           // expired
           cookieJar.setCookie(
             'g=7; Domain=example.com; Path=/; Expires=Tue, 18 Oct 2011 00:00:00 GMT',
-            url,
+            insecureUrl,
             at(6000),
           ),
           // expired via Max-Age
           cookieJar.setCookie(
             'h=8; Domain=example.com; Path=/; Max-Age=1',
-            url,
+            insecureUrl,
           ),
         ])
 
@@ -837,32 +849,37 @@ describe('CookieJar', () => {
 
     describe('retrieving cookie strings', () => {
       beforeEach(async () => {
-        const url = 'http://example.com/index.html'
+        const insecureUrl = 'http://example.com/index.html'
+        const secureUrl = 'https://example.com/index.html'
 
         const at = (timeFromNow: number): { now: Date } => ({
           now: new Date(Date.now() + timeFromNow),
         })
 
         const cookies = await Promise.all([
-          cookieJar.setCookie('a=1; Domain=example.com; Path=/', url, at(0)),
+          cookieJar.setCookie(
+            'a=1; Domain=example.com; Path=/',
+            insecureUrl,
+            at(0),
+          ),
           cookieJar.setCookie(
             'b=2; Domain=example.com; Path=/; HttpOnly',
-            url,
+            insecureUrl,
             at(1000),
           ),
           cookieJar.setCookie(
             'c=3; Domain=example.com; Path=/; Secure',
-            url,
+            secureUrl,
             at(2000),
           ),
           // path
           cookieJar.setCookie(
             'd=4; Domain=example.com; Path=/foo',
-            url,
+            insecureUrl,
             at(3000),
           ),
           // host only
-          cookieJar.setCookie('e=5', url, at(4000)),
+          cookieJar.setCookie('e=5', insecureUrl, at(4000)),
           // other domain
           cookieJar.setCookie(
             'f=6; Domain=nodejs.org; Path=/',
@@ -872,13 +889,13 @@ describe('CookieJar', () => {
           // expired
           cookieJar.setCookie(
             'g=7; Domain=example.com; Path=/; Expires=Tue, 18 Oct 2011 00:00:00 GMT',
-            url,
+            insecureUrl,
             at(6000),
           ),
           // expired via Max-Age
           cookieJar.setCookie(
             'h=8; Domain=example.com; Path=/; Max-Age=1',
-            url,
+            insecureUrl,
           ),
         ])
 
@@ -1026,6 +1043,237 @@ describe('CookieJar', () => {
       ])
     })
   })
+
+  describe('Potentially Trustworthy Contexts', () => {
+    let jar: CookieJar
+
+    beforeEach(() => {
+      jar = new CookieJar()
+    })
+
+    it('should store and retrieve a secure cookie on http://localhost', async () => {
+      const c = await jar.setCookie(
+        'mysession=abc; Secure; Path=/',
+        'http://localhost/test',
+      )
+      expect(c).toBeInstanceOf(Cookie)
+      expect(c?.key).toBe('mysession')
+      expect(c?.value).toBe('abc')
+
+      // Because `localhost` is now considered trustworthy, we should retrieve the Secure cookie.
+      const cookies = await jar.getCookies('http://localhost/test')
+      expect(cookies.map((ck) => ck.key)).toEqual(['mysession'])
+    })
+
+    it('should store and retrieve a secure cookie on http://127.0.0.1', async () => {
+      const c = await jar.setCookie(
+        'loopcookie=loop123; Secure; Path=/',
+        'http://127.0.0.1/',
+      )
+      expect(c).toBeInstanceOf(Cookie)
+      expect(c?.key).toBe('loopcookie')
+      expect(c?.value).toBe('loop123')
+
+      // Loopback IPv4 addresses are also considered trustworthy
+      const cookies = await jar.getCookies('http://127.0.0.1/')
+      expect(cookies.map((ck) => ck.key)).toEqual(['loopcookie'])
+    })
+
+    it('should store and retrieve a secure cookie on http://[::1]', async () => {
+      const c = await jar.setCookie(
+        'ipv6cookie=ipv6val; Secure; Path=/',
+        'http://[::1]/',
+      )
+      expect(c).toBeInstanceOf(Cookie)
+      expect(c?.key).toBe('ipv6cookie')
+      expect(c?.value).toBe('ipv6val')
+
+      // IPv6 loopback is also deemed trustworthy
+      const cookies = await jar.getCookies('http://[::1]/')
+      expect(cookies.map((ck) => ck.key)).toEqual(['ipv6cookie'])
+    })
+
+    it('should return false for an invalid URL string', async () => {
+      expect.assertions(1)
+      await expect(
+        jar.setCookie('securecookie=1; Secure; Path=/', 'invalid-url'),
+      ).rejects.toThrow()
+    })
+
+    it('should accept a pre-constructed URL object', async () => {
+      const urlObject = new URL('http://localhost/foo')
+      const c = await jar.setCookie('testurlobj=abc; Secure', urlObject)
+      expect(c).toBeInstanceOf(Cookie)
+      expect(c?.key).toBe('testurlobj')
+
+      // Because it's localhost, the cookie is considered secure.
+      const cookies = await jar.getCookies(urlObject)
+      expect(cookies.map((ck) => ck.key)).toEqual(['testurlobj'])
+    })
+
+    it('should store and retrieve a secure cookie on *.localhost', async () => {
+      const c = await jar.setCookie(
+        'appcookie=someval; Secure; Path=/',
+        'http://subdomain.localhost/',
+      )
+      expect(c).toBeInstanceOf(Cookie)
+      expect(c?.key).toBe('appcookie')
+      expect(c?.value).toBe('someval')
+
+      const cookies = await jar.getCookies('http://subdomain.localhost/')
+      expect(cookies.map((ck) => ck.key)).toEqual(['appcookie'])
+    })
+
+    it('should store and retrieve a secure cookie on https://example.com', async () => {
+      const c = await jar.setCookie(
+        'secureCookie=onHTTPS; Secure; Path=/',
+        'https://example.com/',
+      )
+      expect(c).toBeInstanceOf(Cookie)
+      expect(c?.key).toBe('secureCookie')
+      expect(c?.value).toBe('onHTTPS')
+
+      const cookies = await jar.getCookies('https://example.com/')
+      expect(cookies.map((ck) => ck.key)).toEqual(['secureCookie'])
+    })
+
+    it('should store and retrieve a secure cookie on wss://example.com', async () => {
+      const c = await jar.setCookie(
+        'wssCookie=onWSS; Secure; Path=/',
+        'wss://example.com/',
+      )
+      expect(c).toBeInstanceOf(Cookie)
+      expect(c?.key).toBe('wssCookie')
+      expect(c?.value).toBe('onWSS')
+
+      const cookies = await jar.getCookies('wss://example.com/')
+      expect(cookies.map((ck) => ck.key)).toEqual(['wssCookie'])
+    })
+
+    describe('when allowSecureOnLocal is set to false', () => {
+      beforeEach(() => {
+        jar = new CookieJar(null, { allowSecureOnLocal: false })
+      })
+
+      describe('Storing a secure cookie on a non-secure local origin', () => {
+        it('should throw when setting a Secure cookie on http://localhost', async () => {
+          await expect(
+            jar.setCookie(
+              'testLocalhost=abc; Secure; Path=/',
+              'http://localhost/',
+            ),
+          ).rejects.toThrow(
+            'Cookie is Secure but this is not a secure connection',
+          )
+        })
+
+        it('should throw when setting a Secure cookie on http://127.0.0.1', async () => {
+          await expect(
+            jar.setCookie('loopcookie=val; Secure', 'http://127.0.0.1/'),
+          ).rejects.toThrow(
+            'Cookie is Secure but this is not a secure connection',
+          )
+        })
+
+        it('should throw when setting a Secure cookie on http://[::1]', async () => {
+          await expect(
+            jar.setCookie('ipv6cookie=ipv6val; Secure', 'http://[::1]/'),
+          ).rejects.toThrow(
+            'Cookie is Secure but this is not a secure connection',
+          )
+        })
+
+        it('should throw when setting a Secure cookie on http://subdomain.localhost', async () => {
+          await expect(
+            jar.setCookie(
+              'appcookie=someval; Secure',
+              'http://subdomain.localhost/',
+            ),
+          ).rejects.toThrow(
+            'Cookie is Secure but this is not a secure connection',
+          )
+        })
+
+        it('should NOT throw if ignoreError=true, but the cookie is NOT stored', async () => {
+          await expect(
+            jar.setCookie('ignoredCookie=val; Secure', 'http://localhost/', {
+              ignoreError: true,
+            }),
+          ).resolves.toBeUndefined()
+
+          const cookies = await jar.getCookies('http://localhost/')
+          expect(cookies.map((c) => c.key)).not.toContain('ignoredCookie')
+        })
+      })
+
+      describe('Storing a secure cookie on a secure local origin', () => {
+        it('should allow storing on https://localhost and retrieve it from https://localhost', async () => {
+          // https:// is always considered potentially trustworthy
+          await expect(
+            jar.setCookie('secureLocal=foo; Secure', 'https://localhost/'),
+          ).resolves.toBeInstanceOf(Cookie)
+
+          // Retrieving from the secure origin => cookie should be returned
+          const cookiesHttps = await jar.getCookies('https://localhost/')
+          expect(cookiesHttps.map((c) => c.key)).toContain('secureLocal')
+        })
+
+        it('should NOT retrieve a secure cookie set on https://localhost if requested from http://localhost', async () => {
+          await jar.setCookie('secureLocal=foo; Secure', 'https://localhost/')
+          // Retrieving from a non-secure scheme => should NOT return the secure cookie
+          const cookiesHttp = await jar.getCookies('http://localhost/')
+          expect(cookiesHttp.map((c) => c.key)).not.toContain('secureLocal')
+        })
+
+        it('should allow storing on https://127.0.0.1 and retrieve it from https://127.0.0.1 only', async () => {
+          await jar.setCookie(
+            'secureLoopback=abc; Secure',
+            'https://127.0.0.1/',
+          )
+          // Secure retrieval => cookie should be returned
+          const cookiesSecure = await jar.getCookies('https://127.0.0.1/')
+          expect(cookiesSecure.map((c) => c.key)).toContain('secureLoopback')
+
+          // Non-secure retrieval => not returned
+          const cookiesInsecure = await jar.getCookies('http://127.0.0.1/')
+          expect(cookiesInsecure.map((c) => c.key)).not.toContain(
+            'secureLoopback',
+          )
+        })
+
+        it('should allow storing on wss://localhost and retrieve it from wss://localhost only', async () => {
+          await jar.setCookie('wsscookie=val; Secure', 'wss://localhost/')
+          const cookiesWss = await jar.getCookies('wss://localhost/')
+          expect(cookiesWss.map((c) => c.key)).toContain('wsscookie')
+
+          // ws:// is not a secure scheme => should not see the cookie
+          const cookiesWs = await jar.getCookies('ws://localhost/')
+          expect(cookiesWs.map((c) => c.key)).not.toContain('wsscookie')
+        })
+      })
+
+      describe('Storing a non-secure (plain) cookie on any local origin', () => {
+        // These tests confirm that "allowSecureOnLocal = false" doesn't block normal cookies
+        it('should store and retrieve a non-secure cookie on http://localhost', async () => {
+          await jar.setCookie('plainCookie=123; Path=/', 'http://localhost/')
+          const cookies = await jar.getCookies('http://localhost/')
+          expect(cookies.map((c) => c.key)).toContain('plainCookie')
+        })
+
+        it('should store and retrieve a non-secure cookie on http://127.0.0.1', async () => {
+          await jar.setCookie('plainLoop=xyz; Path=/', 'http://127.0.0.1/')
+          const cookies = await jar.getCookies('http://127.0.0.1/')
+          expect(cookies.map((c) => c.key)).toContain('plainLoop')
+        })
+
+        it('should store and retrieve a non-secure cookie on https://localhost', async () => {
+          await jar.setCookie('plainSecure=abc; Path=/', 'https://localhost/')
+          const cookies = await jar.getCookies('https://localhost/')
+          expect(cookies.map((c) => c.key)).toContain('plainSecure')
+        })
+      })
+    })
+  })
 })
 
 it('should allow cookies with the same name under different domains and/or paths', async () => {
diff --git a/lib/__tests__/cookiePrefixes.spec.ts b/lib/__tests__/cookiePrefixes.spec.ts
index 6b7d9d3..a067683 100644
--- a/lib/__tests__/cookiePrefixes.spec.ts
+++ b/lib/__tests__/cookiePrefixes.spec.ts
@@ -40,14 +40,16 @@ describe('When `prefixSecurity` is enabled for `CookieJar`', () => {
         ])
       })
 
-      it('should fail silently if cookie has Secure attribute but domain is http', async () => {
-        await cookieJar.setCookie(
-          '__Secure-SID=12345; Domain=example.com; Secure',
-          insecureUrl,
-          {},
+      it('should throw if cookie has Secure attribute but domain is http', async () => {
+        await expect(
+          cookieJar.setCookie(
+            '__Secure-SID=12345; Domain=example.com; Secure',
+            insecureUrl,
+            {},
+          ),
+        ).rejects.toThrow(
+          'Cookie is Secure but this is not a secure connection',
         )
-        const cookies = await cookieJar.getCookies(insecureUrl)
-        expect(cookies).toEqual([])
       })
     })
 
@@ -59,7 +61,7 @@ describe('When `prefixSecurity` is enabled for `CookieJar`', () => {
       })
 
       it('should fail silently when no Domain or Path', async () => {
-        await cookieJar.setCookie('__Host-SID=12345; Secure', insecureUrl, {})
+        await cookieJar.setCookie('__Host-SID=12345; Secure', secureUrl, {})
         const cookies = await cookieJar.getCookies(insecureUrl)
         expect(cookies).toEqual([])
       })
@@ -67,20 +69,20 @@ describe('When `prefixSecurity` is enabled for `CookieJar`', () => {
       it('should fail silently when no Path', async () => {
         await cookieJar.setCookie(
           '__Host-SID=12345; Secure; Domain=example.com',
-          insecureUrl,
+          secureUrl,
           {},
         )
-        const cookies = await cookieJar.getCookies(insecureUrl)
+        const cookies = await cookieJar.getCookies(secureUrl)
         expect(cookies).toEqual([])
       })
 
       it('should fail silently with Domain', async () => {
         await cookieJar.setCookie(
           '__Host-SID=12345; Secure; Domain=example.com; Path=/',
-          insecureUrl,
+          secureUrl,
           {},
         )
-        const cookies = await cookieJar.getCookies(insecureUrl)
+        const cookies = await cookieJar.getCookies(secureUrl)
         expect(cookies).toEqual([])
       })
 
diff --git a/test/cookie_jar_test.js b/test/cookie_jar_test.js
index 1544df1..3b62689 100644
--- a/test/cookie_jar_test.js
+++ b/test/cookie_jar_test.js
@@ -289,6 +289,7 @@ vows
       topic: function() {
         const cj = new CookieJar();
         const ex = "http://example.com/index.html";
+        const exSecure = "https://example.com/index.html";
         const tasks = [];
         tasks.push(next => {
           cj.setCookie("a=1; Domain=example.com; Path=/", ex, at(0), next);
@@ -304,7 +305,7 @@ vows
         tasks.push(next => {
           cj.setCookie(
             "c=3; Domain=example.com; Path=/; Secure",
-            ex,
+            exSecure,
             at(2000),
             next
           );
diff --git a/test/cookie_prefixes_test.js b/test/cookie_prefixes_test.js
index 20f9fb0..c98a93b 100644
--- a/test/cookie_prefixes_test.js
+++ b/test/cookie_prefixes_test.js
@@ -66,17 +66,26 @@ vows
             assert.strictEqual(cookies[0].key, "__Secure-SID");
             assert.strictEqual(cookies[0].value, "12345");
           },
-          "with Secure attribute but not over https, should fail silently": function(
-            cj
-          ) {
-            assert.equal(PrefixSecurityEnum.SILENT, cj.prefixSecurity);
-            cj.setCookieSync(
-              "__Secure-SID=12345; Domain=example.com; Secure",
-              "http://www.example.com",
-              {}
-            );
-            const cookies = cj.getCookiesSync("http://www.example.com");
-            assert.isEmpty(cookies); // no cookies set
+          "with Secure attribute but not over https, should throw an error": {
+            topic: function(cj) {
+              try {
+                cj.setCookieSync(
+                  "__Secure-SID=12345; Domain=example.com; Secure",
+                  "http://www.example.com",
+                  {}
+                );
+                // If we get here, no error was thrown, so pass null
+                this.callback(null, null);
+              } catch (err) {
+                // Pass the error back to the test
+                this.callback(err);
+              }
+            },
+            "we should get an error": function(err, result) {
+              // `err` should not be null/undefined
+              assert.isNotNull(err);
+              assert.match(err.message, /Cookie is Secure but this is not a secure connection/);
+            }
           }
         },
         "for __Host prefix": {
@@ -94,7 +103,7 @@ vows
           "with no Domain or Path, should fail silently": function(cj) {
             assert.equal(PrefixSecurityEnum.SILENT, cj.prefixSecurity);
             cj.setCookieSync(
-              "__Host-SID=12345; Secure",
+              "__Host-SID=12345",
               "http://www.example.com",
               {}
             );
@@ -105,16 +114,16 @@ vows
             assert.equal(PrefixSecurityEnum.SILENT, cj.prefixSecurity);
             cj.setCookieSync(
               "__Host-SID=12345; Secure; Domain=example.com",
-              "http://www.example.com",
+              "https://www.example.com",
               {}
             );
-            const cookies = cj.getCookiesSync("http://www.example.com");
+            const cookies = cj.getCookiesSync("https://www.example.com");
             assert.isEmpty(cookies); // no cookies set
           },
           "with Domain, should fail silently": function(cj) {
             assert.equal(PrefixSecurityEnum.SILENT, cj.prefixSecurity);
             cj.setCookieSync(
-              "__Host-SID=12345; Secure; Domain=example.com; Path=/",
+              "__Host-SID=12345; Domain=example.com; Path=/",
               "http://www.example.com",
               {}
             );