Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions spec/HeaderAliasesValidation.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict';

const Config = require('../lib/Config');

describe('Config.validateHeaderAliases', () => {
it('should accept null and undefined', () => {
expect(() => Config.validateHeaderAliases(null)).not.toThrow();
expect(() => Config.validateHeaderAliases(undefined)).not.toThrow();
});

it('should accept a valid headerAliases object', () => {
expect(() =>
Config.validateHeaderAliases({
'X-Parse-Application-Id': ['X-App-Id'],
'X-Parse-Session-Token': ['X-Session-Token-Alias'],
})
).not.toThrow();
});

it('should reject an alias that normalizes to the same value as its canonical header', () => {
expect(() =>
Config.validateHeaderAliases({
'X-Foo': ['x-foo'],
})
).toThrowError(/must not normalize to the same value as the canonical header name/);
});

it('should reject duplicate normalized aliases within the same aliases array', () => {
expect(() =>
Config.validateHeaderAliases({
'X-A': ['foo', 'FOO'],
})
).toThrowError(/Duplicate normalized header alias/);
});

it('should reject two canonical keys that normalize to the same value', () => {
expect(() =>
Config.validateHeaderAliases({
'X-Foo': [],
'x-foo': [],
})
).toThrowError(/collides with.*after trim and lowercasing/);
});

it('should reject the same normalized alias used for two different canonical headers', () => {
expect(() =>
Config.validateHeaderAliases({
'X-A': ['shared-alias'],
'X-B': ['Shared-Alias'],
})
).toThrowError(/collides with alias/);
});

it('should reject an alias that normalizes to another canonical header key', () => {
expect(() =>
Config.validateHeaderAliases({
'X-Parse-Bar': [],
'X-Parse-Foo': ['x-parse-bar'],
})
).toThrowError(/collides with canonical header/);
});
});
111 changes: 111 additions & 0 deletions spec/Middlewares.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,25 @@ describe('middlewares', () => {
expect(headers['Access-Control-Allow-Headers']).toContain(middlewares.DEFAULT_ALLOWED_HEADERS);
});

it('should append configured header aliases to Access-Control-Allow-Headers', () => {
AppCachePut(fakeReq.body._ApplicationId, {
headerAliases: {
'X-Parse-Application-Id': ['X-App-Id'],
'X-Parse-Session-Token': ['X-Session-Token-Alias'],
},
});
const headers = {};
const res = {
header: (key, value) => {
headers[key] = value;
},
};
const allowCrossDomain = middlewares.allowCrossDomain(fakeReq.body._ApplicationId);
allowCrossDomain(fakeReq, res, () => {});
expect(headers['Access-Control-Allow-Headers']).toContain('X-App-Id');
expect(headers['Access-Control-Allow-Headers']).toContain('X-Session-Token-Alias');
});

it('should set default Access-Control-Allow-Origin if allowOrigin is empty', () => {
AppCachePut(fakeReq.body._ApplicationId, {
allowOrigin: undefined,
Expand Down Expand Up @@ -409,6 +428,98 @@ describe('middlewares', () => {
});
});

it('should resolve app id from configured header alias', done => {
AppCachePut(fakeReq.body._ApplicationId, {
headerAliases: {
'X-Parse-Application-Id': ['X-App-Id'],
},
masterKeyIps: ['0.0.0.0/0'],
});
fakeReq.headers['x-app-id'] = fakeReq.body._ApplicationId;
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, () => {
expect(fakeReq.headers['x-parse-application-id']).toEqual(fakeReq.body._ApplicationId);
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.info.appId).toEqual(fakeReq.body._ApplicationId);
done();
});
});
});

it('should resolve session token from configured header alias', done => {
const sessionToken = 'session-token-via-alias';
AppCachePut(fakeReq.body._ApplicationId, {
headerAliases: {
'X-Parse-Session-Token': ['X-Session-Token-Alias'],
},
masterKeyIps: ['0.0.0.0/0'],
});
fakeReq.headers['x-session-token-alias'] = sessionToken;
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, () => {
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.info.sessionToken).toEqual(sessionToken);
done();
});
});
});

it('should prefer canonical session token over alias when both headers are present', done => {
const canonicalToken = 'session-token-canonical';
const aliasToken = 'session-token-alias-value';
AppCachePut(fakeReq.body._ApplicationId, {
headerAliases: {
'X-Parse-Session-Token': ['X-Session-Token-Alias'],
},
masterKeyIps: ['0.0.0.0/0'],
});
fakeReq.headers['x-parse-session-token'] = canonicalToken;
fakeReq.headers['x-session-token-alias'] = aliasToken;
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, () => {
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.info.sessionToken).toEqual(canonicalToken);
done();
});
});
});

it('should resolve master key from configured alias in handleParseAuth', async () => {
AppCachePut(fakeReq.body._ApplicationId, {
headerAliases: {
'X-Parse-Master-Key': ['X-Master-Key-Alias'],
},
masterKey: 'masterKey',
masterKeyIps: ['0.0.0.0/0'],
});
fakeReq.headers['x-master-key-alias'] = 'masterKey';
await new Promise(resolve =>
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, resolve)
);
await new Promise(resolve =>
middlewares.handleParseAuth(fakeReq.body._ApplicationId)(fakeReq, fakeRes, resolve)
);
expect(fakeReq.auth.isMaster).toBe(true);
});

it('should call next without throwing when app is not in AppCache', () => {
const next = jasmine.createSpy('next');
middlewares.handleHeaderAliases('NotInCacheAppId')(fakeReq, fakeRes, next);
expect(next).toHaveBeenCalled();
});

it('should call next without throwing when headerAliases is missing or null', done => {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['0.0.0.0/0'],
});
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, () => {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['0.0.0.0/0'],
headerAliases: null,
});
middlewares.handleHeaderAliases(fakeReq.body._ApplicationId)(fakeReq, fakeRes, done);
});
});

it('should give invalid response when upload file without x-parse-application-id in header', () => {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
Expand Down
98 changes: 97 additions & 1 deletion spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const {
GraphQLList,
} = require('graphql');
const { ParseServer } = require('../');
const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer');
const { ParseGraphQLServer, getCSRFRequestHeaders } = require('../lib/GraphQL/ParseGraphQLServer');
const { ReadPreference, Collection } = require('mongodb');
let uuidv4;

Expand Down Expand Up @@ -105,6 +105,30 @@ describe('ParseGraphQLServer', () => {
});
});

describe('getCSRFRequestHeaders', () => {
it('should include safe application-id header aliases with canonical header', () => {
const headers = getCSRFRequestHeaders({
'X-Parse-Application-Id': ['X-App-Id', 'X-Client-App'],
});
expect(headers).toEqual(['X-Parse-Application-Id', 'X-App-Id', 'X-Client-App']);
});

it('should exclude CORS-safelisted request-header names and Range from CSRF whitelist', () => {
const headers = getCSRFRequestHeaders({
'X-Parse-Application-Id': [
'Accept',
'accept-language',
'Content-Language',
'Content-Type',
'Range',
'rAnGe',
'X-Safe-Custom',
],
});
expect(headers).toEqual(['X-Parse-Application-Id', 'X-Safe-Custom']);
});
});

describe('_getServer', () => {
it('should only return new server on schema changes', async () => {
parseGraphQLServer.server = undefined;
Expand Down Expand Up @@ -135,6 +159,7 @@ describe('ParseGraphQLServer', () => {
expect(server).toBe(firstServer);
});
});

});

describe('_getGraphQLOptions', () => {
Expand Down Expand Up @@ -201,6 +226,46 @@ describe('ParseGraphQLServer', () => {
).not.toThrow();
expect(useCount).toBeGreaterThan(0);
});

it('registers header alias normalization before parse header handling', async () => {
const parseServerWithAliases = await global.reconfigureServer({
maintenanceKey: 'test2',
maxUploadSize: '1kb',
headerAliases: {
'X-Parse-Application-Id': ['X-App-Id'],
},
});
const graphQLServerWithAliases = new ParseGraphQLServer(parseServerWithAliases, {
graphQLPath: '/graphql',
playgroundPath: '/playground',
});
const middlewares = require('../lib/middlewares');
const useCalls = [];
const app = {
use: (...args) => {
useCalls.push(args);
},
};
graphQLServerWithAliases.applyGraphQL(app);
const parseHeadersIndex = useCalls.findIndex(
([path, middleware]) => path === '/graphql' && middleware === middlewares.handleParseHeaders
);
expect(parseHeadersIndex).toBeGreaterThan(0);
const [path, aliasMiddleware] = useCalls[parseHeadersIndex - 1];
expect(path).toBe('/graphql');
const req = {
originalUrl: '/graphql',
url: '/graphql',
protocol: 'http',
headers: {
host: 'localhost',
'x-app-id': parseServerWithAliases.config.appId,
},
get: key => req.headers[key.toLowerCase()],
};
await new Promise(resolve => aliasMiddleware(req, {}, resolve));
expect(req.headers['x-parse-application-id']).toBe(parseServerWithAliases.config.appId);
});
});

describe('applyPlayground', () => {
Expand Down Expand Up @@ -8325,6 +8390,37 @@ describe('ParseGraphQLServer', () => {
});

describe('Session Token', () => {
it('should retrieve me with session token header alias', async () => {
parseServer = await global.reconfigureServer({
headerAliases: {
'X-Parse-Session-Token': ['X-Session-Token-Alias'],
},
});
await createGQLFromParseServer(parseServer);
const username = `alias-gql-${uuidv4()}`;
const user = new Parse.User();
user.setUsername(username);
user.setPassword('password');
await user.signUp();
const result = await apolloClient.query({
query: gql`
query GetCurrentUser {
viewer {
user {
username
}
}
}
`,
context: {
headers: {
'X-Session-Token-Alias': user.getSessionToken(),
},
},
});
expect(result.data.viewer.user.username).toBe(username);
});

it('should fail due to invalid session token', async () => {
try {
await apolloClient.query({
Expand Down
49 changes: 49 additions & 0 deletions spec/rest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1738,6 +1738,55 @@ describe('read-only masterKey', () => {
});
});

describe('rest header aliases', () => {
it('supports REST requests with application-id header alias only', async () => {
await reconfigureServer({
headerAliases: {
'X-Parse-Application-Id': ['X-App-Id'],
},
});
try {
const response = await request({
url: `${Parse.serverURL}/schemas`,
method: 'GET',
headers: {
'X-App-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
},
});
expect(response.data.results).toBeDefined();
expect(Array.isArray(response.data.results)).toBe(true);
} finally {
await reconfigureServer();
}
});

it('supports /users/me with session-token header alias', async () => {
await reconfigureServer({
headerAliases: {
'X-Parse-Session-Token': ['X-Session-Token-Alias'],
},
});
try {
const username = `alias-rest-${Date.now()}`;
const user = await Parse.User.signUp(username, 'password');
const response = await request({
url: `${Parse.serverURL}/users/me`,
method: 'GET',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
'X-Session-Token-Alias': user.getSessionToken(),
},
});
expect(response.data.objectId).toBe(user.id);
expect(response.data.username).toBe(username);
} finally {
await reconfigureServer();
}
});
});
Comment on lines +1741 to +1788
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add a canonical-over-alias precedence test for REST headers.

These tests validate alias-only success, but they don’t assert the critical contract that canonical headers must win when both canonical and alias headers are present with conflicting values. Please add a regression test for that precedence behavior.

Suggested test addition
 describe('rest header aliases', () => {
+  it('prefers canonical session token over alias when both are set', async () => {
+    await reconfigureServer({
+      headerAliases: {
+        'X-Parse-Session-Token': ['X-Session-Token-Alias'],
+      },
+    });
+    try {
+      const canonicalUser = await Parse.User.signUp(`canonical-${Date.now()}`, 'password');
+      await Parse.User.logOut();
+      const aliasUser = await Parse.User.signUp(`alias-${Date.now()}`, 'password');
+      await Parse.User.logOut();
+
+      const response = await request({
+        url: `${Parse.serverURL}/users/me`,
+        method: 'GET',
+        headers: {
+          'X-Parse-Application-Id': Parse.applicationId,
+          'X-Parse-REST-API-Key': 'rest',
+          'X-Parse-Session-Token': canonicalUser.getSessionToken(),
+          'X-Session-Token-Alias': aliasUser.getSessionToken(),
+        },
+      });
+
+      expect(response.data.objectId).toBe(canonicalUser.id);
+    } finally {
+      await reconfigureServer();
+    }
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/rest.spec.js` around lines 1741 - 1788, Add a regression test that
verifies canonical REST headers take precedence over aliases when both are
present with conflicting values: within the existing "rest header aliases"
describe block create a test that calls reconfigureServer({ headerAliases: {
'X-Parse-Application-Id': ['X-App-Id'], 'X-Parse-Session-Token':
['X-Session-Token-Alias'] } }), then send a request to /schemas (or another
endpoint) including both the canonical header (e.g., 'X-Parse-Application-Id')
set to an invalid value and the alias header ('X-App-Id') set to a valid value
and assert the request fails (or uses the canonical value), and similarly create
a /users/me test where you sign up a user, send both 'X-Parse-Session-Token'
with an invalid token and 'X-Session-Token-Alias' with the real session token
and assert the canonical 'X-Parse-Session-Token' governs authentication (e.g.,
request is rejected or returns the user matching the canonical token behavior);
ensure you call reconfigureServer() in finally to reset server.


describe('rest context', () => {
it('should support dependency injection on rest api', async () => {
const requestContextMiddleware = (req, res, next) => {
Expand Down
Loading
Loading