From cb454372f226198a27657c11a9be4b1d41730ba1 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 4 Dec 2025 08:55:39 +0100 Subject: [PATCH] Debug mode: Marks UMB-DEBUG cookie as HttpOnly and Secure (#21032) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: sets profiling cookie to httpOnly and strict in order to run non-secure * fix: adds extra message to explain when you can set a cookie * fix: simplify cookie explanation comment in WebProfilerRepository 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * fix: checks that the profiler is actually enabled and/or disabled and warns the user if that is not the case * Update src/Umbraco.Web.UI.Client/src/assets/lang/en.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Repositories/WebProfilerRepository.cs | 25 ++++- .../src/assets/lang/en.ts | 8 +- ...dashboard-performance-profiling.element.ts | 92 +++++++++++++++---- 3 files changed, 101 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs b/src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs index 668f1f46de..dc8c8d2510 100644 --- a/src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs +++ b/src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs @@ -1,5 +1,8 @@ using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Web; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Repositories; @@ -11,21 +14,35 @@ internal sealed class WebProfilerRepository : IWebProfilerRepository private const string QueryName = "umbDebug"; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ICookieManager _cookieManager; + private readonly GlobalSettings _globalSettings; - public WebProfilerRepository(IHttpContextAccessor httpContextAccessor) + public WebProfilerRepository(IHttpContextAccessor httpContextAccessor, ICookieManager cookieManager, IOptions globalSettings) { _httpContextAccessor = httpContextAccessor; + _cookieManager = cookieManager; + _globalSettings = globalSettings.Value; } public void SetStatus(int userId, bool status) { if (status) { - _httpContextAccessor.GetRequiredHttpContext().Response.Cookies.Append(CookieName, "1", new CookieOptions { Expires = DateTime.Now.AddYears(1) }); + // This cookie enables debug profiling on the front-end without needing query strings or headers. + // It uses SameSite=Strict, so it only works when the BackOffice and front-end share the same domain. + // It's marked httpOnly to prevent JavaScript access (the server reads it, not client-side code). + // No expiration is set, so it's a session cookie and will be deleted when the browser closes. + // For cross-site setups, use the query string (?umbDebug=true) or header (X-UMB-DEBUG) instead. + _cookieManager.SetCookieValue( + CookieName, + "1", + httpOnly: true, + secure: _globalSettings.UseHttps, + sameSiteMode: "Strict"); } else { - _httpContextAccessor.GetRequiredHttpContext().Response.Cookies.Delete(CookieName); + _cookieManager.ExpireCookie(CookieName); } } @@ -43,6 +60,6 @@ internal sealed class WebProfilerRepository : IWebProfilerRepository return xUmbDebug; } - return request.Cookies.ContainsKey(CookieName); + return _cookieManager.HasCookie(CookieName); } } diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index dec7546ff4..b9e16e8b20 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2550,13 +2550,19 @@ export default { profiling: { performanceProfiling: 'Performance profiling', performanceProfilingDescription: - "

Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages.

If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page.

If you want the profiler to be activated by default for all page renderings, you can use the toggle below. It will set a cookie in your browser, which then activates the profiler automatically. In other words, the profiler will only be active by default in your browser - not everyone else's.

", + "

Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages.

If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page.

If you want the profiler to be activated by default for all page renderings, you can use the toggle below. It will set a cookie in your browser, which then activates the profiler automatically. In other words, the profiler will only be active by default in your browser - not everyone else's.

Note: This will only work if the Backoffice is currently located on the same URL as the front-end website.

", activateByDefault: 'Activate the profiler by default', reminder: 'Friendly reminder', reminderDescription: '

You should never let a production site run in debug mode. Debug mode is turned off by setting Umbraco:CMS:Hosting:Debug to false in appsettings.json, appsettings.{Environment}.json or via an environment variable.

', profilerEnabledDescription: "

Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site.

Debug mode is turned on by setting Umbraco:CMS:Hosting:Debug to true in appsettings.json, appsettings.{Environment}.json or via an environment variable.

", + errorEnablingProfilerTitle: 'Error enabling profiler', + errorEnablingProfilerDescription: + 'It was not possible to enable the profiler. Check that you are accessing the Backoffice on the same URL as the front-end website, and try again. If the problem persists, please check the log for more details.', + errorDisablingProfilerTitle: 'Error disabling profiler', + errorDisablingProfilerDescription: + 'It was not possible to disable the profiler. Try again, and if the problem persists, please check the log for more details.', }, settingsDashboard: { documentationHeader: 'Documentation', diff --git a/src/Umbraco.Web.UI.Client/src/packages/performance-profiling/dashboard-performance-profiling.element.ts b/src/Umbraco.Web.UI.Client/src/packages/performance-profiling/dashboard-performance-profiling.element.ts index 84cb993113..dd9a6ad1bb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/performance-profiling/dashboard-performance-profiling.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/performance-profiling/dashboard-performance-profiling.element.ts @@ -1,8 +1,10 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, customElement, state, query, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, query, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { ProfilingService } from '@umbraco-cms/backoffice/external/backend-api'; import { tryExecute } from '@umbraco-cms/backoffice/resources'; +import { consumeContext } from '@umbraco-cms/backoffice/context-api'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; @customElement('umb-dashboard-performance-profiling') export class UmbDashboardPerformanceProfilingElement extends UmbLitElement { @@ -13,55 +15,107 @@ export class UmbDashboardPerformanceProfilingElement extends UmbLitElement { @state() private _isDebugMode = true; + @state() + private _isLoading = true; + @query('#toggle') private _toggle!: HTMLInputElement; + @consumeContext({ context: UMB_NOTIFICATION_CONTEXT }) + private _notificationContext: typeof UMB_NOTIFICATION_CONTEXT.TYPE | undefined; + #setToggle(value: boolean) { this._toggle.checked = value; this._profilingStatus = value; + this._isLoading = false; } - override firstUpdated() { - this._getProfilingStatus(); + override async firstUpdated() { + const status = await this.#getProfilingStatus(); + this.#setToggle(status); } - private async _getProfilingStatus() { + async #getProfilingStatus() { const { data } = await tryExecute(this, ProfilingService.getProfilingStatus()); - if (!data) return; - this._profilingStatus = data.enabled ?? false; + return data?.enabled ?? false; } - private async _changeProfilingStatus() { - const { error } = await tryExecute( - this, - ProfilingService.putProfilingStatus({ body: { enabled: !this._profilingStatus } }), - ); + async #disableProfilingStatus() { + this._isLoading = true; + const { error } = await tryExecute(this, ProfilingService.putProfilingStatus({ body: { enabled: false } })); if (error) { - this.#setToggle(this._profilingStatus); - } else { - this.#setToggle(!this._profilingStatus); + this.#setToggle(true); + return; } + + // Test that it was actually disabled + const status = await this.#getProfilingStatus(); + + if (status) { + this.#setToggle(true); + this._notificationContext?.peek('warning', { + data: { + headline: this.localize.term('profiling_errorDisablingProfilerTitle'), + message: this.localize.term('profiling_errorDisablingProfilerDescription'), + }, + }); + return; + } + + this.#setToggle(false); + } + + async #enableProfilingStatus() { + this._isLoading = true; + const { error } = await tryExecute(this, ProfilingService.putProfilingStatus({ body: { enabled: true } })); + + if (error) { + this.#setToggle(false); + return; + } + + // Test that it was actually enabled + const status = await this.#getProfilingStatus(); + + if (!status) { + this.#setToggle(false); + this._notificationContext?.peek('warning', { + data: { + headline: this.localize.term('profiling_errorEnablingProfilerTitle'), + message: this.localize.term('profiling_errorEnablingProfilerDescription'), + }, + }); + return; + } + + this.#setToggle(true); } #renderProfilingStatus() { return this._isDebugMode ? html` - ${unsafeHTML(this.localize.term('profiling_performanceProfilingDescription'))} + + ?disabled="${this._isLoading}" + @change="${() => + this._profilingStatus ? this.#disableProfilingStatus() : this.#enableProfilingStatus()}"> -

${this.localize.term('profiling_reminder')}

+ ${when(this._isLoading, () => html``)} - ${unsafeHTML(this.localize.term('profiling_reminderDescription'))} +

+ +

+ + ` - : html` ${unsafeHTML(this.localize.term('profiling_profilerEnabledDescription'))} `; + : html``; } override render() {