improvements

This commit is contained in:
gsmithun4 2024-07-16 17:42:59 +05:30 committed by Muhsin Shah
parent 994686a1f1
commit e86697dbf1
7 changed files with 128 additions and 54 deletions

View file

@ -81,3 +81,12 @@ ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=
#session expiry in minutes
USER_SESSION_EXPIRY=
#Disable app embed feature, if true then private and public app embed is not allowed
DISABLE_APP_EMBED=
# if true then private app embed is not allowed
ENABLE_PRIVATE_APP_EMBED=
#Enable cors else restricted to TOOLJET_HOST
ENABLE_CORS=

View file

@ -47,6 +47,9 @@ import { ImportExportResourcesModule } from './modules/import_export_resources/i
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { GetConnection } from './helpers/getconnection';
import { APP_INTERCEPTOR } from '@nestjs/core/constants';
import { HelmetInterceptor } from './interceptors/helmet.interceptor';
import { CustomHeadersInterceptor } from './interceptors/custom-headers.interceptors';
const imports = [
ScheduleModule.forRoot(),
@ -172,7 +175,19 @@ if (process.env.ENABLE_TOOLJET_DB === 'true') {
@Module({
imports,
controllers: [AppController],
providers: [EmailService, SeedsService, GetConnection],
providers: [
EmailService,
SeedsService,
GetConnection,
{
provide: APP_INTERCEPTOR,
useClass: HelmetInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: CustomHeadersInterceptor,
},
],
})
export class AppModule implements OnModuleInit {
constructor() {}

View file

@ -303,3 +303,7 @@ export const isValidDomain = (email: string, restrictedDomain: string): boolean
}
return true;
};
export const isHttpsEnabled = () => {
return !!process.env.TOOLJET_HOST?.startsWith('https');
};

View file

@ -0,0 +1,17 @@
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class CustomHeadersInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
const response = context.switchToHttp().getResponse();
response.setHeader('Permissions-Policy', 'geolocation=(self), camera=(), microphone=()');
response.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
return data;
})
);
}
}

View file

@ -0,0 +1,74 @@
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import * as helmet from 'helmet';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class HelmetInterceptor implements NestInterceptor {
private readonly helmet: ReturnType<typeof helmet>;
constructor(private configService: ConfigService) {
const host = new URL(this.configService.get('TOOLJET_HOST'));
const domain = host.hostname;
this.helmet = helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
upgradeInsecureRequests: null,
'img-src': ['*', 'data:', 'blob:'],
'script-src': [
'maps.googleapis.com',
'storage.googleapis.com',
'apis.google.com',
'accounts.google.com',
"'self'",
"'unsafe-inline'",
"'unsafe-eval'",
'blob:',
'https://unpkg.com/@babel/standalone@7.17.9/babel.min.js',
'https://unpkg.com/react@16.7.0/umd/react.production.min.js',
'https://unpkg.com/react-dom@16.7.0/umd/react-dom.production.min.js',
'cdn.skypack.dev',
'cdn.jsdelivr.net',
'https://esm.sh',
'www.googletagmanager.com',
],
'default-src': [
'maps.googleapis.com',
'storage.googleapis.com',
'apis.google.com',
'accounts.google.com',
'*.sentry.io',
"'self'",
'blob:',
'www.googletagmanager.com',
],
'connect-src': ['ws://' + domain, "'self'", '*'],
'frame-ancestors': ['*'],
'frame-src': ['*'],
},
},
frameguard:
this.configService.get('DISABLE_APP_EMBED') !== 'true' ||
this.configService.get('ENABLE_PRIVATE_APP_EMBED') === 'true'
? false
: { action: 'deny' },
hidePoweredBy: true,
referrerPolicy: {
policy: 'no-referrer',
},
});
}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const httpContext = context.switchToHttp();
const request = httpContext.getRequest();
const response = httpContext.getResponse();
return new Observable((subscriber) => {
this.helmet(request, response, () => {
next.handle().subscribe(subscriber);
});
});
}
}

View file

@ -4,7 +4,6 @@ import { WsAdapter } from '@nestjs/platform-ws';
import * as cookieParser from 'cookie-parser';
import * as compression from 'compression';
import { AppModule } from './app.module';
import * as helmet from 'helmet';
import { Logger } from 'nestjs-pino';
import { urlencoded, json } from 'express';
import { AllExceptionsFilter } from './filters/all-exceptions-filter';
@ -46,8 +45,6 @@ async function bootstrap() {
abortOnError: false,
});
const configService = app.get<ConfigService>(ConfigService);
const host = new URL(process.env.TOOLJET_HOST);
const domain = host.hostname;
app.useLogger(app.get(Logger));
app.useGlobalFilters(new AllExceptionsFilter(app.get(Logger)));
@ -68,57 +65,10 @@ async function bootstrap() {
exclude: pathsToExclude,
});
app.enableCors({
origin: process.env.TOOLJET_HOST,
origin: process.env.ENABLE_CORS === 'true' || process.env.TOOLJET_HOST,
credentials: true,
});
app.use(compression());
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
upgradeInsecureRequests: null,
'img-src': ['*', 'data:', 'blob:'],
'script-src': [
'maps.googleapis.com',
'storage.googleapis.com',
'apis.google.com',
'accounts.google.com',
"'self'",
"'unsafe-inline'",
"'unsafe-eval'",
'blob:',
'https://unpkg.com/@babel/standalone@7.17.9/babel.min.js',
'https://unpkg.com/react@16.7.0/umd/react.production.min.js',
'https://unpkg.com/react-dom@16.7.0/umd/react-dom.production.min.js',
'cdn.skypack.dev',
'cdn.jsdelivr.net',
'https://esm.sh',
'www.googletagmanager.com',
],
'default-src': [
'maps.googleapis.com',
'storage.googleapis.com',
'apis.google.com',
'accounts.google.com',
'*.sentry.io',
"'self'",
'blob:',
'www.googletagmanager.com',
],
'connect-src': ['ws://' + domain, "'self'", '*'],
'frame-ancestors': ['*'],
'frame-src': ['*'],
},
},
frameguard: process.env.ENABLE_APP_EMBED || process.env.ENABLE_PRIVATE_APP_EMBED ? false : { action: 'deny' },
hidePoweredBy: true,
})
);
// app.use(
// helmet.crossOriginEmbedderPolicy({}))
app.use(cookieParser());
app.use(json({ limit: '50mb' }));
app.use(urlencoded({ extended: true, limit: '50mb', parameterLimit: 1000000 }));
@ -142,7 +92,8 @@ async function bootstrap() {
await app.listen(port, listen_addr, function () {
const tooljetHost = configService.get<string>('TOOLJET_HOST');
console.log(`Ready to use at ${tooljetHost} 🚀`);
const subPath = configService.get<string>('SUB_PATH');
console.log(`Ready to use at ${tooljetHost}${subPath || ''} 🚀`);
});
}

View file

@ -28,6 +28,7 @@ import {
generateNextNameAndSlug,
generateOrgInviteURL,
isValidDomain,
isHttpsEnabled,
} from 'src/helpers/utils.helper';
import {
getUserErrorMessages,
@ -702,7 +703,8 @@ export class AuthService {
async forgotPassword(email: string) {
const user = await this.usersService.findByEmail(email);
if (!user) {
throw new BadRequestException('Email address not found');
// No need to throw error - To prevent Username Enumeration vulnerability
return;
}
const forgotPasswordToken = uuid.v4();
await this.usersService.updateUser(user.id, { forgotPasswordToken });
@ -1078,6 +1080,7 @@ export class AuthService {
if (organization) user.organizationId = organization.id;
const cookieOptions: CookieOptions = {
secure: isHttpsEnabled(),
httpOnly: true,
sameSite: 'strict',
maxAge: 2 * 365 * 24 * 60 * 60 * 1000, // maximum expiry 2 years
@ -1211,6 +1214,7 @@ export class AuthService {
const cookieOptions: CookieOptions = {
httpOnly: true,
secure: isHttpsEnabled(),
sameSite: 'strict',
maxAge: 2 * 365 * 24 * 60 * 60 * 1000, // maximum expiry 2 years
};