mirror of
https://github.com/laurent22/joplin.git
synced 2025-04-04 21:35:03 +02:00
Server: Increase cookies security - set HttpOnly, Secure and SameSite flags
This commit is contained in:
parent
e0971baec4
commit
bcadb3662b
@ -46,6 +46,8 @@ export interface EnvVariables {
|
|||||||
SUPPORT_NAME?: string;
|
SUPPORT_NAME?: string;
|
||||||
|
|
||||||
BUSINESS_EMAIL?: string;
|
BUSINESS_EMAIL?: string;
|
||||||
|
|
||||||
|
COOKIES_SECURE?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let runningInDocker_: boolean = false;
|
let runningInDocker_: boolean = false;
|
||||||
@ -168,6 +170,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
|
|||||||
supportEmail,
|
supportEmail,
|
||||||
supportName: env.SUPPORT_NAME || appName,
|
supportName: env.SUPPORT_NAME || appName,
|
||||||
businessEmail: env.BUSINESS_EMAIL || supportEmail,
|
businessEmail: env.BUSINESS_EMAIL || supportEmail,
|
||||||
|
cookieSecure: env.COOKIES_SECURE === '1',
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Session } from '../../db';
|
import { Session } from '../../db';
|
||||||
import routeHandler from '../../middleware/routeHandler';
|
import routeHandler from '../../middleware/routeHandler';
|
||||||
|
import { cookieGet } from '../../utils/cookies';
|
||||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, models, parseHtml, createUser } from '../../utils/testing/testUtils';
|
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, models, parseHtml, createUser } from '../../utils/testing/testUtils';
|
||||||
import { AppContext } from '../../utils/types';
|
import { AppContext } from '../../utils/types';
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ describe('index_login', function() {
|
|||||||
const user = await createUser(1);
|
const user = await createUser(1);
|
||||||
|
|
||||||
const context = await doLogin(user.email, '123456');
|
const context = await doLogin(user.email, '123456');
|
||||||
const sessionId = context.cookies.get('sessionId');
|
const sessionId = cookieGet(context, 'sessionId');
|
||||||
const session: Session = await models().session().load(sessionId);
|
const session: Session = await models().session().load(sessionId);
|
||||||
expect(session.user_id).toBe(user.id);
|
expect(session.user_id).toBe(user.id);
|
||||||
});
|
});
|
||||||
@ -62,12 +63,12 @@ describe('index_login', function() {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const context = await doLogin('bad', '123456');
|
const context = await doLogin('bad', '123456');
|
||||||
expect(!context.cookies.get('sessionId')).toBe(true);
|
expect(!cookieGet(context, 'sessionId')).toBe(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const context = await doLogin(user.email, 'bad');
|
const context = await doLogin(user.email, 'bad');
|
||||||
expect(!context.cookies.get('sessionId')).toBe(true);
|
expect(!cookieGet(context, 'sessionId')).toBe(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import config from '../../config';
|
|||||||
import defaultView from '../../utils/defaultView';
|
import defaultView from '../../utils/defaultView';
|
||||||
import { View } from '../../services/MustacheService';
|
import { View } from '../../services/MustacheService';
|
||||||
import limiterLoginBruteForce from '../../utils/request/limiterLoginBruteForce';
|
import limiterLoginBruteForce from '../../utils/request/limiterLoginBruteForce';
|
||||||
|
import { cookieSet } from '../../utils/cookies';
|
||||||
|
|
||||||
function makeView(error: any = null): View {
|
function makeView(error: any = null): View {
|
||||||
const view = defaultView('login', 'Login');
|
const view = defaultView('login', 'Login');
|
||||||
@ -32,7 +33,7 @@ router.post('login', async (_path: SubPath, ctx: AppContext) => {
|
|||||||
const body = await formParse(ctx.req);
|
const body = await formParse(ctx.req);
|
||||||
|
|
||||||
const session = await ctx.joplin.models.session().authenticate(body.fields.email, body.fields.password);
|
const session = await ctx.joplin.models.session().authenticate(body.fields.email, body.fields.password);
|
||||||
ctx.cookies.set('sessionId', session.id);
|
cookieSet(ctx, 'sessionId', session.id);
|
||||||
return redirect(ctx, `${config().baseUrl}/home`);
|
return redirect(ctx, `${config().baseUrl}/home`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return makeView(error);
|
return makeView(error);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import routeHandler from '../../middleware/routeHandler';
|
import routeHandler from '../../middleware/routeHandler';
|
||||||
|
import { cookieGet } from '../../utils/cookies';
|
||||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, models, createUserAndSession } from '../../utils/testing/testUtils';
|
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, models, createUserAndSession } from '../../utils/testing/testUtils';
|
||||||
|
|
||||||
describe('index_logout', function() {
|
describe('index_logout', function() {
|
||||||
@ -26,11 +27,11 @@ describe('index_logout', function() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(context.cookies.get('sessionId')).toBe(session.id);
|
expect(cookieGet(context, 'sessionId')).toBe(session.id);
|
||||||
expect(!!(await models().session().load(session.id))).toBe(true);
|
expect(!!(await models().session().load(session.id))).toBe(true);
|
||||||
await routeHandler(context);
|
await routeHandler(context);
|
||||||
|
|
||||||
expect(!context.cookies.get('sessionId')).toBe(true);
|
expect(!cookieGet(context, 'sessionId')).toBe(true);
|
||||||
expect(!!(await models().session().load(session.id))).toBe(false);
|
expect(!!(await models().session().load(session.id))).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4,12 +4,13 @@ import { RouteType } from '../../utils/types';
|
|||||||
import { AppContext } from '../../utils/types';
|
import { AppContext } from '../../utils/types';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { contextSessionId } from '../../utils/requestUtils';
|
import { contextSessionId } from '../../utils/requestUtils';
|
||||||
|
import { cookieSet } from '../../utils/cookies';
|
||||||
|
|
||||||
const router = new Router(RouteType.Web);
|
const router = new Router(RouteType.Web);
|
||||||
|
|
||||||
router.post('logout', async (_path: SubPath, ctx: AppContext) => {
|
router.post('logout', async (_path: SubPath, ctx: AppContext) => {
|
||||||
const sessionId = contextSessionId(ctx, false);
|
const sessionId = contextSessionId(ctx, false);
|
||||||
ctx.cookies.set('sessionId', '');
|
cookieSet(ctx, 'sessionId', '');
|
||||||
await ctx.joplin.models.session().logout(sessionId);
|
await ctx.joplin.models.session().logout(sessionId);
|
||||||
return redirect(ctx, `${config().baseUrl}/login`);
|
return redirect(ctx, `${config().baseUrl}/login`);
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,7 @@ import { NotificationKey } from '../../models/NotificationModel';
|
|||||||
import { AccountType } from '../../models/UserModel';
|
import { AccountType } from '../../models/UserModel';
|
||||||
import { getCanShareFolder, getMaxItemSize } from '../../models/utils/user';
|
import { getCanShareFolder, getMaxItemSize } from '../../models/utils/user';
|
||||||
import { MB } from '../../utils/bytes';
|
import { MB } from '../../utils/bytes';
|
||||||
|
import { cookieGet } from '../../utils/cookies';
|
||||||
import { execRequestC } from '../../utils/testing/apiUtils';
|
import { execRequestC } from '../../utils/testing/apiUtils';
|
||||||
import { beforeAllDb, afterAllTests, beforeEachDb, models } from '../../utils/testing/testUtils';
|
import { beforeAllDb, afterAllTests, beforeEachDb, models } from '../../utils/testing/testUtils';
|
||||||
import uuidgen from '../../utils/uuidgen';
|
import uuidgen from '../../utils/uuidgen';
|
||||||
@ -50,7 +51,7 @@ describe('index_signup', function() {
|
|||||||
expect(getMaxItemSize(user)).toBe(10 * MB);
|
expect(getMaxItemSize(user)).toBe(10 * MB);
|
||||||
|
|
||||||
// Check that the user is logged in
|
// Check that the user is logged in
|
||||||
const session = await models().session().load(context.cookies.get('sessionId'));
|
const session = await models().session().load(cookieGet(context, 'sessionId'));
|
||||||
expect(session.user_id).toBe(user.id);
|
expect(session.user_id).toBe(user.id);
|
||||||
|
|
||||||
// Check that the notification has been created
|
// Check that the notification has been created
|
||||||
|
@ -10,6 +10,7 @@ import { checkRepeatPassword } from './users';
|
|||||||
import { NotificationKey } from '../../models/NotificationModel';
|
import { NotificationKey } from '../../models/NotificationModel';
|
||||||
import { AccountType } from '../../models/UserModel';
|
import { AccountType } from '../../models/UserModel';
|
||||||
import { ErrorForbidden } from '../../utils/errors';
|
import { ErrorForbidden } from '../../utils/errors';
|
||||||
|
import { cookieSet } from '../../utils/cookies';
|
||||||
|
|
||||||
function makeView(error: Error = null): View {
|
function makeView(error: Error = null): View {
|
||||||
const view = defaultView('signup', 'Sign Up');
|
const view = defaultView('signup', 'Sign Up');
|
||||||
@ -51,7 +52,7 @@ router.post('signup', async (_path: SubPath, ctx: AppContext) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const session = await ctx.joplin.models.session().createUserSession(user.id);
|
const session = await ctx.joplin.models.session().createUserSession(user.id);
|
||||||
ctx.cookies.set('sessionId', session.id);
|
cookieSet(ctx, 'sessionId', session.id);
|
||||||
|
|
||||||
await ctx.joplin.models.notification().add(user.id, NotificationKey.ConfirmEmail);
|
await ctx.joplin.models.notification().add(user.id, NotificationKey.ConfirmEmail);
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { User } from '../../db';
|
import { User } from '../../db';
|
||||||
import routeHandler from '../../middleware/routeHandler';
|
import routeHandler from '../../middleware/routeHandler';
|
||||||
import { NotificationKey } from '../../models/NotificationModel';
|
import { NotificationKey } from '../../models/NotificationModel';
|
||||||
|
import { cookieGet } from '../../utils/cookies';
|
||||||
import { ErrorForbidden } from '../../utils/errors';
|
import { ErrorForbidden } from '../../utils/errors';
|
||||||
import { execRequest, execRequestC } from '../../utils/testing/apiUtils';
|
import { execRequest, execRequestC } from '../../utils/testing/apiUtils';
|
||||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, createUserAndSession, models, parseHtml, checkContextError, expectHttpError } from '../../utils/testing/testUtils';
|
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, createUserAndSession, models, parseHtml, checkContextError, expectHttpError } from '../../utils/testing/testUtils';
|
||||||
@ -240,7 +241,7 @@ describe('index/users', function() {
|
|||||||
password: newPassword,
|
password: newPassword,
|
||||||
password2: newPassword,
|
password2: newPassword,
|
||||||
});
|
});
|
||||||
const sessionId = context.cookies.get('sessionId');
|
const sessionId = cookieGet(context, 'sessionId');
|
||||||
expect(sessionId).toBeFalsy();
|
expect(sessionId).toBeFalsy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +254,7 @@ describe('index/users', function() {
|
|||||||
password2: newPassword,
|
password2: newPassword,
|
||||||
token: token2,
|
token: token2,
|
||||||
});
|
});
|
||||||
const sessionId = context.cookies.get('sessionId');
|
const sessionId = cookieGet(context, 'sessionId');
|
||||||
expect(sessionId).toBeFalsy();
|
expect(sessionId).toBeFalsy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,7 +267,7 @@ describe('index/users', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check that the user has been logged in
|
// Check that the user has been logged in
|
||||||
const sessionId = context.cookies.get('sessionId');
|
const sessionId = cookieGet(context, 'sessionId');
|
||||||
const session = await models().session().load(sessionId);
|
const session = await models().session().load(sessionId);
|
||||||
expect(session.user_id).toBe(user1.id);
|
expect(session.user_id).toBe(user1.id);
|
||||||
|
|
||||||
@ -303,7 +304,7 @@ describe('index/users', function() {
|
|||||||
user1 = await models().user().load(user1.id);
|
user1 = await models().user().load(user1.id);
|
||||||
|
|
||||||
// Check that the user has been logged in
|
// Check that the user has been logged in
|
||||||
const sessionId = context.cookies.get('sessionId');
|
const sessionId = cookieGet(context, 'sessionId');
|
||||||
expect(sessionId).toBeFalsy();
|
expect(sessionId).toBeFalsy();
|
||||||
|
|
||||||
// Check that the email has been verified
|
// Check that the email has been verified
|
||||||
|
@ -19,6 +19,7 @@ import { confirmUrl } from '../../utils/urlUtils';
|
|||||||
import { cancelSubscriptionByUserId, updateSubscriptionType } from '../../utils/stripe';
|
import { cancelSubscriptionByUserId, updateSubscriptionType } from '../../utils/stripe';
|
||||||
import { createCsrfTag } from '../../utils/csrf';
|
import { createCsrfTag } from '../../utils/csrf';
|
||||||
import { formatDateTime } from '../../utils/time';
|
import { formatDateTime } from '../../utils/time';
|
||||||
|
import { cookieSet } from '../../utils/cookies';
|
||||||
|
|
||||||
export interface CheckRepeatPasswordInput {
|
export interface CheckRepeatPasswordInput {
|
||||||
password: string;
|
password: string;
|
||||||
@ -236,7 +237,7 @@ router.post('users/:id/confirm', async (path: SubPath, ctx: AppContext) => {
|
|||||||
await ctx.joplin.models.token().deleteByValue(userId, fields.token);
|
await ctx.joplin.models.token().deleteByValue(userId, fields.token);
|
||||||
|
|
||||||
const session = await ctx.joplin.models.session().createUserSession(userId);
|
const session = await ctx.joplin.models.session().createUserSession(userId);
|
||||||
ctx.cookies.set('sessionId', session.id);
|
cookieSet(ctx, 'sessionId', session.id);
|
||||||
|
|
||||||
await ctx.joplin.models.notification().add(userId, NotificationKey.PasswordSet);
|
await ctx.joplin.models.notification().add(userId, NotificationKey.PasswordSet);
|
||||||
|
|
||||||
|
17
packages/server/src/utils/cookies.ts
Normal file
17
packages/server/src/utils/cookies.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import config from '../config';
|
||||||
|
import { AppContext } from './types';
|
||||||
|
|
||||||
|
export function cookieSet(ctx: AppContext, name: string, value: string) {
|
||||||
|
ctx.cookies.set(name, value, {
|
||||||
|
// Means that the cookies cannot be accessed from JavaScript
|
||||||
|
httpOnly: true,
|
||||||
|
// Can only be transferred over https
|
||||||
|
secure: config().cookieSecure,
|
||||||
|
// Prevent cookies from being sent in cross-site requests
|
||||||
|
sameSite: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cookieGet(ctx: AppContext, name: string) {
|
||||||
|
return ctx.cookies.get(name);
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { cookieGet } from './cookies';
|
||||||
import { ErrorForbidden } from './errors';
|
import { ErrorForbidden } from './errors';
|
||||||
import { AppContext } from './types';
|
import { AppContext } from './types';
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ export function headerSessionId(headers: any): string {
|
|||||||
export function contextSessionId(ctx: AppContext, throwIfNotFound = true): string {
|
export function contextSessionId(ctx: AppContext, throwIfNotFound = true): string {
|
||||||
if (ctx.headers['x-api-auth']) return ctx.headers['x-api-auth'];
|
if (ctx.headers['x-api-auth']) return ctx.headers['x-api-auth'];
|
||||||
|
|
||||||
const id = ctx.cookies.get('sessionId');
|
const id = cookieGet(ctx, 'sessionId');
|
||||||
if (!id && throwIfNotFound) throw new ErrorForbidden('Invalid or missing session');
|
if (!id && throwIfNotFound) throw new ErrorForbidden('Invalid or missing session');
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import { initializeJoplinUtils } from '../joplinUtils';
|
|||||||
import MustacheService from '../../services/MustacheService';
|
import MustacheService from '../../services/MustacheService';
|
||||||
import uuidgen from '../uuidgen';
|
import uuidgen from '../uuidgen';
|
||||||
import { createCsrfToken } from '../csrf';
|
import { createCsrfToken } from '../csrf';
|
||||||
|
import { cookieSet } from '../cookies';
|
||||||
|
|
||||||
// Takes into account the fact that this file will be inside the /dist directory
|
// Takes into account the fact that this file will be inside the /dist directory
|
||||||
// when it runs.
|
// when it runs.
|
||||||
@ -211,7 +212,7 @@ export async function koaAppContext(options: AppContextTestOptions = null): Prom
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (options.sessionId) {
|
if (options.sessionId) {
|
||||||
appContext.cookies.set('sessionId', options.sessionId);
|
cookieSet(appContext, 'sessionId', options.sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return appContext as AppContext;
|
return appContext as AppContext;
|
||||||
|
@ -108,6 +108,7 @@ export interface Config {
|
|||||||
supportName: string;
|
supportName: string;
|
||||||
businessEmail: string;
|
businessEmail: string;
|
||||||
isJoplinCloud: boolean;
|
isJoplinCloud: boolean;
|
||||||
|
cookieSecure: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum HttpMethod {
|
export enum HttpMethod {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user