Skip to content

Commit

Permalink
Add extra validation checks for cookies
Browse files Browse the repository at this point in the history
Cookies are now checked for attributes that match the current
Kibana configuration.
  • Loading branch information
jportner committed Nov 13, 2019
1 parent 59e0a1c commit 8eb4b9a
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 77 deletions.
32 changes: 29 additions & 3 deletions src/core/server/http/cookie_session_storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface SessionStorageCookieOptions<T> {
/**
* Function called to validate a cookie content.
*/
validate: (sessionValue: T) => boolean | Promise<boolean>;
validate: (sessionValue: T) => ValidationResult;
/**
* Flag indicating whether the cookie should be sent only via a secure connection.
*/
Expand Down Expand Up @@ -85,6 +85,12 @@ class ScopedCookieSessionStorage<T extends Record<string, any>> implements Sessi
}
}

interface ValidationResult {
isValid: boolean;
path?: string;
isSecure?: boolean;
}

/**
* Creates SessionStorage factory, which abstract the way of
* session storage implementation and scoping to the incoming requests.
Expand All @@ -98,15 +104,35 @@ export async function createCookieSessionStorageFactory<T>(
cookieOptions: SessionStorageCookieOptions<T>,
basePath?: string
): Promise<SessionStorageFactory<T>> {
function clearInvalidCookie(
req: Request | undefined,
path: string = basePath || '/',
isSecure: boolean = cookieOptions.isSecure
) {
// if the cookie did not include the 'path' or 'isSecure' attributes in the session value, it is a legacy cookie
// we will assume that the cookie was created with the current configuration
log.debug(`Clearing invalid session cookie`);
// need to use Hapi toolkit to clear cookie with defined options
if (req) {
(req.cookieAuth as any).h.unstate(cookieOptions.name, { path, isSecure });
}
}

await server.register({ plugin: hapiAuthCookie });

server.auth.strategy('security-cookie', 'cookie', {
cookie: cookieOptions.name,
password: cookieOptions.encryptionKey,
validateFunc: async (req, session: T) => ({ valid: await cookieOptions.validate(session) }),
validateFunc: async (req, session: T) => {
const result = cookieOptions.validate(session);
if (!result.isValid) {
clearInvalidCookie(req, result.path, result.isSecure);
}
return { valid: result.isValid };
},
isSecure: cookieOptions.isSecure,
path: basePath,
clearInvalid: true,
clearInvalid: false,
isHttpOnly: true,
isSameSite: false,
});
Expand Down
68 changes: 60 additions & 8 deletions src/core/server/http/cookie_sesson_storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ interface User {
interface Storage {
value: User;
expires: number;
path: string;
}

function retrieveSessionCookie(cookies: string) {
Expand All @@ -92,13 +93,18 @@ function retrieveSessionCookie(cookies: string) {

const userData = { id: '42' };
const sessionDurationMs = 1000;
const path = '/';
const sessVal = () => ({ value: userData, expires: Date.now() + sessionDurationMs, path });
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
const cookieOptions = {
name: 'sid',
encryptionKey: 'something_at_least_32_characters',
validate: (session: Storage) => session.expires > Date.now(),
validate: (session: Storage) => {
const isValid = session.path === path && session.expires > Date.now();
return { isValid, path: session.path, isSecure: false };
},
isSecure: false,
path: '/',
path,
};

describe('Cookie based SessionStorage', () => {
Expand All @@ -107,9 +113,9 @@ describe('Cookie based SessionStorage', () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('');

router.get({ path: '/', validate: false }, (context, req, res) => {
router.get({ path, validate: false }, (context, req, res) => {
const sessionStorage = factory.asScoped(req);
sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs });
sessionStorage.set(sessVal());
return res.ok({});
});

Expand All @@ -136,6 +142,7 @@ describe('Cookie based SessionStorage', () => {
expect(sessionCookie.httpOnly).toBe(true);
});
});

describe('#get()', () => {
it('reads from session storage', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
Expand All @@ -145,7 +152,7 @@ describe('Cookie based SessionStorage', () => {
const sessionStorage = factory.asScoped(req);
const sessionValue = await sessionStorage.get();
if (!sessionValue) {
sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs });
sessionStorage.set(sessVal());
return res.ok();
}
return res.ok({ body: { value: sessionValue.value } });
Expand Down Expand Up @@ -173,6 +180,7 @@ describe('Cookie based SessionStorage', () => {
.set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`)
.expect(200, { value: userData });
});

it('returns null for empty session', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);

Expand All @@ -198,7 +206,7 @@ describe('Cookie based SessionStorage', () => {
expect(cookies).not.toBeDefined();
});

it('returns null for invalid session & clean cookies', async () => {
it('returns null for invalid session (expired) & clean cookies', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);

const router = createRouter('');
Expand All @@ -208,7 +216,7 @@ describe('Cookie based SessionStorage', () => {
const sessionStorage = factory.asScoped(req);
if (!setOnce) {
setOnce = true;
sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs });
sessionStorage.set(sessVal());
return res.ok({ body: { value: userData } });
}
const sessionValue = await sessionStorage.get();
Expand Down Expand Up @@ -242,6 +250,50 @@ describe('Cookie based SessionStorage', () => {
'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/',
]);
});

it('returns null for invalid session (incorrect path) & clean cookies accurately', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);

const router = createRouter('');

let setOnce = false;
router.get({ path: '/', validate: false }, async (context, req, res) => {
const sessionStorage = factory.asScoped(req);
if (!setOnce) {
setOnce = true;
sessionStorage.set({ ...sessVal(), path: '/foo' });
return res.ok({ body: { value: userData } });
}
const sessionValue = await sessionStorage.get();
return res.ok({ body: { value: sessionValue } });
});

const factory = await createCookieSessionStorageFactory(
logger.get(),
innerServer,
cookieOptions
);
await server.start();

const response = await supertest(innerServer.listener)
.get('/')
.expect(200, { value: userData });

const cookies = response.get('set-cookie');
expect(cookies).toBeDefined();

const sessionCookie = retrieveSessionCookie(cookies[0]);
const response2 = await supertest(innerServer.listener)
.get('/')
.set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`)
.expect(200, { value: null });

const cookies2 = response2.get('set-cookie');
expect(cookies2).toEqual([
'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/foo',
]);
});

// use mocks to simplify test setup
it('returns null if multiple session cookies are detected.', async () => {
const mockServer = {
Expand Down Expand Up @@ -342,7 +394,7 @@ describe('Cookie based SessionStorage', () => {
sessionStorage.clear();
return res.ok({});
}
sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs });
sessionStorage.set(sessVal());
return res.ok({});
});

Expand Down
2 changes: 1 addition & 1 deletion src/core/server/http/http_server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { HttpServer } from './http_server';
const cookieOptions = {
name: 'sid',
encryptionKey: 'something_at_least_32_characters',
validate: () => true,
validate: () => ({ isValid: true }),
isSecure: false,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('http service', () => {
const cookieOptions = {
name: 'sid',
encryptionKey: 'something_at_least_32_characters',
validate: (session: StorageData) => true,
validate: (session: StorageData) => ({ isValid: true }),
isSecure: false,
path: '/',
};
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/http/integration_tests/lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ describe('Auth', () => {
const cookieOptions = {
name: 'sid',
encryptionKey: 'something_at_least_32_characters',
validate: () => true,
validate: () => ({ isValid: true }),
isSecure: false,
};

Expand Down
Loading

0 comments on commit 8eb4b9a

Please sign in to comment.