Independent security research

@nadler

I find the gap between intended and possible.

Security researcher and penetration tester focused on web applications, API authorization, and the exploit chains hiding between intended behavior and what is actually possible.

  • 01 Web security
  • 02 API authorization
  • 03 Exploit chains

Field notes / write-ups

Latest research

View all posts
Dark technical schematic showing a leaked identity crossing a broken authorization boundary before an account email is rerouted

From an Email-Change IDOR to Zero-Click Account Takeover

How a body-supplied clientId, an attacker-directed OTP, and a login-error identifier leak chained into a zero-click account takeover.

August 11, 2026 · 10 min · 2047 words · nadler
Abstract diagram of parallel API moderation routes where one bypasses an authorization checkpoint

i found moderation endpoint in JS files made me remove any post from the default feed.

recon since i can’t disclose the program let’s call it target.com. the app had a lot of functionality, and reading the js files was the move to understand those functions and find hidden endpoints. while analyzing the bundles i hit a really interesting block: // POST /api/posts/{id}/moderate — community "flag" (the BFLA endpoint), request schema only allows reason:"MATURE" aq=s.z.object({description:s.z.string().optional(),reason:s.z.literal("MATURE")}), aK=s.z.object({adult:s.z.boolean().optional(),message:s.z.string(),postId:s.z.string()}), aV=async(e,t,r)=>{let{data:a}=await e.post(`/api/posts/${t}/moderate`,aq,aK,r);return a}, // shared enums aG={NC17:"NC17",PG:"PG",R:"R"}, aH={BLOCKED:"BLOCKED",COMPLETE:"COMPLETE",DELETED:"DELETED",REVIEW:"REVIEW"}, // POST /api/posts/{id}/moderation — staff status/category path (correctly 403 for FREE) aY=s.z.object({category:s.z.enum(aG).optional().catch(void 0),reason:s.z.string().trim().min(1).max(500).optional(),status:s.z.enum(aH).optional().catch(void 0)}).refine(e=>void 0!==e.status||void 0!==e.category,{message:"At least one of status or category must be provided."}), aX=async(e,t,r)=>{let{data:a}=await e.post(`/api/posts/${t}/moderation`,aY,aY,r);return a}, // GET /api/posts/{id}/moderation — moderation decision log io=s.z.object({createdAt:s.z.string(),decision:s.z.string(),fromCat:s.z.enum(aG).nullable().catch(null),fromStatus:s.z.enum(aH).catch("REVIEW" ),id:s.z.string(),reason:s.z.string().nullable(),reviewerId:s.z.string().nullable(),toCat:s.z.enum(aG).nullable().catch(null),toStatus:s.z.enu m(aH).catch("REVIEW")}), is=s.z.object({data:s.z.array(io)}), il=async(e,t)=>{let{data:r}=await e.get(`/api/posts/${t}/moderation`,is);return r} the two endpoints (this is the whole bug) look closely — there are two near-identical moderation routes, one character apart: ...

June 22, 2026 · 2 min · 403 words · nadler