diff --git a/package.json b/package.json index d9b7debe..8aca4f6a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "ejs": "^3.1.9", "express": "^4.17.6", "express-session": "^1.17.3", + "he": "^1.2.0", "helmet": "^6.0.1", "http-errors": "^2.0.0", "http-proxy-middleware": "^2.0.6", @@ -68,6 +69,7 @@ "@types/express-oauth-server": "^2.0.4", "@types/express-serve-static-core": "^4.17.33", "@types/express-session": "^1.17.7", + "@types/he": "^1.2.0", "@types/http-errors": "^2.0.1", "@types/jest": "^29.2.6", "@types/jsonwebtoken": "^9.0.1", diff --git a/src/app.ts b/src/app.ts index 6cb198a8..dbd617aa 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,8 +8,16 @@ import { pool } from './shared/databases/postgres'; import pgSession from 'connect-pg-simple'; import { authProviderFactory } from './main/services/authProviderFactory'; import path from 'path'; +import helmet from 'helmet'; const app = express(); +app.use(helmet({ + hsts: { + maxAge: 31536000, // 1 year in seconds + includeSubDomains: true, + preload: true + } +})); const sessionSecret: any = process.env.SESSION_SECRET const PostgresqlStore = pgSession(session) const sessionStore: any = new PostgresqlStore({ @@ -44,13 +52,14 @@ const keycloakConfig = { bearerOnly: false }; -const authProvider = authProviderFactory(authenticationType,keycloakConfig, sessionStore); -app.use(authProvider.init()) -app.get('/console/logout', authProvider.authenticate(), async (req:any, res) => { +const authProvider = authProviderFactory(authenticationType, keycloakConfig, sessionStore); +app.use(authProvider.init()); +app.get('/console/logout', authProvider.authenticate(), async (req: any, res) => { await authProvider.logout(req, res); res.redirect('/console'); }); app.get('/console', authProvider.authenticate(), (req, res) => { + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); res.sendFile(path.join(__dirname, 'build', 'index.html')); }); diff --git a/src/index.ts b/src/index.ts index a72092b4..0fe8b1d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,14 @@ const invalidRouteHandler = sharedMiddlewares.get('invalidRoute'); app.set('port', port); app.set('logger', logger); app.disable('x-powered-by'); -// app.use(helmet()); +// ToDo: Move helmet and cors to app.ts and remove from here +app.use(helmet({ + hsts: { + maxAge: 31536000, // 1 year in seconds + includeSubDomains: true, + preload: true + } +})); app.use(cors()); app.use(express.static(path.join(__dirname, 'build'))); @@ -26,6 +33,7 @@ app.use(express.static(path.join(__dirname, 'build'))); mountRoutes(app); app.get('*', function (req, res) { + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); res.sendFile(path.join(__dirname, 'build', 'index.html')); }); diff --git a/src/main/controllers/auth_logout.ts b/src/main/controllers/auth_logout.ts index 2e1b1c34..35b681c3 100644 --- a/src/main/controllers/auth_logout.ts +++ b/src/main/controllers/auth_logout.ts @@ -6,6 +6,7 @@ export default { request.logout({ keepSessionInfo: false }, ((error: any) => { error && console.log("Error while logout", error) })) + response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); response.status(200).json({ "status": "successful" }) }, }; \ No newline at end of file diff --git a/src/main/controllers/auth_user_info.ts b/src/main/controllers/auth_user_info.ts index 8ef409f7..5b1afddc 100644 --- a/src/main/controllers/auth_user_info.ts +++ b/src/main/controllers/auth_user_info.ts @@ -11,6 +11,7 @@ export default { scope: authInfo?.scope, email: user?.email_address } + response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); response.json(data); }, }; \ No newline at end of file diff --git a/src/main/controllers/config.ts b/src/main/controllers/config.ts index 64197001..84537203 100644 --- a/src/main/controllers/config.ts +++ b/src/main/controllers/config.ts @@ -4,6 +4,7 @@ import appConfig from '../../shared/resources/appConfig'; export default { name: 'config:vars', handler: () => async (request: Request, response: Response, next: NextFunction) => { + response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); response.json({ "GRAFANA_URL": appConfig.GRAFANA.URL, "SUPERSET_URL": appConfig.SUPERSET.URL, diff --git a/src/main/controllers/dataset_aggregator.ts b/src/main/controllers/dataset_aggregator.ts index e2600c22..0f957c1a 100644 --- a/src/main/controllers/dataset_aggregator.ts +++ b/src/main/controllers/dataset_aggregator.ts @@ -221,9 +221,11 @@ const generateDatasetState = async (state: Record) => { export default { name: 'dataset:state', handler: () => async (request: Request, response: Response, next: NextFunction) => { - let { datasetId } = request.params; - let { status } = request.query; + let datasetId = _.get(request, 'params.datasetId'); + const status = _.get(request, 'query.status'); + datasetId = status === "Live" ? _.split(datasetId, '.', 1)[0] : datasetId + response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); try { const dataset = await fetchDataset({ datasetId, status }); const payload = { diff --git a/src/main/controllers/dataset_diff.ts b/src/main/controllers/dataset_diff.ts index 258c5a10..fcf7f0e2 100644 --- a/src/main/controllers/dataset_diff.ts +++ b/src/main/controllers/dataset_diff.ts @@ -7,6 +7,7 @@ export default { name: 'dataset:diff', handler: () => async (request: Request, response: Response, next: NextFunction) => { const { datasetId } = request.params; + response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); try { let liveDataset = await fetchDataset({ datasetId }); if (_.get(liveDataset, "api_version") !== "v2") { diff --git a/src/main/controllers/dataset_exists.ts b/src/main/controllers/dataset_exists.ts index 039979f8..6def8e82 100644 --- a/src/main/controllers/dataset_exists.ts +++ b/src/main/controllers/dataset_exists.ts @@ -1,28 +1,27 @@ -import { NextFunction, Request, Response } from "express"; -import { fetchDataset, fetchDraftDataset } from "../services/dataset"; -import _ from "lodash"; -import { getDiff } from "json-difference"; +import { NextFunction, Request, Response } from 'express'; +import _ from 'lodash'; +import { fetchDataset, fetchDraftDataset } from '../services/dataset'; export default { name: 'dataset:exists', handler: () => async (request: Request, response: Response, next: NextFunction) => { - const { datasetId } = request.params; + const datasetId = _.get(request.params, 'datasetId'); + response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); try { - const liveDataset = await Promise.allSettled([fetchDataset({ datasetId })]) - if(liveDataset[0].status == 'fulfilled') { + const liveDataset = await Promise.allSettled([fetchDataset({ datasetId })]) + if (liveDataset[0].status === 'fulfilled') { return response.status(200).json(liveDataset[0].value) } - - const draftDataset = await Promise.allSettled([fetchDraftDataset({ datasetId })]) - if(draftDataset[0].status == 'fulfilled') { + + const draftDataset = await Promise.allSettled([fetchDraftDataset({ datasetId })]) + if (draftDataset[0].status === 'fulfilled') { return response.status(200).json(draftDataset[0].value) } - if(draftDataset[0].status == 'rejected') { - return response.status(_.get(draftDataset[0], ['reason', 'status'])).json(_.get(draftDataset[0], ['reason', 'response', 'data'])) + if (draftDataset[0].status === 'rejected') { + const errorData = _.get(draftDataset[0], ['reason', 'response', 'data']); + return response.status(_.get(draftDataset[0], ['reason', 'status'])).json(errorData) } - - } catch (error) { next(error); } diff --git a/src/main/controllers/getAllFields.ts b/src/main/controllers/getAllFields.ts index a731e871..ce1e375c 100644 --- a/src/main/controllers/getAllFields.ts +++ b/src/main/controllers/getAllFields.ts @@ -9,6 +9,7 @@ export default { const { dataset_id } = request.params; const status: any = request.query.status; let flattenResult: any = []; + response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); try { const dataset = await fetchDatasetRecord(dataset_id, status); const data_schema = _.get(dataset, "data_schema", {}); diff --git a/src/main/controllers/metrics_scrap.ts b/src/main/controllers/metrics_scrap.ts index 7815f295..b191fd64 100644 --- a/src/main/controllers/metrics_scrap.ts +++ b/src/main/controllers/metrics_scrap.ts @@ -7,7 +7,8 @@ export default { handler: () => async (request: Request, response: Response, next: NextFunction) => { try { response.set('Content-Type', register.contentType); - const metrics = await register.metrics() + const metrics = await register.metrics(); + response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); response.status(200).send(metrics); } catch (error) { next(error); diff --git a/src/main/controllers/test_connection.ts b/src/main/controllers/test_connection.ts index 09de2f3e..9fbd0583 100644 --- a/src/main/controllers/test_connection.ts +++ b/src/main/controllers/test_connection.ts @@ -1,20 +1,30 @@ -import { NextFunction, Request, Response } from "express"; -import { Kafka } from "kafkajs"; -import _ from "lodash"; +import { NextFunction, Request, Response } from 'express'; +import { Kafka } from 'kafkajs'; +import * as _ from 'lodash'; export default { name: 'connector:test', handler: () => async (request: Request, response: Response, next: NextFunction) => { + response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); try { - const { kafkaBrokers, topic } = request.body; + const topic = _.get(request.body, 'topic', "").toString().trim(); + const kafkaBrokers = _.get(request.body, 'kafkaBrokers', "").toString().trim(); + + if (!kafkaBrokers || !topic) { + response.setHeader('Content-Type', 'application/json'); + return response.status(400).send({ error: "kafkaBrokers and topic are required" }); + } + const topicsList = await service.getTopics(kafkaBrokers); const topicExists = topicsList.includes(topic); - if (!topicExists) throw { message: "Topic does not exist" }; - const result = { connectionEstablished: true, topicExists: topicExists } + if (!topicExists) throw new Error("Topic does not exist"); + const result = { connectionEstablished: true, topicExists: topicExists }; + response.setHeader('Content-Type', 'application/json'); response.status(200).send(result); } catch (error: any) { console.log(error?.message); - next("Failed to establish connection to the client") + response.setHeader('Content-Type', 'application/json'); + response.status(500).send({ error: "Failed to establish connection to the client" }); } } }; diff --git a/src/main/controllers/user_create.ts b/src/main/controllers/user_create.ts index 966cdb4d..e892ef0a 100644 --- a/src/main/controllers/user_create.ts +++ b/src/main/controllers/user_create.ts @@ -11,9 +11,10 @@ export default { name: 'user:create', handler: () => async (req: Request, res: Response, next: NextFunction) => { try { + const apiId = _.get(req, ['body', 'id']); const userRequest = _.get(req, ['body', 'request']); const isOwner = _.get(req, ['session', 'userDetails', 'is_owner']); - + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); if (!isOwner && userRequest?.roles?.includes('admin')) { return res.status(403).json({ error: 'Only an owner can assign the admin role' @@ -28,10 +29,10 @@ export default { const keycloakToken = JSON.parse(req?.session['keycloak-token']); const access_token = keycloakToken.access_token; const result = await userCreateWithKeycloak(access_token, userRequest); - res.status(200).json(transform({ id: req.body.id, result: { id: result.id, user_name: result.user_name, email_address: result.email_address } })); + res.status(200).json(transform({ id: apiId, result: { id: result.id, user_name: result.user_name } })); } else if (authenticationType === 'basic') { const result = await userCreateAsBasic(userRequest); - res.status(200).json(transform({ id: req.body.id, result: { id: result.id, user_name: result.user_name, email_address: result.email_address } })); + res.status(200).json(transform({ id: apiId, result: { id: result.id, user_name: result.user_name } })); } } catch (error) { next(error); diff --git a/src/main/controllers/user_list.ts b/src/main/controllers/user_list.ts index d99e65f1..28de274d 100644 --- a/src/main/controllers/user_list.ts +++ b/src/main/controllers/user_list.ts @@ -7,6 +7,7 @@ export default { name: 'user:list', handler: () => async (request: Request, response: Response, next: NextFunction) => { try { + const apiId = _.get(request, ['body', 'id']); const user = _.get(request, ['body', 'request']); const result = await userService.findAll(user); @@ -16,7 +17,8 @@ export default { }); const responseData = { data: usersList, count: _.size(usersList) }; - response.status(200).json(transform({ id: request.body.id, result: responseData })); + response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); + response.status(200).json(transform({ id: apiId, result: responseData })); } catch (error) { if (error === 'user_not_found') { const err = new Error('User not found'); diff --git a/src/main/controllers/user_manage_roles.ts b/src/main/controllers/user_manage_roles.ts index 5357a1e0..18a94eaf 100644 --- a/src/main/controllers/user_manage_roles.ts +++ b/src/main/controllers/user_manage_roles.ts @@ -38,6 +38,7 @@ export default { updated_by: userId, }, ); + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); res.status(200).json(transform({ id: req.body.id, result: { id: result.id, user_name: result.user_name, roles: result.roles } })); } catch (error) { if (error === 'user_not_found') { @@ -47,6 +48,7 @@ export default { } else { const e = error as Error; if (e.message && (e.message.includes('Only the owner can modify the admin role'))) { + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); return res.status(403).json({ error: e.message }); } next(error); diff --git a/src/main/controllers/user_manage_status.ts b/src/main/controllers/user_manage_status.ts index a210da96..b35ca758 100644 --- a/src/main/controllers/user_manage_status.ts +++ b/src/main/controllers/user_manage_status.ts @@ -14,7 +14,7 @@ export default { const user = await userService.find({ user_name }); const hasAdminRole = user?.roles.includes('admin'); - + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); if (hasAdminRole && !isOwner) { return res.status(403).json({ error: 'Only the owner can change the status of an admin user' diff --git a/src/main/controllers/user_read.ts b/src/main/controllers/user_read.ts index 4a96169a..db103f4d 100644 --- a/src/main/controllers/user_read.ts +++ b/src/main/controllers/user_read.ts @@ -16,7 +16,8 @@ const getUserDetails = function (request: Request) { }; return userDetails; } else if (authenticationType === 'keycloak') { - const keycloakToken = JSON.parse(request?.session['keycloak-token']); + const sessionToken = _.get(request, ['session','keycloak-token']); + const keycloakToken = typeof sessionToken === 'string' ? JSON.parse(sessionToken) : sessionToken; const access_token = keycloakToken?.access_token; const preferred_username = request?.session?.preferred_username; const userDetails = { @@ -36,6 +37,7 @@ export default { const sessionUserName = sessionUserDetails?.sessionUserName; const user = await userService.find({ user_name: sessionUserName }); const { password, ...userInfo } = user; + const responseData = { id: 'api.user.read', result: userInfo, @@ -45,6 +47,7 @@ export default { if (includeToken) { responseData.result.token = sessionUserDetails?.token; } + response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); response.status(200).json(transform(responseData)); } catch (error) { next(error); diff --git a/src/main/controllers/user_update.ts b/src/main/controllers/user_update.ts index dca3efa9..26cb3fcb 100644 --- a/src/main/controllers/user_update.ts +++ b/src/main/controllers/user_update.ts @@ -11,8 +11,9 @@ export default { const { user_name, ...updateInfo } = _.get(req, ['body', 'request']); const sessionUserName = _.get(req, ['session', 'userDetails', 'user_name']); const userId = _.get(req, ['session', 'userDetails', 'id']); + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); if (user_name !== sessionUserName) { - res.status(403).json( + return res.status(403).json( transform({ responseCode: 'FORBIDDEN', params: { diff --git a/src/main/helpers/proxy.ts b/src/main/helpers/proxy.ts index fc0d7936..aa760a34 100644 --- a/src/main/helpers/proxy.ts +++ b/src/main/helpers/proxy.ts @@ -9,6 +9,7 @@ const authenticationType = appConfig.AUTHENTICATION_TYPE; export const onError = ({ entity }: any) => (err: any, req: Request, res: Response) => { incrementFailedApiCalls({ entity, endpoint: req.url }); + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); res.status(500).send('Something went wrong. Please try again later.'); } diff --git a/src/main/middlewares/authorization.ts b/src/main/middlewares/authorization.ts index 4dddce1e..5e434bf4 100644 --- a/src/main/middlewares/authorization.ts +++ b/src/main/middlewares/authorization.ts @@ -30,6 +30,7 @@ export default { if (hasAccess) { next(); } else { + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); res.status(403).json( transform({ params: { diff --git a/src/main/services/keycloakAuthProvider.ts b/src/main/services/keycloakAuthProvider.ts index a425f023..4240b608 100644 --- a/src/main/services/keycloakAuthProvider.ts +++ b/src/main/services/keycloakAuthProvider.ts @@ -38,6 +38,7 @@ export class KeycloakAuthProvider implements BaseAuthProvider { deauthenticated(req); } catch (error) { console.error('Logout error:', error); + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); res.status(500).send('Logout failed'); } } diff --git a/src/main/services/oauthUsers.ts b/src/main/services/oauthUsers.ts index 4962808c..b072d1c2 100644 --- a/src/main/services/oauthUsers.ts +++ b/src/main/services/oauthUsers.ts @@ -1,18 +1,3 @@ - -// const service = { -// async find(data: any): Promise { -// const users = await find(table, data) -// if (users.length > 0) { -// return Promise.resolve(users[0]) -// } -// return Promise.reject('user_not_found') -// }, -// async create(userInfo: User): Promise { -// const user = await insert(table, userInfo); -// return user; -// } -// } - import { getFind, getSave, getUpdate, getFindAll} from "./oauthHelper"; const table = "oauth_users"; diff --git a/src/shared/middlewares/globalErrorHandler.ts b/src/shared/middlewares/globalErrorHandler.ts index c2a8fd0a..54848a44 100644 --- a/src/shared/middlewares/globalErrorHandler.ts +++ b/src/shared/middlewares/globalErrorHandler.ts @@ -15,6 +15,7 @@ export default { } = error; const { id = 'api' } = request.responsePayload || {}; + response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); logger.error(error) if (request.url.includes("oauth/v1/login")) { return response.redirect(`${appConfig.BASE_URL}/login?err=Invalid Credentials`); diff --git a/src/shared/utils/transformResponse.ts b/src/shared/utils/transformResponse.ts index e947ce91..42be9747 100644 --- a/src/shared/utils/transformResponse.ts +++ b/src/shared/utils/transformResponse.ts @@ -1,10 +1,24 @@ import { IResponse } from '../types'; import { v4 as uuidv4 } from 'uuid'; +import * as he from 'he'; const transform = (payload: Partial) => { - const { id, ver = 'v1', ets = Date.now(), params = {}, responseCode = 'OK', result = {} } = payload; + let { id, ver = 'v1', ets = Date.now(), params = {}, responseCode = 'OK', result = {} } = payload; - const { resmsgid = `${uuidv4()}`, err = '', status = responseCode === 'OK' ? 'SUCCESSFUL' : 'FAILED', errmsg = '' } = params; + // Sanitize ID to prevent Reflected XSS + if (typeof id === 'string') { + id = he.encode(id); + } + + let { resmsgid = `${uuidv4()}`, err = '', status = responseCode === 'OK' ? 'SUCCESSFUL' : 'FAILED', errmsg = '' } = params; + + // Sanitize error messages to prevent Reflected XSS + if (typeof err === 'string') { + err = he.encode(err); + } + if (typeof errmsg === 'string') { + errmsg = he.encode(errmsg); + } return { id,