From 326b4f5915df69b3cb0e791b59f03958aaa35402 Mon Sep 17 00:00:00 2001 From: Amy Pillow Date: Sun, 15 Feb 2026 19:13:17 +0100 Subject: [PATCH 1/2] Add e2e test to test if sensitive config property is available --- cypress/e2e/limit-public-config.cy.ts | 16 ++++++++++++++++ src/config/config.server.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 cypress/e2e/limit-public-config.cy.ts diff --git a/cypress/e2e/limit-public-config.cy.ts b/cypress/e2e/limit-public-config.cy.ts new file mode 100644 index 00000000000..ac23e65ade4 --- /dev/null +++ b/cypress/e2e/limit-public-config.cy.ts @@ -0,0 +1,16 @@ +describe('Limit public config properties', () => { + it('Should not include cache.serverSide properties in the html source code', + () => { + cy.request('/').its('body').then((text: string) => { + expect(text).to.not.contain('"serverSide":'); + }); + }, + ); + it('Should not include cache.serverSide properties in the config.json', + () => { + cy.request('/assets/config.json').its('body').then((obj: any) => { + expect(obj.cache).to.not.haveOwnProperty('serverSide'); + }); + }, + ); +}); diff --git a/src/config/config.server.ts b/src/config/config.server.ts index b0a71e96985..c2924de0398 100644 --- a/src/config/config.server.ts +++ b/src/config/config.server.ts @@ -121,7 +121,7 @@ const overrideWithConfig = (config: Config, pathToConfig: string) => { try { console.log(`Overriding app config with ${pathToConfig}`); const externalConfig = readFileSync(pathToConfig, 'utf8'); - mergeConfig(config, load(externalConfig)); + mergeConfig(config, load(externalConfig) as AppConfig); } catch (err) { console.error(err); } From 955b524339047b650c724225e7adc0c3b011d2ef Mon Sep 17 00:00:00 2001 From: Amy Pillow Date: Sun, 15 Feb 2026 00:00:50 +0100 Subject: [PATCH 2/2] Remove SSR related configuration from client app --- server.ts | 3 ++- src/config/app-config.interface.ts | 30 ++++++++++++++++++++++++++ src/config/config.server.ts | 8 +++++-- src/modules/app/server-init.service.ts | 19 ++++++---------- 4 files changed, 44 insertions(+), 16 deletions(-) diff --git a/server.ts b/server.ts index 24e027e7d37..a17e309c2fe 100644 --- a/server.ts +++ b/server.ts @@ -45,6 +45,7 @@ import { buildAppConfig } from './src/config/config.server'; import { APP_CONFIG, AppConfig, + toClientConfig, } from './src/config/app-config.interface'; import { extendEnvironmentWithAppConfig } from './src/config/config.util'; import { logStartupMessage } from './startup-message'; @@ -265,7 +266,7 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) { }, { provide: APP_CONFIG, - useValue: environment, + useValue: toClientConfig(environment as AppConfig), }, ], }) diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 681e494c6bd..83f1d054216 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -79,8 +79,38 @@ const APP_CONFIG = new InjectionToken('APP_CONFIG'); const APP_CONFIG_STATE = makeStateKey('APP_CONFIG_STATE'); +type DeepPartial = T extends object ? { [k in keyof T]?: DeepPartial} : T; + +/** + * Removes all server-side specific settings from the application configuration. + * This method is used to ensure the "assets/config.json" that provides runtime + * configuration to CSR (client side rendering) excludes these server-side keys. + * + * @param config the application configuration + */ +const toClientConfig = ({ + rest: { + ssrBaseUrl: _ssrBaseUrl, + hasSsrBaseUrl: _hasSsrBaseUrl, + ...rest + }, + cache: { + serverSide: _serverSide, + ...cache + }, + ui: { + rateLimiter: _rateLimiter, + useProxies: _useProxies, + ...ui + }, + ...config +}: AppConfig): DeepPartial => ({ + ...config, rest, cache, ui, +}); + export { APP_CONFIG, APP_CONFIG_STATE, AppConfig, + toClientConfig, }; diff --git a/src/config/config.server.ts b/src/config/config.server.ts index c2924de0398..3bf813da374 100644 --- a/src/config/config.server.ts +++ b/src/config/config.server.ts @@ -14,7 +14,10 @@ import { } from 'colors'; import { load } from 'js-yaml'; -import { AppConfig } from './app-config.interface'; +import { + AppConfig, + toClientConfig, +} from './app-config.interface'; import { Config } from './config.interface'; import { mergeConfig } from './config.util'; import { DefaultAppConfig } from './default-app-config'; @@ -247,7 +250,8 @@ export const buildAppConfig = (destConfigPath?: string): AppConfig => { buildBaseUrl(appConfig.rest); if (isNotEmpty(destConfigPath)) { - writeFileSync(destConfigPath, JSON.stringify(appConfig, null, 2)); + const clientConfig = toClientConfig(appConfig); + writeFileSync(destConfigPath, JSON.stringify(clientConfig, null, 2)); console.log(`Angular ${bold('config.json')} file generated correctly at ${bold(destConfigPath)} \n`); } diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index 71012c40cdf..f203389f0f2 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -14,15 +14,13 @@ import { APP_CONFIG, APP_CONFIG_STATE, AppConfig, + toClientConfig, } from '@dspace/config/app-config.interface'; import { BuildConfig } from '@dspace/config/build-config.interface'; import { CorrelationIdService } from '@dspace/core/correlation-id/correlation-id.service'; import { LocaleService } from '@dspace/core/locale/locale.service'; import { HeadTagService } from '@dspace/core/metadata/head-tag.service'; -import { - isEmpty, - isNotEmpty, -} from '@dspace/shared/utils/empty.util'; +import { isEmpty } from '@dspace/shared/utils/empty.util'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { lastValueFrom } from 'rxjs'; @@ -117,14 +115,9 @@ export class ServerInitService extends InitService { } private saveAppConfigForCSR(): void { - if (isNotEmpty(environment.rest.ssrBaseUrl) && environment.rest.baseUrl !== environment.rest.ssrBaseUrl) { - // Avoid to transfer ssrBaseUrl in order to prevent security issues - const config: AppConfig = Object.assign({}, environment as AppConfig, { - rest: Object.assign({}, environment.rest, { ssrBaseUrl: '', hasSsrBaseUrl: true }), - }); - this.transferState.set(APP_CONFIG_STATE, config); - } else { - this.transferState.set(APP_CONFIG_STATE, environment as AppConfig); - } + this.transferState.set( + APP_CONFIG_STATE, + toClientConfig(environment as AppConfig) as AppConfig, + ); } }