CVE-2023-0642: Cross-Site Request Forgery (CSRF) (CWE-352)
value: Cross-Site Request Forgery (CSRF) in GitHub repository squidex/squidex prior to 7.4.0.
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 6.5
Published: 2023-02-02T12:15Z
Repository: https://github.com/squidex/squidex
Language: Other
Files changed: backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs, backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs, backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs, backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml, backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml, backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml, backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml, frontend/src/app/shared/components/contents/content-list-cell.directive.ts, frontend/src/app/shared/components/contents/content-list-field.component.html
Commit date: 2023-01-28 19:05:47 +0100
Commit message: Antiforgery for profile pages.

Diff: diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
index aca33f5e2..a3a85c9f8 100644
--- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
+++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
@@ -17,10 +17,11 @@ using Squidex.Shared.Identity;
 using Squidex.Shared.Users;
 using Squidex.Web;
 
 namespace Squidex.Areas.IdentityServer.Controllers.Account;
 
+[AutoValidateAntiforgeryToken]
 public sealed class AccountController : IdentityServerController
 {
     private readonly IUserService userService;
     private readonly MyIdentityOptions identityOptions;
 
diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
index 45c7c2e80..b71f50b30 100644
--- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
+++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
@@ -23,10 +23,11 @@ using Squidex.Shared.Users;
 using Squidex.Web;
 
 namespace Squidex.Areas.IdentityServer.Controllers.Profile;
 
 [Authorize]
+[AutoValidateAntiforgeryToken]
 public sealed class ProfileController : IdentityServerController
 {
     private readonly IUserPictureStore userPictureStore;
     private readonly IUserService userService;
     private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs
index 5287099dd..72359170b 100644
--- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs
+++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs
@@ -19,10 +19,11 @@ using Squidex.Infrastructure.Translations;
 using Squidex.Infrastructure.Validation;
 using Squidex.Web;
 
 namespace Squidex.Areas.IdentityServer.Controllers.Setup;
 
+[AutoValidateAntiforgeryToken]
 public class SetupController : IdentityServerController
 {
     private readonly IAssetStore assetStore;
     private readonly IUrlGenerator urlGenerator;
     private readonly IUserService userService;
diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml
index acc02cf78..e28922603 100644
--- a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml
+++ b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml
@@ -10,10 +10,12 @@
         return ViewData.ModelState[error]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid ? "border-danger" : "";
     }
 }
 
 <form asp-controller="Account" asp-action="Consent" asp-route-returnurl="@Model!.ReturnUrl" method="post">
+    @Html.AntiForgeryToken()
+
     <h2>@T.Get("users.consent.headline")</h2>
         
     <label for="consentToAutomatedEmails">
         <div class="card card-consent @ErrorClass("ConsentToAutomatedEmails")">
             <div class="card-body">
diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml
index 956ab2586..04527ee7e 100644
--- a/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml
+++ b/backend/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml
@@ -29,10 +29,12 @@
             </div>
         </div>
     </div>
 
     <form asp-controller="Account" asp-action="External" asp-route-returnurl="@Model!.ReturnUrl" method="post">
+        @Html.AntiForgeryToken()
+
         @foreach (var provider in Model!.ExternalProviders)
         {
             var schema = provider.AuthenticationScheme.ToLowerInvariant();
 
             <div class="form-group">
@@ -58,10 +60,12 @@
             {
                 <div class="form-alert form-alert-error">@T.Get("users.login.error")</div>
             }
 
             <form asp-controller="Account" asp-action="Login" asp-route-returnurl="@Model!.ReturnUrl" method="post">
+                @Html.AntiForgeryToken()
+
                 <div class="form-group">
                     <input type="email" class="form-control" name="email" id="email" placeholder="@T.Get("users.login.emailPlaceholder")" />
                 </div>
 
                 <div class="form-group">
diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
index 703748120..12b2933fa 100644
--- a/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
+++ b/backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
@@ -36,20 +36,24 @@
     <div class="col profile-picture-col">
         <img class="profile-picture" src="api/users/{Model!.Id}/picture/?q={@Guid.NewGuid()}" />
     </div>
     <div class="col">
         <form id="pictureForm" class="profile-picture-form" asp-controller="Profile" asp-action="UploadPicture" method="post" enctype="multipart/form-data">
+            @Html.AntiForgeryToken()
+
             <span class="btn btn-secondary" id="pictureButton">
                 <span>@T.Get("users.profile.uploadPicture")</span>
 
                 <input class="profile-picture-input" name="file" type="file" id="pictureInput" />
             </span>
         </form>
     </div>
 </div>
 
 <form class="profile-form profile-section" asp-controller="Profile" asp-action="UpdateProfile" method="post">
+    @Html.AntiForgeryToken()
+
     <div class="form-group">
         <label for="email">@T.Get("common.email")</label>
 
         @{ RenderValidation("Email"); }
 
@@ -99,10 +103,12 @@
                     </td>
                     <td class="text-right">
                         @if (Model!.ExternalLogins.Count > 1 || Model!.HasPassword)
                         {
                             <form asp-controller="Profile" asp-action="RemoveLogin" method="post">
+                                @Html.AntiForgeryToken()
+
                                 <input type="hidden" value="@login.LoginProvider" name="LoginProvider" />
                                 <input type="hidden" value="@login.ProviderKey" name="ProviderKey" />
 
                                 <button type="submit" class="btn btn-text-danger btn-sm">
                                     @T.Get("common.remove")
@@ -113,10 +119,12 @@
                 </tr>
             }
         </table>
 
         <form asp-controller="Profile" asp-action="AddLogin" method="post">
+            @Html.AntiForgeryToken()
+
             @foreach (var provider in Model!.ExternalProviders.Where(x => Model!.ExternalLogins.All(y => x.AuthenticationScheme != y.LoginProvider)))
             {
                 var schema = provider.AuthenticationScheme.ToLowerInvariant();
 
                 <button class="btn external-button-small btn-@schema" type="submit" name="provider" value="@provider.AuthenticationScheme">
@@ -132,13 +140,15 @@
     <hr />
 
     <div class="profile-section">
         <h2>@T.Get("users.profile.passwordTitle")</h2>
 
-       @if (Model!.HasPassword)
+        @if (Model!.HasPassword)
         {
             <form class="profile-form" asp-controller="Profile" asp-action="ChangePassword" method="post">
+                @Html.AntiForgeryToken()
+
                 <div class="form-group">
                     <label for="oldPassword">@T.Get("common.oldPassword")</label>
 
                     @{ RenderValidation("OldPassword"); }
 
@@ -167,10 +177,12 @@
             </form>
         }
         else
         {
             <form class="profile-form" asp-controller="Profile" asp-action="SetPassword" method="post">
+                @Html.AntiForgeryToken()
+
                 <div class="form-group">
                     <label for="password">@T.Get("common.password")</label>
 
                     @{ RenderValidation("Password"); }
 
@@ -215,10 +227,12 @@
         </div>
         <div class="col-4 pl-2">
             <label for="generate">&nbsp;</label>
 
             <form class="profile-form" asp-controller="Profile" asp-action="GenerateClientSecret" method="post">
+                @Html.AntiForgeryToken()
+
                 <button type="submit" class="btn btn-success btn-block" id="generate">@T.Get("users.profile.generateClient")</button>
             </form>
         </div>
     </div>
 </div>
@@ -229,10 +243,12 @@
     <h2>@T.Get("users.profile.propertiesTitle")</h2>
 
     <small class="form-text text-muted mt-2 mb-2">@T.Get("users.profile.propertiesHint")</small>
 
     <form class="profile-form" asp-controller="Profile" asp-action="UpdateProperties" method="post">
+        @Html.AntiForgeryToken()
+
         <div class="mb-2" id="properties">
             @for (var i = 0; i < Model!.Properties.Count; i++)
             {
                 <div class="row g-2 form-group">
                     <div class="col-5 pr-2">
diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml
index eb6b6b95c..65e311e85 100644
--- a/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml
+++ b/backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml
@@ -158,10 +158,12 @@
                         @Model!.ErrorMessage
                     </div>
         }
 
         <form class="profile-form" asp-controller="Setup" asp-action="Setup" method="post">
+            @Html.AntiForgeryToken()
+
             <div class="form-group">
                 <label for="email">@T.Get("common.email")</label>
 
                 @{ RenderValidation("Email"); }
 
diff --git a/frontend/src/app/shared/components/contents/content-list-cell.directive.ts b/frontend/src/app/shared/components/contents/content-list-cell.directive.ts
index b61e2df8c..4bfb5f14a 100644
--- a/frontend/src/app/shared/components/contents/content-list-cell.directive.ts
+++ b/frontend/src/app/shared/components/contents/content-list-cell.directive.ts
@@ -34,11 +34,11 @@ export function getCellWidth(field: TableField, sizes: FieldSizes | undefined |
         case META_FIELDS.status:
             return 200;
         case META_FIELDS.statusNext:
             return 240;
         case META_FIELDS.statusColor:
-            return 50;
+            return 80;
         case META_FIELDS.version:
             return 80;
         default:
             return 200;
     }
diff --git a/frontend/src/app/shared/components/contents/content-list-field.component.html b/frontend/src/app/shared/components/contents/content-list-field.component.html
index 7122d94d0..a806453ca 100644
--- a/frontend/src/app/shared/components/contents/content-list-field.component.html
+++ b/frontend/src/app/shared/components/contents/content-list-field.component.html
@@ -80,11 +80,11 @@
             {{ 'contents.scheduledAt' | sqxTranslate }}&nbsp;{{content.scheduleJob.dueTime | sqxShortDate}}
         </span>
     </ng-container>
     <ng-container *ngSwitchCase="metaFields.statusColor">
         <ng-container *ngIf="content.newStatus; else singleStatus">
-            <span class="text-nowrap">
+            <span class="text-nowrap truncate">
                 <sqx-content-status
                     [status]="content.status"
                     [statusColor]="content.statusColor">
                 </sqx-content-status>
     

Vulnerable code:        @if (Model!.HasPassword)
            return 50;
            <span class="text-nowrap">
Fixed code: [AutoValidateAntiforgeryToken]
[AutoValidateAntiforgeryToken]
[AutoValidateAntiforgeryToken]
    @Html.AntiForgeryToken()

        @Html.AntiForgeryToken()

                @Html.AntiForgeryToken()

            @Html.AntiForgeryToken()

    @Html.AntiForgeryToken()

                                @Html.AntiForgeryToken()

            @Html.AntiForgeryToken()

        @if (Model!.HasPassword)
                @Html.AntiForgeryToken()

                @Html.AntiForgeryToken()

                @Html.AntiForgeryToken()

        @Html.AntiForgeryToken()

            @Html.AntiForgeryToken()

            return 80;
            <span class="text-nowrap truncate">