feat: iframe configuration

This commit is contained in:
Philipinho
2026-05-18 22:02:31 +01:00
parent b7b99cb3b2
commit 0d6538ab1a
5 changed files with 66 additions and 0 deletions
+7
View File
@@ -48,6 +48,13 @@ GOTENBERG_URL=
DISABLE_TELEMETRY=false
# Allow other sites to embed Docmost in an iframe.
IFRAME_EMBED_ALLOWED=false
# Only used when IFRAME_EMBED_ALLOWED=true. When empty, any origin is allowed.
# Example: https://intranet.example.com,https://portal.example.com
IFRAME_ALLOWED_ORIGINS=
# Enable debug logging in production (default: false)
DEBUG_MODE=false
+1
View File
@@ -2,3 +2,4 @@ export * from './utils';
export * from './nanoid.utils';
export * from './file.helper';
export * from './constants';
export * from './security-headers';
@@ -0,0 +1,19 @@
export type SecurityHeader = { name: string; value: string };
export function resolveFrameHeader(
iframeEmbedAllowed: boolean,
allowedOrigins: string[],
): SecurityHeader | null {
if (!iframeEmbedAllowed) {
return { name: 'X-Frame-Options', value: 'SAMEORIGIN' };
}
if (allowedOrigins.length === 0) {
return null;
}
return {
name: 'Content-Security-Policy',
value: `frame-ancestors 'self' ${allowedOrigins.join(' ')}`,
};
}
@@ -325,4 +325,19 @@ export class EnvironmentService {
.toLowerCase();
return disabled === 'true';
}
isIframeEmbedAllowed(): boolean {
const allowed = this.configService
.get<string>('IFRAME_EMBED_ALLOWED', 'false')
.toLowerCase();
return allowed === 'true';
}
getIframeAllowedOrigins(): string[] {
const raw = this.configService.get<string>('IFRAME_ALLOWED_ORIGINS', '');
return raw
.split(',')
.map((o) => o.trim())
.filter(Boolean);
}
}
+24
View File
@@ -12,6 +12,8 @@ import fastifyMultipart from '@fastify/multipart';
import fastifyCookie from '@fastify/cookie';
import fastifyIp from 'fastify-ip';
import { InternalLogFilter } from './common/logger/internal-log-filter';
import { EnvironmentService } from './integrations/environment/environment.service';
import { resolveFrameHeader } from './common/helpers';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
@@ -50,6 +52,28 @@ async function bootstrap() {
await app.register(fastifyMultipart);
await app.register(fastifyCookie);
const environmentService = app.get(EnvironmentService);
const frameHeader = resolveFrameHeader(
environmentService.isIframeEmbedAllowed(),
environmentService.getIframeAllowedOrigins(),
);
if (frameHeader) {
// Skipped routes:
// /api/files/ - attachment controller sets its own CSP we'd overwrite
// /share/ 0 public share pages are safe to embed
const frameHeaderSkippedPrefixes = ['/api/files/', '/share/'];
app
.getHttpAdapter()
.getInstance()
.addHook('onSend', (req, reply, payload, done) => {
if (frameHeaderSkippedPrefixes.some((p) => req.url.startsWith(p))) {
return done(null, payload);
}
reply.header(frameHeader.name, frameHeader.value);
done(null, payload);
});
}
app
.getHttpAdapter()
.getInstance()