Skip to content

Commit 1ef4b56

Browse files
authored
backport of #1449 (#1453)
* backport of #1449 * bump patch version
1 parent 8fe5c4e commit 1ef4b56

File tree

4 files changed

+94
-11
lines changed

4 files changed

+94
-11
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-fetch",
3-
"version": "2.6.6",
3+
"version": "2.6.7",
44
"description": "A light-weight module that brings window.fetch to node.js",
55
"main": "lib/index.js",
66
"browser": "./browser.js",

src/index.js

+40-9
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,29 @@ import https from 'https';
1313
import zlib from 'zlib';
1414
import Stream from 'stream';
1515

16-
import Body, { writeToStream, getTotalBytes } from './body';
17-
import Response from './response';
18-
import Headers, { createHeadersLenient } from './headers';
19-
import Request, { getNodeRequestOptions } from './request';
20-
import FetchError from './fetch-error';
21-
import AbortError from './abort-error';
16+
import Body, { writeToStream, getTotalBytes } from './body.js';
17+
import Response from './response.js';
18+
import Headers, { createHeadersLenient } from './headers.js';
19+
import Request, { getNodeRequestOptions } from './request.js';
20+
import FetchError from './fetch-error.js';
21+
import AbortError from './abort-error.js';
22+
23+
import whatwgUrl from 'whatwg-url';
24+
25+
const URL = Url.URL || whatwgUrl.URL;
2226

2327
// fix an issue where "PassThrough", "resolve" aren't a named export for node <10
2428
const PassThrough = Stream.PassThrough;
25-
const resolve_url = Url.resolve;
29+
30+
const isDomainOrSubdomain = (destination, original) => {
31+
const orig = new URL(original).hostname;
32+
const dest = new URL(destination).hostname;
33+
34+
return orig === dest || (
35+
orig[orig.length - dest.length - 1] === '.' && orig.endsWith(dest)
36+
);
37+
};
38+
2639

2740
/**
2841
* Fetch function
@@ -109,7 +122,19 @@ export default function fetch(url, opts) {
109122
const location = headers.get('Location');
110123

111124
// HTTP fetch step 5.3
112-
const locationURL = location === null ? null : resolve_url(request.url, location);
125+
let locationURL = null;
126+
try {
127+
locationURL = location === null ? null : new URL(location, request.url).toString();
128+
} catch (err) {
129+
// error here can only be invalid URL in Location: header
130+
// do not throw when options.redirect == manual
131+
// let the user extract the errorneous redirect URL
132+
if (request.redirect !== 'manual') {
133+
reject(new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, 'invalid-redirect'));
134+
finalize();
135+
return;
136+
}
137+
}
113138

114139
// HTTP fetch step 5.5
115140
switch (request.redirect) {
@@ -154,9 +179,15 @@ export default function fetch(url, opts) {
154179
body: request.body,
155180
signal: request.signal,
156181
timeout: request.timeout,
157-
size: request.size
182+
size: request.size
158183
};
159184

185+
if (!isDomainOrSubdomain(request.url, locationURL)) {
186+
for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) {
187+
requestOpts.headers.delete(name);
188+
}
189+
}
190+
160191
// HTTP-redirect fetch step 9
161192
if (res.statusCode !== 303 && request.body && getTotalBytes(request) === null) {
162193
reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect'));

test/server.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as http from 'http';
22
import { parse } from 'url';
33
import * as zlib from 'zlib';
4-
import * as stream from 'stream';
54
import { multipart as Multipart } from 'parted';
65

76
let convert;
@@ -66,6 +65,12 @@ export default class TestServer {
6665
}));
6766
}
6867

68+
if (p.startsWith('/redirect-to/3')) {
69+
res.statusCode = p.slice(13, 16);
70+
res.setHeader('Location', p.slice(17));
71+
res.end();
72+
}
73+
6974
if (p === '/gzip') {
7075
res.statusCode = 200;
7176
res.setHeader('Content-Type', 'text/plain');

test/test.js

+47
Original file line numberDiff line numberDiff line change
@@ -1569,6 +1569,53 @@ describe('node-fetch', () => {
15691569
});
15701570
});
15711571

1572+
it('should not forward secure headers to 3th party', () => {
1573+
return fetch(`${base}redirect-to/302/https://httpbin.org/get`, {
1574+
headers: new Headers({
1575+
cookie: 'gets=removed',
1576+
cookie2: 'gets=removed',
1577+
authorization: 'gets=removed',
1578+
'www-authenticate': 'gets=removed',
1579+
'other-safe-headers': 'stays',
1580+
'x-foo': 'bar'
1581+
})
1582+
}).then(res => res.json()).then(json => {
1583+
const headers = new Headers(json.headers);
1584+
// Safe headers are not removed
1585+
expect(headers.get('other-safe-headers')).to.equal('stays');
1586+
expect(headers.get('x-foo')).to.equal('bar');
1587+
// Unsafe headers should not have been sent to httpbin
1588+
expect(headers.get('cookie')).to.equal(null);
1589+
expect(headers.get('cookie2')).to.equal(null);
1590+
expect(headers.get('www-authenticate')).to.equal(null);
1591+
expect(headers.get('authorization')).to.equal(null);
1592+
});
1593+
});
1594+
1595+
it('should forward secure headers to same host', () => {
1596+
return fetch(`${base}redirect-to/302/${base}inspect`, {
1597+
headers: new Headers({
1598+
cookie: 'is=cookie',
1599+
cookie2: 'is=cookie2',
1600+
authorization: 'is=authorization',
1601+
'other-safe-headers': 'stays',
1602+
'www-authenticate': 'is=www-authenticate',
1603+
'x-foo': 'bar'
1604+
})
1605+
}).then(res => res.json().then(json => {
1606+
const headers = new Headers(json.headers);
1607+
// Safe headers are not removed
1608+
expect(res.url).to.equal(`${base}inspect`);
1609+
expect(headers.get('other-safe-headers')).to.equal('stays');
1610+
expect(headers.get('x-foo')).to.equal('bar');
1611+
// Unsafe headers should not have been sent to httpbin
1612+
expect(headers.get('cookie')).to.equal('is=cookie');
1613+
expect(headers.get('cookie2')).to.equal('is=cookie2');
1614+
expect(headers.get('www-authenticate')).to.equal('is=www-authenticate');
1615+
expect(headers.get('authorization')).to.equal('is=authorization');
1616+
}));
1617+
});
1618+
15721619
it('should allow PATCH request', function() {
15731620
const url = `${base}inspect`;
15741621
const opts = {

0 commit comments

Comments
 (0)