Skip to content

Commit 4328fb1

Browse files
committed
add static helper method: "getNormalizedFormData"
During some real-world testing, I noticed that Chrome doesn't always parse 'application/x-www-form-urlencoded' data in the POST body. This new helper method is available to providers to normalize the format of this data into a key/value hash object. Furthermore, it will also parse 'application/json' data.
1 parent 29febe8 commit 4328fb1

File tree

6 files changed

+132
-77
lines changed

6 files changed

+132
-77
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "privacy-pass",
3-
"version": "3.6.4",
3+
"version": "3.6.5",
44
"private": true,
55
"contributors": [
66
"Suphanat Chunhapanya <[email protected]>",

public/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "__MSG_appName__",
33
"description": "__MSG_appDescription__",
4-
"version": "3.6.4",
4+
"version": "3.6.5",
55
"manifest_version": 2,
66
"default_locale": "en",
77
"icons": {

src/background/providers/cloudflare.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ describe('issuance', () => {
116116
},
117117
};
118118

119+
const flattenFormData: { [key: string]: string[] | string } = {
120+
'h-captcha-response': 'body-param',
121+
};
122+
119123
test('valid request', async () => {
120124
const storage = new StorageMock();
121125
const updateIcon = jest.fn();
@@ -137,7 +141,7 @@ describe('issuance', () => {
137141
issueInfo = provider['issueInfo'];
138142
expect(issueInfo!.requestId).toEqual(bodyDetails.requestId);
139143
expect(issueInfo!.url).toEqual(bodyDetails.url);
140-
expect(issueInfo!.formData).toEqual(bodyDetails.requestBody!.formData);
144+
expect(issueInfo!.formData).toEqual(flattenFormData);
141145

142146
const headDetails: any = {
143147
...validDetails,
@@ -199,7 +203,7 @@ describe('issuance', () => {
199203
issueInfo = provider['issueInfo'];
200204
expect(issueInfo!.requestId).toEqual(bodyDetails.requestId);
201205
expect(issueInfo!.url).toEqual(bodyDetails.url);
202-
expect(issueInfo!.formData).toEqual(bodyDetails.requestBody!.formData);
206+
expect(issueInfo!.formData).toEqual(flattenFormData);
203207

204208
const headDetails: any = {
205209
...validDetails,
@@ -271,7 +275,7 @@ describe('issuance', () => {
271275
issueInfo = provider['issueInfo'];
272276
expect(issueInfo!.requestId).toEqual(bodyDetails.requestId);
273277
expect(issueInfo!.url).toEqual(bodyDetails.url);
274-
expect(issueInfo!.formData).toEqual(bodyDetails.requestBody!.formData);
278+
expect(issueInfo!.formData).toEqual(flattenFormData);
275279

276280
const headDetails: any = {
277281
...validDetails,

src/background/providers/cloudflare.ts

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as voprf from '../crypto/voprf';
22

3-
import { Provider, EarnedTokenCookie, Callbacks, QUALIFIED_HOSTNAMES, QUALIFIED_PATHNAMES, QUALIFIED_PARAMS, isIssuingHostname, isQualifiedPathname, areQualifiedQueryParams, areQualifiedBodyFormParams } from './provider';
3+
import { Provider, EarnedTokenCookie, Callbacks, QUALIFIED_HOSTNAMES, QUALIFIED_PATHNAMES, QUALIFIED_PARAMS, isIssuingHostname, isQualifiedPathname, areQualifiedQueryParams, areQualifiedBodyFormParams, getNormalizedFormData } from './provider';
44
import { Storage } from '../storage';
55
import Token from '../token';
66
import axios from 'axios';
@@ -16,23 +16,22 @@ const COMMITMENT_URL: string =
1616
'https://raw.githubusercontent.com/privacypass/ec-commitments/master/commitments-p256.json';
1717

1818
const ALL_ISSUING_CRITERIA: {
19-
HOSTNAMES: QUALIFIED_HOSTNAMES;
20-
PATHNAMES: QUALIFIED_PATHNAMES;
21-
QUERY_PARAMS: QUALIFIED_PARAMS;
22-
BODY_PARAMS: QUALIFIED_PARAMS;
19+
HOSTNAMES: QUALIFIED_HOSTNAMES | void;
20+
PATHNAMES: QUALIFIED_PATHNAMES | void;
21+
QUERY_PARAMS: QUALIFIED_PARAMS | void;
22+
BODY_PARAMS: QUALIFIED_PARAMS | void;
2323
} = {
2424
HOSTNAMES: {
2525
exact : [DEFAULT_ISSUING_HOSTNAME],
2626
contains: [`.${DEFAULT_ISSUING_HOSTNAME}`],
2727
},
28-
PATHNAMES: {
29-
},
28+
PATHNAMES: undefined,
3029
QUERY_PARAMS: {
31-
some: ['__cf_chl_captcha_tk__', '__cf_chl_managed_tk__'],
30+
some: ['__cf_chl_captcha_tk__', '__cf_chl_managed_tk__'],
3231
},
3332
BODY_PARAMS: {
34-
some: ['g-recaptcha-response', 'h-captcha-response', 'cf_captcha_kind'],
35-
}
33+
some: ['g-recaptcha-response', 'h-captcha-response', 'cf_captcha_kind'],
34+
},
3635
}
3736

3837
const VERIFICATION_KEY: string = `-----BEGIN PUBLIC KEY-----
@@ -110,26 +109,18 @@ export class CloudflareProvider extends Provider {
110109
handleBeforeRequest(
111110
details: chrome.webRequest.WebRequestBodyDetails,
112111
): chrome.webRequest.BlockingResponse | void {
113-
const url = new URL(details.url);
114-
const formData: { [key: string]: string[] | string } = (details.requestBody && details.requestBody.formData)
115-
? details.requestBody.formData
116-
: {}
117-
;
118-
119-
if (this.matchesIssuingBodyCriteria(details, url, formData)) {
120-
this.issueInfo = { requestId: details.requestId, url: details.url, formData };
121112

113+
if (this.matchesIssuingBodyCriteria(details)) {
122114
// do NOT cancel the request with captcha solution.
123115
// note: "handleBeforeSendHeaders" will cancel this request if additional criteria are satisfied.
124116
return { cancel: false };
125117
}
126118
}
127119

128120
private matchesIssuingBodyCriteria(
129-
details: chrome.webRequest.WebRequestBodyDetails,
130-
url: URL,
131-
formData: { [key: string]: string[] | string },
121+
details: chrome.webRequest.WebRequestBodyDetails,
132122
): boolean {
123+
133124
// Only issue tokens for POST requests that contain data in body.
134125
if (
135126
(details.method.toUpperCase() !== 'POST' ) ||
@@ -139,6 +130,8 @@ export class CloudflareProvider extends Provider {
139130
return false;
140131
}
141132

133+
const url: URL = new URL(details.url);
134+
142135
// Only issue tokens to hosts belonging to the provider.
143136
if (!isIssuingHostname(ALL_ISSUING_CRITERIA.HOSTNAMES, url)) {
144137
return false;
@@ -149,11 +142,15 @@ export class CloudflareProvider extends Provider {
149142
return false;
150143
}
151144

152-
// Only issue tokens when 'application/x-www-form-urlencoded' data parameters in POST body pass defined criteria.
145+
const formData: { [key: string]: string[] | string } = getNormalizedFormData(details, /* flatten= */ true);
146+
147+
// Only issue tokens when 'application/x-www-form-urlencoded' or 'application/json' data parameters in POST body pass defined criteria.
153148
if (!areQualifiedBodyFormParams(ALL_ISSUING_CRITERIA.BODY_PARAMS, formData)) {
154149
return false;
155150
}
156151

152+
this.issueInfo = { requestId: details.requestId, url: details.url, formData };
153+
157154
return true;
158155
}
159156

@@ -321,19 +318,8 @@ export class CloudflareProvider extends Provider {
321318
formData: { [key: string]: string[] | string },
322319
): Promise<void> {
323320
try {
324-
// Normalize 'application/x-www-form-urlencoded' data parameters in POST body
325-
const flattenFormData: { [key: string]: string[] | string } = {};
326-
for (const key in formData) {
327-
if (Array.isArray(formData[key]) && (formData[key].length === 1)) {
328-
const [value] = formData[key];
329-
flattenFormData[key] = value;
330-
} else {
331-
flattenFormData[key] = formData[key];
332-
}
333-
}
334-
335321
// Issue tokens.
336-
const tokens = await this.issue(url, flattenFormData);
322+
const tokens = await this.issue(url, formData);
337323

338324
// Store tokens.
339325
const cached = this.getStoredTokens();

src/background/providers/hcaptcha.ts

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as voprf from '../crypto/voprf';
22

3-
import { Provider, EarnedTokenCookie, Callbacks, QUALIFIED_HOSTNAMES, QUALIFIED_PATHNAMES, QUALIFIED_PARAMS, isIssuingHostname, isQualifiedPathname, areQualifiedQueryParams, areQualifiedBodyFormParams } from './provider';
3+
import { Provider, EarnedTokenCookie, Callbacks, QUALIFIED_HOSTNAMES, QUALIFIED_PATHNAMES, QUALIFIED_PARAMS, isIssuingHostname, isQualifiedPathname, areQualifiedQueryParams, areQualifiedBodyFormParams, getNormalizedFormData } from './provider';
44
import { Storage } from '../storage';
55
import Token from '../token';
66
import axios from 'axios';
@@ -15,10 +15,10 @@ const COMMITMENT_URL: string =
1515
'https://raw.githubusercontent.com/privacypass/ec-commitments/master/commitments-p256.json';
1616

1717
const ALL_ISSUING_CRITERIA: {
18-
HOSTNAMES: QUALIFIED_HOSTNAMES;
19-
PATHNAMES: QUALIFIED_PATHNAMES;
20-
QUERY_PARAMS: QUALIFIED_PARAMS;
21-
BODY_PARAMS: QUALIFIED_PARAMS;
18+
HOSTNAMES: QUALIFIED_HOSTNAMES | void;
19+
PATHNAMES: QUALIFIED_PATHNAMES | void;
20+
QUERY_PARAMS: QUALIFIED_PARAMS | void;
21+
BODY_PARAMS: QUALIFIED_PARAMS | void;
2222
} = {
2323
HOSTNAMES: {
2424
exact : [DEFAULT_ISSUING_HOSTNAME],
@@ -28,17 +28,16 @@ const ALL_ISSUING_CRITERIA: {
2828
contains: ['/checkcaptcha'],
2929
},
3030
QUERY_PARAMS: {
31-
some: ['s=00000000-0000-0000-0000-000000000000'],
31+
some: ['s=00000000-0000-0000-0000-000000000000'],
3232
},
33-
BODY_PARAMS: {
34-
}
33+
BODY_PARAMS: undefined,
3534
}
3635

3736
const ALL_REDEMPTION_CRITERIA: {
38-
HOSTNAMES: QUALIFIED_HOSTNAMES;
39-
PATHNAMES: QUALIFIED_PATHNAMES;
40-
QUERY_PARAMS: QUALIFIED_PARAMS;
41-
BODY_PARAMS: QUALIFIED_PARAMS;
37+
HOSTNAMES: QUALIFIED_HOSTNAMES | void;
38+
PATHNAMES: QUALIFIED_PATHNAMES | void;
39+
QUERY_PARAMS: QUALIFIED_PARAMS | void;
40+
BODY_PARAMS: QUALIFIED_PARAMS | void;
4241
} = {
4342
HOSTNAMES: {
4443
exact : [DEFAULT_ISSUING_HOSTNAME],
@@ -48,11 +47,11 @@ const ALL_REDEMPTION_CRITERIA: {
4847
contains: ['/getcaptcha'],
4948
},
5049
QUERY_PARAMS: {
51-
some: ['s!=00000000-0000-0000-0000-000000000000'],
50+
some: ['s!=00000000-0000-0000-0000-000000000000'],
5251
},
5352
BODY_PARAMS: {
54-
every: ['sitekey!=00000000-0000-0000-0000-000000000000', 'motionData', 'host!=www.hcaptcha.com'],
55-
}
53+
every: ['sitekey!=00000000-0000-0000-0000-000000000000', 'motionData', 'host!=www.hcaptcha.com'],
54+
},
5655
}
5756

5857
const VERIFICATION_KEY: string = `-----BEGIN PUBLIC KEY-----
@@ -128,33 +127,23 @@ export class HcaptchaProvider extends Provider {
128127
handleBeforeRequest(
129128
details: chrome.webRequest.WebRequestBodyDetails,
130129
): chrome.webRequest.BlockingResponse | void {
131-
const url = new URL(details.url);
132-
const formData: { [key: string]: string[] | string } = (details.requestBody && details.requestBody.formData)
133-
? details.requestBody.formData
134-
: {}
135-
;
136-
137-
if (this.matchesIssuingCriteria(details, url, formData)) {
138-
this.issueInfo = { requestId: details.requestId, url: details.url };
139130

131+
if (this.matchesIssuingCriteria(details)) {
140132
// do NOT cancel the request with captcha solution.
141133
return { cancel: false };
142134
}
143135

144-
if (this.matchesRedemptionCriteria(details, url, formData)) {
145-
this.redeemInfo = { requestId: details.requestId };
146-
136+
if (this.matchesRedemptionCriteria(details)) {
147137
// do NOT cancel the request to generate a new captcha.
148138
// note: "handleBeforeSendHeaders" will add request headers to embed a token.
149139
return { cancel: false };
150140
}
151141
}
152142

153143
private matchesIssuingCriteria(
154-
details: chrome.webRequest.WebRequestBodyDetails,
155-
url: URL,
156-
formData: { [key: string]: string[] | string }
144+
details: chrome.webRequest.WebRequestBodyDetails,
157145
): boolean {
146+
158147
// Only issue tokens for POST requests that contain data in body.
159148
if (
160149
(details.method.toUpperCase() !== 'POST' ) ||
@@ -164,6 +153,8 @@ export class HcaptchaProvider extends Provider {
164153
return false;
165154
}
166155

156+
const url: URL = new URL(details.url);
157+
167158
// Only issue tokens to hosts belonging to the provider.
168159
if (!isIssuingHostname(ALL_ISSUING_CRITERIA.HOSTNAMES, url)) {
169160
return false;
@@ -179,19 +170,25 @@ export class HcaptchaProvider extends Provider {
179170
return false;
180171
}
181172

182-
// Only issue tokens when 'application/x-www-form-urlencoded' data parameters in POST body pass defined criteria.
183-
if (!areQualifiedBodyFormParams(ALL_ISSUING_CRITERIA.BODY_PARAMS, formData)) {
184-
return false;
173+
// conditionally short-circuit an expensive operation
174+
if (ALL_ISSUING_CRITERIA.BODY_PARAMS !== undefined) {
175+
const formData: { [key: string]: string[] | string } = getNormalizedFormData(details);
176+
177+
// Only issue tokens when 'application/x-www-form-urlencoded' or 'application/json' data parameters in POST body pass defined criteria.
178+
if (!areQualifiedBodyFormParams(ALL_ISSUING_CRITERIA.BODY_PARAMS, formData)) {
179+
return false;
180+
}
185181
}
186182

183+
this.issueInfo = { requestId: details.requestId, url: details.url };
184+
187185
return true;
188186
}
189187

190188
private matchesRedemptionCriteria(
191-
details: chrome.webRequest.WebRequestBodyDetails,
192-
url: URL,
193-
formData: { [key: string]: string[] | string }
189+
details: chrome.webRequest.WebRequestBodyDetails,
194190
): boolean {
191+
195192
// Only redeem tokens for POST requests that contain data in body.
196193
if (
197194
(details.method.toUpperCase() !== 'POST' ) ||
@@ -201,6 +198,8 @@ export class HcaptchaProvider extends Provider {
201198
return false;
202199
}
203200

201+
const url: URL = new URL(details.url);
202+
204203
// Only redeem tokens to hosts belonging to the provider.
205204
if (!isIssuingHostname(ALL_REDEMPTION_CRITERIA.HOSTNAMES, url)) {
206205
return false;
@@ -216,11 +215,18 @@ export class HcaptchaProvider extends Provider {
216215
return false;
217216
}
218217

219-
// Only redeem tokens when 'application/x-www-form-urlencoded' data parameters in POST body pass defined criteria.
220-
if (!areQualifiedBodyFormParams(ALL_REDEMPTION_CRITERIA.BODY_PARAMS, formData)) {
221-
return false;
218+
// conditionally short-circuit an expensive operation
219+
if (ALL_REDEMPTION_CRITERIA.BODY_PARAMS !== undefined) {
220+
const formData: { [key: string]: string[] | string } = getNormalizedFormData(details);
221+
222+
// Only redeem tokens when 'application/x-www-form-urlencoded' or 'application/json' data parameters in POST body pass defined criteria.
223+
if (!areQualifiedBodyFormParams(ALL_REDEMPTION_CRITERIA.BODY_PARAMS, formData)) {
224+
return false;
225+
}
222226
}
223227

228+
this.redeemInfo = { requestId: details.requestId };
229+
224230
return true;
225231
}
226232

0 commit comments

Comments
 (0)