fix(frontend config): Remove origins from frontend configuration

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-10-02 22:07:32 +02:00 committed by Yannick Bungers
parent 9b2cc5ceba
commit 35032eef09
8 changed files with 120 additions and 163 deletions

View file

@ -23,7 +23,7 @@ We also provide an `.env.example` file containing a minimal configuration in the
|--------------------------|-----------|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| `HD_DOMAIN` | - | `https://md.example.com` | The URL the HedgeDoc instance runs on. |
| `PORT` | 3000 | | The port the HedgeDoc instance runs on. |
| `HD_RENDERER_ORIGIN` | HD_DOMAIN | | The URL the renderer runs on. If omitted this will be same as `HD_DOMAIN`. |
| `HD_RENDERER_BASE_URL` | HD_DOMAIN | | The URL the renderer runs on. If omitted this will be same as `HD_DOMAIN`. |
| `HD_LOGLEVEL` | warn | | The loglevel that should be used. Options are `error`, `warn`, `info`, `debug` or `trace`. |
| `HD_FORBIDDEN_NOTE_IDS` | - | `notAllowed,alsoNotAllowed` | A list of note ids (separated by `,`), that are not allowed to be created or requested by anyone. |
| `HD_MAX_DOCUMENT_LENGTH` | 100000 | | The maximum length of any one document. Changes to this will impact performance for your users. |

View file

@ -52,9 +52,12 @@ export async function setupApp(
);
app.enableCors({
origin: appConfig.rendererOrigin,
origin: appConfig.rendererBaseUrl,
});
logger.log(`Enabling CORS for '${appConfig.rendererOrigin}'`, 'AppBootstrap');
logger.log(
`Enabling CORS for '${appConfig.rendererBaseUrl}'`,
'AppBootstrap',
);
app.useGlobalPipes(setupValidationPipe(logger));

View file

@ -11,7 +11,7 @@ import { Loglevel } from './loglevel.enum';
describe('appConfig', () => {
const domain = 'https://example.com';
const invalidDomain = 'localhost';
const rendererOrigin = 'https://render.example.com';
const rendererBaseUrl = 'https://render.example.com';
const port = 3333;
const negativePort = -9000;
const floatPort = 3.14;
@ -27,7 +27,7 @@ describe('appConfig', () => {
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
HD_RENDERER_ORIGIN: rendererOrigin,
HD_RENDERER_BASE_URL: rendererBaseUrl,
PORT: port.toString(),
HD_LOGLEVEL: loglevel,
HD_PERSIST_INTERVAL: '100',
@ -39,7 +39,7 @@ describe('appConfig', () => {
);
const config = appConfig();
expect(config.domain).toEqual(domain);
expect(config.rendererOrigin).toEqual(rendererOrigin);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(loglevel);
expect(config.persistInterval).toEqual(100);
@ -62,7 +62,7 @@ describe('appConfig', () => {
);
const config = appConfig();
expect(config.domain).toEqual(domain);
expect(config.rendererOrigin).toEqual(domain);
expect(config.rendererBaseUrl).toEqual(domain);
expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(loglevel);
expect(config.persistInterval).toEqual(100);
@ -74,7 +74,7 @@ describe('appConfig', () => {
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
HD_RENDERER_ORIGIN: rendererOrigin,
HD_RENDERER_BASE_URL: rendererBaseUrl,
HD_LOGLEVEL: loglevel,
HD_PERSIST_INTERVAL: '100',
/* eslint-enable @typescript-eslint/naming-convention */
@ -85,7 +85,7 @@ describe('appConfig', () => {
);
const config = appConfig();
expect(config.domain).toEqual(domain);
expect(config.rendererOrigin).toEqual(rendererOrigin);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(3000);
expect(config.loglevel).toEqual(loglevel);
expect(config.persistInterval).toEqual(100);
@ -97,7 +97,7 @@ describe('appConfig', () => {
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
HD_RENDERER_ORIGIN: rendererOrigin,
HD_RENDERER_BASE_URL: rendererBaseUrl,
PORT: port.toString(),
HD_PERSIST_INTERVAL: '100',
/* eslint-enable @typescript-eslint/naming-convention */
@ -108,7 +108,7 @@ describe('appConfig', () => {
);
const config = appConfig();
expect(config.domain).toEqual(domain);
expect(config.rendererOrigin).toEqual(rendererOrigin);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(Loglevel.WARN);
expect(config.persistInterval).toEqual(100);
@ -120,7 +120,7 @@ describe('appConfig', () => {
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
HD_RENDERER_ORIGIN: rendererOrigin,
HD_RENDERER_BASE_URL: rendererBaseUrl,
HD_LOGLEVEL: loglevel,
PORT: port.toString(),
/* eslint-enable @typescript-eslint/naming-convention */
@ -131,7 +131,7 @@ describe('appConfig', () => {
);
const config = appConfig();
expect(config.domain).toEqual(domain);
expect(config.rendererOrigin).toEqual(rendererOrigin);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(Loglevel.TRACE);
expect(config.persistInterval).toEqual(10);
@ -143,7 +143,7 @@ describe('appConfig', () => {
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
HD_RENDERER_ORIGIN: rendererOrigin,
HD_RENDERER_BASE_URL: rendererBaseUrl,
HD_LOGLEVEL: loglevel,
PORT: port.toString(),
HD_PERSIST_INTERVAL: '0',
@ -155,7 +155,7 @@ describe('appConfig', () => {
);
const config = appConfig();
expect(config.domain).toEqual(domain);
expect(config.rendererOrigin).toEqual(rendererOrigin);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(Loglevel.TRACE);
expect(config.persistInterval).toEqual(0);

View file

@ -11,7 +11,7 @@ import { buildErrorMessage, parseOptionalNumber } from './utils';
export interface AppConfig {
domain: string;
rendererOrigin: string;
rendererBaseUrl: string;
port: number;
loglevel: Loglevel;
persistInterval: number;
@ -23,13 +23,13 @@ const schema = Joi.object({
scheme: /https?/,
})
.label('HD_DOMAIN'),
rendererOrigin: Joi.string()
rendererBaseUrl: Joi.string()
.uri({
scheme: /https?/,
})
.default(Joi.ref('domain'))
.optional()
.label('HD_RENDERER_ORIGIN'),
.label('HD_RENDERER_BASE_URL'),
port: Joi.number()
.positive()
.integer()
@ -54,7 +54,7 @@ export default registerAs('appConfig', () => {
const appConfig = schema.validate(
{
domain: process.env.HD_DOMAIN,
rendererOrigin: process.env.HD_RENDERER_ORIGIN,
rendererBaseUrl: process.env.HD_RENDERER_BASE_URL,
port: parseOptionalNumber(process.env.PORT),
loglevel: process.env.HD_LOGLEVEL,
persistInterval: process.env.HD_PERSIST_INTERVAL,

View file

@ -12,7 +12,7 @@ export default registerAs(
'appConfig',
(): AppConfig => ({
domain: 'md.example.com',
rendererOrigin: 'md-renderer.example.com',
rendererBaseUrl: 'md-renderer.example.com',
port: 3000,
loglevel: Loglevel.ERROR,
persistInterval: 10,

View file

@ -123,22 +123,6 @@ export class SpecialUrlsDto extends BaseDto {
imprint?: URL;
}
export class IframeCommunicationDto extends BaseDto {
/**
* The origin under which the editor page will be served
* @example https://md.example.com
*/
@IsUrl()
editorOrigin: URL;
/**
* The origin under which the renderer page will be served
* @example https://md-renderer.example.com
*/
@IsUrl()
rendererOrigin: URL;
}
export class FrontendConfigDto extends BaseDto {
/**
* Maximum access level for guest users
@ -195,12 +179,4 @@ export class FrontendConfigDto extends BaseDto {
*/
@IsNumber()
maxDocumentLength: number;
/**
* The frontend capsules the markdown rendering into a secured iframe, to increase the security. The browser will treat the iframe target as cross-origin even if they are on the same domain.
* You can go even one step further and serve the editor and the renderer on different (sub)domains to eliminate even more attack vectors by making sessions, cookies, etc. not available for the renderer, because they aren't set on the renderer origin.
* However, The editor and the renderer need to know the other's origin to communicate with each other, even if they are the same.
*/
@ValidateNested()
iframeCommunication: IframeCommunicationDto;
}

View file

@ -168,7 +168,7 @@ describe('FrontendConfigService', () => {
it(`works with ${JSON.stringify(authConfigConfigured)}`, async () => {
const appConfig: AppConfig = {
domain: domain,
rendererOrigin: domain,
rendererBaseUrl: 'https://renderer.example.org',
port: 3000,
loglevel: Loglevel.ERROR,
persistInterval: 10,
@ -315,119 +315,106 @@ describe('FrontendConfigService', () => {
const customName = 'Test Branding Name';
let index = 1;
for (const renderOrigin of [undefined, 'http://md-renderer.example.com']) {
for (const customLogo of [undefined, 'https://example.com/logo.png']) {
for (const privacyLink of [undefined, 'https://example.com/privacy']) {
for (const termsOfUseLink of [undefined, 'https://example.com/terms']) {
for (const imprintLink of [
for (const customLogo of [undefined, 'https://example.com/logo.png']) {
for (const privacyLink of [undefined, 'https://example.com/privacy']) {
for (const termsOfUseLink of [undefined, 'https://example.com/terms']) {
for (const imprintLink of [undefined, 'https://example.com/imprint']) {
for (const plantUmlServer of [
undefined,
'https://example.com/imprint',
'https://plantuml.example.com',
]) {
for (const plantUmlServer of [
undefined,
'https://plantuml.example.com',
]) {
it(`combination #${index} works`, async () => {
const appConfig: AppConfig = {
domain: domain,
rendererOrigin: renderOrigin ?? domain,
port: 3000,
loglevel: Loglevel.ERROR,
persistInterval: 10,
};
const authConfig: AuthConfig = {
...emptyAuthConfig,
local: {
enableLogin: true,
enableRegister,
minimalPasswordStrength: 3,
it(`combination #${index} works`, async () => {
const appConfig: AppConfig = {
domain: domain,
rendererBaseUrl: 'https://renderer.example.org',
port: 3000,
loglevel: Loglevel.ERROR,
persistInterval: 10,
};
const authConfig: AuthConfig = {
...emptyAuthConfig,
local: {
enableLogin: true,
enableRegister,
minimalPasswordStrength: 3,
},
};
const customizationConfig: CustomizationConfig = {
branding: {
customName: customName,
customLogo: customLogo,
},
specialUrls: {
privacy: privacyLink,
termsOfUse: termsOfUseLink,
imprint: imprintLink,
},
};
const externalServicesConfig: ExternalServicesConfig = {
plantUmlServer: plantUmlServer,
imageProxy: imageProxy,
};
const noteConfig: NoteConfig = {
forbiddenNoteIds: [],
maxDocumentLength: maxDocumentLength,
guestAccess: GuestAccess.CREATE,
permissions: {
default: {
everyone: DefaultAccessPermission.READ,
loggedIn: DefaultAccessPermission.WRITE,
},
};
const customizationConfig: CustomizationConfig = {
branding: {
customName: customName,
customLogo: customLogo,
},
specialUrls: {
privacy: privacyLink,
termsOfUse: termsOfUseLink,
imprint: imprintLink,
},
};
const externalServicesConfig: ExternalServicesConfig = {
plantUmlServer: plantUmlServer,
imageProxy: imageProxy,
};
const noteConfig: NoteConfig = {
forbiddenNoteIds: [],
maxDocumentLength: maxDocumentLength,
guestAccess: GuestAccess.CREATE,
permissions: {
default: {
everyone: DefaultAccessPermission.READ,
loggedIn: DefaultAccessPermission.WRITE,
},
},
};
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [
registerAs('appConfig', () => appConfig),
registerAs('authConfig', () => authConfig),
registerAs(
'customizationConfig',
() => customizationConfig,
),
registerAs(
'externalServicesConfig',
() => externalServicesConfig,
),
registerAs('noteConfig', () => noteConfig),
],
}),
LoggerModule,
],
providers: [FrontendConfigService],
}).compile();
},
};
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [
registerAs('appConfig', () => appConfig),
registerAs('authConfig', () => authConfig),
registerAs(
'customizationConfig',
() => customizationConfig,
),
registerAs(
'externalServicesConfig',
() => externalServicesConfig,
),
registerAs('noteConfig', () => noteConfig),
],
}),
LoggerModule,
],
providers: [FrontendConfigService],
}).compile();
const service = module.get(FrontendConfigService);
const config = await service.getFrontendConfig();
expect(config.allowRegister).toEqual(enableRegister);
expect(config.guestAccess).toEqual(noteConfig.guestAccess);
expect(config.branding.name).toEqual(customName);
expect(config.branding.logo).toEqual(
customLogo ? new URL(customLogo) : undefined,
);
expect(config.iframeCommunication.editorOrigin).toEqual(
new URL(appConfig.domain),
);
expect(config.iframeCommunication.rendererOrigin).toEqual(
appConfig.rendererOrigin
? new URL(appConfig.rendererOrigin)
: new URL(appConfig.domain),
);
expect(config.maxDocumentLength).toEqual(maxDocumentLength);
expect(config.plantUmlServer).toEqual(
plantUmlServer ? new URL(plantUmlServer) : undefined,
);
expect(config.specialUrls.imprint).toEqual(
imprintLink ? new URL(imprintLink) : undefined,
);
expect(config.specialUrls.privacy).toEqual(
privacyLink ? new URL(privacyLink) : undefined,
);
expect(config.specialUrls.termsOfUse).toEqual(
termsOfUseLink ? new URL(termsOfUseLink) : undefined,
);
expect(config.useImageProxy).toEqual(!!imageProxy);
expect(config.version).toEqual(
await getServerVersionFromPackageJson(),
);
});
index += 1;
}
const service = module.get(FrontendConfigService);
const config = await service.getFrontendConfig();
expect(config.allowRegister).toEqual(enableRegister);
expect(config.guestAccess).toEqual(noteConfig.guestAccess);
expect(config.branding.name).toEqual(customName);
expect(config.branding.logo).toEqual(
customLogo ? new URL(customLogo) : undefined,
);
expect(config.maxDocumentLength).toEqual(maxDocumentLength);
expect(config.plantUmlServer).toEqual(
plantUmlServer ? new URL(plantUmlServer) : undefined,
);
expect(config.specialUrls.imprint).toEqual(
imprintLink ? new URL(imprintLink) : undefined,
);
expect(config.specialUrls.privacy).toEqual(
privacyLink ? new URL(privacyLink) : undefined,
);
expect(config.specialUrls.termsOfUse).toEqual(
termsOfUseLink ? new URL(termsOfUseLink) : undefined,
);
expect(config.useImageProxy).toEqual(!!imageProxy);
expect(config.version).toEqual(
await getServerVersionFromPackageJson(),
);
});
index += 1;
}
}
}

View file

@ -22,7 +22,6 @@ import {
AuthProviderType,
BrandingDto,
FrontendConfigDto,
IframeCommunicationDto,
SpecialUrlsDto,
} from './frontend-config.dto';
@ -50,7 +49,6 @@ export class FrontendConfigService {
allowRegister: this.authConfig.local.enableRegister,
authProviders: this.getAuthProviders(),
branding: this.getBranding(),
iframeCommunication: this.getIframeCommunication(),
maxDocumentLength: this.noteConfig.maxDocumentLength,
plantUmlServer: this.externalServicesConfig.plantUmlServer
? new URL(this.externalServicesConfig.plantUmlServer)
@ -146,11 +144,4 @@ export class FrontendConfigService {
: undefined,
};
}
private getIframeCommunication(): IframeCommunicationDto {
return {
editorOrigin: new URL(this.appConfig.domain),
rendererOrigin: new URL(this.appConfig.rendererOrigin),
};
}
}