Debug mode: Marks UMB-DEBUG cookie as HttpOnly and Secure (#21032)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jacob Overgaard
2025-12-04 08:55:39 +01:00
committed by GitHub
parent 84fecd3521
commit cb454372f2
3 changed files with 101 additions and 24 deletions

View File

@@ -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> 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);
}
}

View File

@@ -2550,13 +2550,19 @@ export default {
profiling: {
performanceProfiling: 'Performance profiling',
performanceProfilingDescription:
"<p>Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages.</p><p>If you want to activate the profiler for a specific page rendering, simply add <strong>umbDebug=true</strong> to the querystring when requesting the page.</p><p>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 <em>your</em> browser - not everyone else's.</p>",
"<p>Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages.</p><p>If you want to activate the profiler for a specific page rendering, simply add <strong>umbDebug=true</strong> to the querystring when requesting the page.</p><p>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 <em>your</em> browser - not everyone else's.</p><p><strong>Note:</strong> This will only work if the Backoffice is currently located on the same URL as the front-end website.</p>",
activateByDefault: 'Activate the profiler by default',
reminder: 'Friendly reminder',
reminderDescription:
'<p>You should never let a production site run in debug mode. Debug mode is turned off by setting <strong>Umbraco:CMS:Hosting:Debug</strong> to <strong>false</strong> in appsettings.json, appsettings.{Environment}.json or via an environment variable.</p>',
profilerEnabledDescription:
"<p>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.</p><p>Debug mode is turned on by setting <strong>Umbraco:CMS:Hosting:Debug</strong> to <strong>true</strong> in appsettings.json, appsettings.{Environment}.json or via an environment variable.</p>",
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',

View File

@@ -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'))}
<umb-localize key="profiling_performanceProfilingDescription"></umb-localize>
<uui-toggle
id="toggle"
label=${this.localize.term('profiling_activateByDefault')}
label-position="left"
?checked="${this._profilingStatus}"
@change="${this._changeProfilingStatus}"></uui-toggle>
?disabled="${this._isLoading}"
@change="${() =>
this._profilingStatus ? this.#disableProfilingStatus() : this.#enableProfilingStatus()}"></uui-toggle>
<h4>${this.localize.term('profiling_reminder')}</h4>
${when(this._isLoading, () => html`<uui-loader-circle></uui-loader-circle>`)}
${unsafeHTML(this.localize.term('profiling_reminderDescription'))}
<h4>
<umb-localize key="profiling_reminder"></umb-localize>
</h4>
<umb-localize key="profiling_reminderDescription"></umb-localize>
`
: html` ${unsafeHTML(this.localize.term('profiling_profilerEnabledDescription'))} `;
: html`<umb-localize key="profiling_profilerEnabledDescription"></umb-localize>`;
}
override render() {