From ca54398cf64a4907ccea948faa096f3bec818c3a Mon Sep 17 00:00:00 2001 From: bogdandina Date: Sat, 4 Jul 2026 11:59:30 +0300 Subject: [PATCH 1/3] fix(85766): fix CodeQL vulnerability --- package.json | 2 ++ src/admin/adminController.ts | 14 +++++++++++++- src/server.ts | 7 ++++++- src/views/Admin.js | 9 +++++---- src/views/components/AutoDomainGroups.js | 3 ++- src/views/components/GroupAssignment.js | 3 ++- src/views/components/UIMessage.js | 3 ++- yarn.lock | 19 +++++++++++++++++++ 8 files changed, 51 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 8fa9f7f4..3113c396 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "knex": "^0.95.6", "knex-postgis": "^0.13.0", "lodash": "^4.17.11", + "lusca": "^1.7.0", "moment": "^2.24.0", "moment-timezone": "^0.5.23", "node-fetch": "^2.3.0", @@ -89,6 +90,7 @@ "@types/ioredis": "^4.0.9", "@types/jest": "^24.0.11", "@types/lodash": "^4.14.122", + "@types/lusca": "^1.7.5", "@types/node": "^14.17.1", "concurrently": "^4.1.0", "husky": "^1.3.1", diff --git a/src/admin/adminController.ts b/src/admin/adminController.ts index 05e8c5eb..2e4e06c5 100644 --- a/src/admin/adminController.ts +++ b/src/admin/adminController.ts @@ -1,4 +1,5 @@ import express from 'express' +import lusca from 'lusca' import { DomainGroup, getSettings, @@ -19,9 +20,20 @@ export const adminController = async (adminPath) => { const prefixedAdminPath = join(PATH_PREFIX, adminPath) const adminRouter = express.Router() + // Protect the state-changing admin form submissions (rendered as HTML forms + // and posted as application/x-www-form-urlencoded) against CSRF. lusca.csrf + // stores a secret in the existing session and exposes the per-session token + // as `res.locals._csrf`, which must be echoed back via the hidden `_csrf` + // field embedded in each form. + adminRouter.use(lusca.csrf()) + adminRouter.get('/', async (req, res) => { const currentState = await getSettings() - res.render('Admin', { adminPath: prefixedAdminPath, settings: currentState }) + res.render('Admin', { + adminPath: prefixedAdminPath, + settings: currentState, + csrfToken: res.locals._csrf, + }) }) function createDomainGroupsFromInput(input: string): DomainGroup[] { diff --git a/src/server.ts b/src/server.ts index 40c389c4..7c96268e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -93,7 +93,12 @@ type RequestContext = { cookie: { secure: SECURE_COOKIE, maxAge: 30 * 24 * 60 * 60 * 1000, - httpOnly: false, + httpOnly: true, + // Do not send the session cookie on cross-site requests. Combined with + // the strict CORS origin, this blocks browser-driven CSRF against the + // JSON API endpoints (/login, GraphQL) that cannot carry a CSRF token + // without changes in the SPA clients. + sameSite: 'lax', }, }) ) diff --git a/src/views/Admin.js b/src/views/Admin.js index 5f562028..ef2aa596 100644 --- a/src/views/Admin.js +++ b/src/views/Admin.js @@ -4,14 +4,14 @@ import GroupAssignment from './components/GroupAssignment' import AutoDomainGroups from './components/AutoDomainGroups' import join from 'proper-url-join' -const AdminView = ({ adminPath, settings }) => { +const AdminView = ({ adminPath, settings, csrfToken }) => { return ( <>

Transitlog server admin


- - - + + +

Clear cache

@@ -19,6 +19,7 @@ const AdminView = ({ adminPath, settings }) => { will result in some queries taking longer due to being fetched from the DB.

+
diff --git a/src/views/components/AutoDomainGroups.js b/src/views/components/AutoDomainGroups.js index c24a526a..7e35795e 100644 --- a/src/views/components/AutoDomainGroups.js +++ b/src/views/components/AutoDomainGroups.js @@ -2,13 +2,14 @@ import React from 'react' import join from 'proper-url-join' import { get } from 'lodash' -const AutoDomainGroups = ({ adminPath, settings }) => { +const AutoDomainGroups = ({ adminPath, settings, csrfToken }) => { const assignedGroups = get(settings, 'auto_domain_groups', []) return ( <>

Create groups for emails

+
HSL ID group creation

diff --git a/src/views/components/GroupAssignment.js b/src/views/components/GroupAssignment.js index 331b733b..5869e4df 100644 --- a/src/views/components/GroupAssignment.js +++ b/src/views/components/GroupAssignment.js @@ -2,13 +2,14 @@ import React from 'react' import join from 'proper-url-join' import { get } from 'lodash' -const GroupAssignment = ({ adminPath, settings }) => { +const GroupAssignment = ({ adminPath, settings, csrfToken }) => { const assignedGroups = get(settings, 'domain_groups', []) return ( <>

Assign email domains to groups

+
HSL ID group assignment

diff --git a/src/views/components/UIMessage.js b/src/views/components/UIMessage.js index 26a4a6ce..069c1bff 100644 --- a/src/views/components/UIMessage.js +++ b/src/views/components/UIMessage.js @@ -2,13 +2,14 @@ import React from 'react' import join from 'proper-url-join' import { get } from 'lodash' -const UIMessageForm = ({ settings, adminPath }) => { +const UIMessageForm = ({ settings, adminPath, csrfToken }) => { const message = get(settings, 'ui_message') || { date: '', message: '' } return ( <>

Set UI message

+
UI message

Set a message that is shown in the UI.

diff --git a/yarn.lock b/yarn.lock index eb6cda36..48a5cc81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1909,6 +1909,13 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== +"@types/lusca@^1.7.5": + version "1.7.5" + resolved "https://registry.yarnpkg.com/@types/lusca/-/lusca-1.7.5.tgz#6fff257dc11176bd3150afba90790e626a12cd0f" + integrity sha512-l49gAf8pu2iMzbKejLcz6Pqj+51H2na6BgORv1ElnE8ByPFcBdh/eZ0WNR1Va/6ZuNSZa01Hoy1DTZ3IZ+y+kA== + dependencies: + "@types/express" "*" + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" @@ -6700,6 +6707,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lusca@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/lusca/-/lusca-1.7.0.tgz#a5d979f1b51776e60d41e0ca98f886f1b8b95502" + integrity sha512-msnrplCfY7zaqlZBDEloCIKld+RUeMZVeWzSPaGUKeRXFlruNSdKg2XxCyR+zj6BqzcXhXlRnvcvx6rAGgsvMA== + dependencies: + tsscmp "^1.0.5" + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -9495,6 +9509,11 @@ tslint@^6.1.3: tslib "^1.13.0" tsutils "^2.29.0" +tsscmp@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" + integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== + tsutils@^2.29.0: version "2.29.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" From c0cd8b5de82c9e38025770777d5be2f80451112e Mon Sep 17 00:00:00 2001 From: bogdandina Date: Mon, 6 Jul 2026 22:40:37 +0300 Subject: [PATCH 2/3] fix(85766): fixes --- codegen.yml | 3 +++ src/types/generated/resolver-types.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/codegen.yml b/codegen.yml index cb27a44a..bf4f875b 100644 --- a/codegen.yml +++ b/codegen.yml @@ -2,6 +2,9 @@ overwrite: true schema: 'src/schema/**/*.ts': noRequire: true +hooks: + afterAllFileWrite: + - prettier --write generates: src/types/generated/schema-types.ts: plugins: diff --git a/src/types/generated/resolver-types.ts b/src/types/generated/resolver-types.ts index 5cb74733..97ce5036 100644 --- a/src/types/generated/resolver-types.ts +++ b/src/types/generated/resolver-types.ts @@ -2,9 +2,7 @@ import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from ' export type Maybe = T | null export type Omit = Pick> export type RequireFields = { [X in Exclude]?: T[X] } & - { - [P in K]-?: NonNullable - } + { [P in K]-?: NonNullable } /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string @@ -1118,8 +1116,8 @@ export type ResolversTypes = { Mutation: ResolverTypeWrapper<{}> Feedback: ResolverTypeWrapper Upload: ResolverTypeWrapper - BBox: ResolverTypeWrapper TlpType: TlpType + BBox: ResolverTypeWrapper } /** Mapping between all available schema types and the resolvers parents */ @@ -1166,7 +1164,9 @@ export type ResolversParentTypes = { ObservedArrival: ObservedArrival ObservedDeparture: ObservedDeparture ExceptionDay: ExceptionDay - Journey: Omit & { events: ResolversParentTypes['JourneyEventType'][] } + Journey: Omit & { + events: ResolversParentTypes['JourneyEventType'][] + } VehiclePosition: VehiclePosition JourneyEventType: | ResolversParentTypes['JourneyEvent'] @@ -1194,8 +1194,8 @@ export type ResolversParentTypes = { Mutation: {} Feedback: Feedback Upload: Scalars['Upload'] - BBox: Scalars['BBox'] TlpType: TlpType + BBox: Scalars['BBox'] } export type AlertResolvers< From dabb0a61ff1f9fb11492a7e52b1ec5dd30a34a3b Mon Sep 17 00:00:00 2001 From: bogdandina Date: Mon, 6 Jul 2026 22:50:20 +0300 Subject: [PATCH 3/3] fix(85766): fixes --- src/types/generated/resolver-types.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/types/generated/resolver-types.ts b/src/types/generated/resolver-types.ts index 97ce5036..a71ab680 100644 --- a/src/types/generated/resolver-types.ts +++ b/src/types/generated/resolver-types.ts @@ -1,8 +1,9 @@ import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql' export type Maybe = T | null export type Omit = Pick> -export type RequireFields = { [X in Exclude]?: T[X] } & - { [P in K]-?: NonNullable } +export type RequireFields = { [X in Exclude]?: T[X] } & { + [P in K]-?: NonNullable +} /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string