Skip to content

Commit e193221

Browse files
committed
fix: Disallow header aliases for master and maintenance keys
Prevent credential-bearing Parse headers from being aliased so secrets cannot be smuggled under alternate names.
1 parent 08ccc17 commit e193221

10 files changed

Lines changed: 92 additions & 34 deletions

File tree

spec/HeaderAliasesValidation.spec.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,19 @@ describe('Config.validateHeaderAliases', () => {
143143
).toThrowError(/is not an allowed Parse header/);
144144
});
145145

146+
it('should reject credential-bearing master and maintenance key aliases', () => {
147+
expect(() =>
148+
Config.validateHeaderAliases({
149+
'X-Parse-Master-Key': ['X-Master-Key-Alias'],
150+
})
151+
).toThrowError(/is not an allowed Parse header/);
152+
expect(() =>
153+
Config.validateHeaderAliases({
154+
'X-Parse-Maintenance-Key': ['X-Maintenance-Key-Alias'],
155+
})
156+
).toThrowError(/is not an allowed Parse header/);
157+
});
158+
146159
it('should reject an alias that normalizes to the same value as its canonical header', () => {
147160
expect(() =>
148161
Config.validateHeaderAliases({

spec/Middlewares.spec.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,21 @@ describe('middlewares', () => {
573573
).toThrowError(/contains invalid characters/);
574574
});
575575

576-
it('should resolve master key from configured alias in handleParseAuth', async () => {
576+
it('should reject privileged master and maintenance key aliases at validation time', () => {
577+
const Config = require('../lib/Config');
578+
expect(() =>
579+
Config.validateHeaderAliases({
580+
'X-Parse-Master-Key': ['X-Master-Key-Alias'],
581+
})
582+
).toThrowError(/is not an allowed Parse header/);
583+
expect(() =>
584+
Config.validateHeaderAliases({
585+
'X-Parse-Maintenance-Key': ['X-Maintenance-Key-Alias'],
586+
})
587+
).toThrowError(/is not an allowed Parse header/);
588+
});
589+
590+
it('should not rewrite master-key aliases when present in request headers', done => {
577591
AppCachePut(fakeReq.body._ApplicationId, {
578592
headerAliases: {
579593
'X-Parse-Master-Key': ['X-Master-Key-Alias'],
@@ -582,13 +596,10 @@ describe('middlewares', () => {
582596
masterKeyIps: ['0.0.0.0/0'],
583597
});
584598
fakeReq.headers['x-master-key-alias'] = 'masterKey';
585-
await new Promise(resolve =>
586-
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, resolve)
587-
);
588-
await new Promise(resolve =>
589-
middlewares.handleParseAuth(fakeReq.body._ApplicationId)(fakeReq, fakeRes, resolve)
590-
);
591-
expect(fakeReq.auth.isMaster).toBe(true);
599+
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, () => {
600+
expect(fakeReq.headers['x-parse-master-key']).toBeUndefined();
601+
done();
602+
});
592603
});
593604

594605
it('should call next without throwing when app is not in AppCache', () => {

spec/ParseGraphQLServer.spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ describe('ParseGraphQLServer', () => {
127127
});
128128
expect(headers).toEqual(['X-Parse-Application-Id', 'X-Safe-Custom']);
129129
});
130+
131+
it('should tolerate null or undefined headerAliases without throwing', () => {
132+
expect(getCSRFRequestHeaders(null)).toEqual(['X-Parse-Application-Id']);
133+
expect(getCSRFRequestHeaders(undefined)).toEqual(['X-Parse-Application-Id']);
134+
});
130135
});
131136

132137
describe('_getServer', () => {

spec/rest.spec.js

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,6 +1739,20 @@ describe('read-only masterKey', () => {
17391739
});
17401740

17411741
describe('rest header aliases', () => {
1742+
const signUpViaRest = async username => {
1743+
const response = await request({
1744+
url: `${Parse.serverURL}/users`,
1745+
method: 'POST',
1746+
headers: {
1747+
'X-Parse-Application-Id': Parse.applicationId,
1748+
'X-Parse-REST-API-Key': 'rest',
1749+
'Content-Type': 'application/json',
1750+
},
1751+
body: { username, password: 'password' },
1752+
});
1753+
return response.data;
1754+
};
1755+
17421756
it('supports REST requests with application-id header alias only', async () => {
17431757
await reconfigureServer({
17441758
headerAliases: {
@@ -1793,19 +1807,20 @@ describe('rest header aliases', () => {
17931807
},
17941808
});
17951809
try {
1796-
const canonicalUser = await Parse.User.signUp(`alias-rest-canonical-${Date.now()}`, 'password');
1797-
const aliasUser = await Parse.User.signUp(`alias-rest-alias-${Date.now()}`, 'password');
1810+
// Create users via REST so the SDK current-user singleton is not shared/overwritten.
1811+
const canonicalUser = await signUpViaRest(`alias-rest-canonical-${Date.now()}`);
1812+
const aliasUser = await signUpViaRest(`alias-rest-alias-${Date.now()}`);
17981813
const response = await request({
17991814
url: `${Parse.serverURL}/users/me`,
18001815
method: 'GET',
18011816
headers: {
18021817
'X-Parse-Application-Id': Parse.applicationId,
18031818
'X-Parse-REST-API-Key': 'rest',
1804-
'X-Parse-Session-Token': canonicalUser.getSessionToken(),
1805-
'X-Session-Token-Alias': aliasUser.getSessionToken(),
1819+
'X-Parse-Session-Token': canonicalUser.sessionToken,
1820+
'X-Session-Token-Alias': aliasUser.sessionToken,
18061821
},
18071822
});
1808-
expect(response.data.objectId).toBe(canonicalUser.id);
1823+
expect(response.data.objectId).toBe(canonicalUser.objectId);
18091824
} finally {
18101825
await reconfigureServer();
18111826
}
@@ -1818,19 +1833,20 @@ describe('rest header aliases', () => {
18181833
},
18191834
});
18201835
try {
1821-
const firstAliasUser = await Parse.User.signUp(`alias-rest-a-${Date.now()}`, 'password');
1822-
const secondAliasUser = await Parse.User.signUp(`alias-rest-b-${Date.now()}`, 'password');
1836+
// Create users via REST so the SDK current-user singleton is not shared/overwritten.
1837+
const firstAliasUser = await signUpViaRest(`alias-rest-a-${Date.now()}`);
1838+
const secondAliasUser = await signUpViaRest(`alias-rest-b-${Date.now()}`);
18231839
const response = await request({
18241840
url: `${Parse.serverURL}/users/me`,
18251841
method: 'GET',
18261842
headers: {
18271843
'X-Parse-Application-Id': Parse.applicationId,
18281844
'X-Parse-REST-API-Key': 'rest',
1829-
'X-Session-Token-Alias-B': secondAliasUser.getSessionToken(),
1830-
'X-Session-Token-Alias-A': firstAliasUser.getSessionToken(),
1845+
'X-Session-Token-Alias-B': secondAliasUser.sessionToken,
1846+
'X-Session-Token-Alias-A': firstAliasUser.sessionToken,
18311847
},
18321848
});
1833-
expect(response.data.objectId).toBe(firstAliasUser.id);
1849+
expect(response.data.objectId).toBe(firstAliasUser.objectId);
18341850
} finally {
18351851
await reconfigureServer();
18361852
}

src/Config.js

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,20 @@ function removeTrailingSlash(str) {
4343
*/
4444
const asyncKeys = ['publicServerURL'];
4545

46+
// Canonical Parse headers that may be aliased. Credential-bearing headers
47+
// (master key, maintenance key) are intentionally excluded.
48+
const ALLOWED_HEADER_ALIAS_CANONICALS = new Set([
49+
'x-parse-application-id',
50+
'x-parse-session-token',
51+
'x-parse-installation-id',
52+
'x-parse-client-key',
53+
'x-parse-javascript-key',
54+
'x-parse-windows-key',
55+
'x-parse-rest-api-key',
56+
]);
57+
4658
export class Config {
59+
static ALLOWED_HEADER_ALIAS_CANONICALS = ALLOWED_HEADER_ALIAS_CANONICALS;
4760
static get(applicationId: string, mount: string) {
4861
const cacheInfo = AppCache.get(applicationId);
4962
if (!cacheInfo) {
@@ -777,17 +790,6 @@ export class Config {
777790
throw 'Header aliases must be an object';
778791
}
779792
const SAFE_HEADER_NAME = /^[A-Za-z0-9-]+$/;
780-
const ALLOWED_CANONICAL_HEADERS = new Set([
781-
'x-parse-application-id',
782-
'x-parse-session-token',
783-
'x-parse-master-key',
784-
'x-parse-maintenance-key',
785-
'x-parse-installation-id',
786-
'x-parse-client-key',
787-
'x-parse-javascript-key',
788-
'x-parse-windows-key',
789-
'x-parse-rest-api-key',
790-
]);
791793
const entries = Object.entries(headerAliases);
792794
for (const [canonicalHeader, aliases] of entries) {
793795
if (typeof canonicalHeader !== 'string' || !canonicalHeader.trim().length) {
@@ -799,7 +801,7 @@ export class Config {
799801
`Header aliases canonical '${canonicalHeader}' contains invalid characters`
800802
);
801803
}
802-
if (!ALLOWED_CANONICAL_HEADERS.has(trimmedCanonical.toLowerCase())) {
804+
if (!Config.ALLOWED_HEADER_ALIAS_CANONICALS.has(trimmedCanonical.toLowerCase())) {
803805
throw new Error(
804806
`Header aliases canonical '${canonicalHeader}' is not an allowed Parse header`
805807
);

src/Options/Definitions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ module.exports.ParseServerOptions = {
318318
},
319319
headerAliases: {
320320
env: 'PARSE_SERVER_HEADER_ALIASES',
321-
help: '(Optional) Define aliases for Parse request headers. For each canonical Parse header, set an array of accepted alias headers. If the canonical header is not present in a request, Parse Server uses the first matching alias.<br><br>Example:<br>`{ "X-Parse-Application-Id": ["X-App-Id"], "X-Parse-Session-Token": ["X-Session-Token"] }`<br><br>When setting this option via an environment variable, provide a JSON object string.',
321+
help: '(Optional) Define aliases for Parse request headers. For each allowed canonical Parse header, set an array of accepted alias headers. Aliases are only supported for non-secret headers (application ID, session token, installation ID, and client/API keys). Credential-bearing headers such as X-Parse-Master-Key and X-Parse-Maintenance-Key cannot be aliased. If the canonical header is not present in a request, Parse Server uses the first matching alias.<br><br>Example:<br>`{ "X-Parse-Application-Id": ["X-App-Id"], "X-Parse-Session-Token": ["X-Session-Token"] }`<br><br>When setting this option via an environment variable, provide a JSON object string.',
322322
action: parsers.objectParser,
323323
},
324324
host: {

src/Options/docs.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export interface ParseServerOptions {
9191
appName: ?string;
9292
/* Add headers to Access-Control-Allow-Headers */
9393
allowHeaders: ?(string[]);
94-
/* (Optional) Define aliases for Parse request headers. For each canonical Parse header, set an array of accepted alias headers. If the canonical header is not present in a request, Parse Server uses the first matching alias.<br><br>Example:<br>`{ "X-Parse-Application-Id": ["X-App-Id"], "X-Parse-Session-Token": ["X-Session-Token"] }`<br><br>When setting this option via an environment variable, provide a JSON object string.
94+
/* (Optional) Define aliases for Parse request headers. For each allowed canonical Parse header, set an array of accepted alias headers. Aliases are only supported for non-secret headers (application ID, session token, installation ID, and client/API keys). Credential-bearing headers such as X-Parse-Master-Key and X-Parse-Maintenance-Key cannot be aliased. If the canonical header is not present in a request, Parse Server uses the first matching alias.<br><br>Example:<br>`{ "X-Parse-Application-Id": ["X-App-Id"], "X-Parse-Session-Token": ["X-Session-Token"] }`<br><br>When setting this option via an environment variable, provide a JSON object string.
9595
:ENV: PARSE_SERVER_HEADER_ALIASES */
9696
headerAliases: ?{ [string]: string[] };
9797
/* Sets origins for Access-Control-Allow-Origin. This can be a string for a single origin or an array of strings for multiple origins. */

src/middlewares.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const mergeHeaders = (...headerSources) => {
8383
};
8484

8585
export function getHeaderAliases(headerAliases, canonicalHeader) {
86-
const aliases = headerAliases[canonicalHeader];
86+
const aliases = headerAliases?.[canonicalHeader];
8787
if (!Array.isArray(aliases)) {
8888
return [];
8989
}
@@ -95,6 +95,11 @@ function applyHeaderAliases(req, headerAliases) {
9595
req.headers = req.headers || {};
9696
for (const [canonicalHeader, aliases] of Object.entries(headerAliases || {})) {
9797
const canonicalKey = String(canonicalHeader).trim().toLowerCase();
98+
// Only rewrite allowlisted, non-secret Parse headers (one-to-one mapping
99+
// and destination allowlist are also enforced in Config.validateHeaderAliases).
100+
if (!Config.ALLOWED_HEADER_ALIAS_CANONICALS.has(canonicalKey)) {
101+
continue;
102+
}
98103
if (req.headers[canonicalKey] !== undefined) {
99104
continue; // canonical wins
100105
}

types/Options/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ export interface ParseServerOptions {
5252
maintenanceKeyIps?: (string[]);
5353
appName?: string;
5454
allowHeaders?: (string[]);
55+
/**
56+
* Optional aliases for non-secret Parse request headers only
57+
* (application ID, session token, installation ID, client/API keys).
58+
* Credential-bearing headers such as X-Parse-Master-Key and
59+
* X-Parse-Maintenance-Key cannot be aliased and are rejected at validation.
60+
*/
5561
headerAliases?: { [headerName: string]: string[] };
5662
allowOrigin?: StringOrStringArray;
5763
analyticsAdapter?: Adapter<AnalyticsAdapter>;

0 commit comments

Comments
 (0)