<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Eduard Arbona</title>
    <description>The latest articles on DEV Community by Eduard Arbona (@earbona23).</description>
    <link>https://dev.to/earbona23</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4203591%2F84463252-b3b9-4565-acac-bddbe27af8bd.jpg</url>
      <title>DEV Community: Eduard Arbona</title>
      <link>https://dev.to/earbona23</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/earbona23"/>
    <language>en</language>
    <item>
      <title>The parser survey a USENIX paper left as future work</title>
      <dc:creator>Eduard Arbona</dc:creator>
      <pubDate>Sun, 01 Nov 2026 12:02:35 +0000</pubDate>
      <link>https://dev.to/earbona23/the-parser-survey-a-usenix-paper-left-as-future-work-59kh</link>
      <guid>https://dev.to/earbona23/the-parser-survey-a-usenix-paper-left-as-future-work-59kh</guid>
      <description>&lt;p&gt;description: Cookie Crumbles surveyed cookie handling across browsers and 13 frameworks, then left the server-side parser study as future work. Three years later it still did not exist, so I built it.&lt;/p&gt;

&lt;p&gt;tags: security, dotnet, go, webdev&lt;br&gt;
In 2023, Squarcina, Adão, Veronese and Maffei presented Cookie Crumbles:&lt;/p&gt;

&lt;p&gt;Breaking and Fixing Web Session Integrity at the 32nd USENIX Security&lt;/p&gt;

&lt;p&gt;Symposium. It is a good paper. It surveys cookie integrity across the top three&lt;/p&gt;

&lt;p&gt;browsers and 13 web frameworks, and it finds session integrity problems in nine&lt;/p&gt;

&lt;p&gt;of them.&lt;br&gt;
Section 4.3 describes a third stage of the work: feeding crafted Cookie&lt;/p&gt;

&lt;p&gt;headers to the server-side parsers and comparing what each one understood. Then&lt;/p&gt;

&lt;p&gt;it says this:&lt;br&gt;
We acknowledge that this approach does not constitute a systematic evaluation&lt;/p&gt;

&lt;p&gt;of server-side parsing inconsistencies. However, our initial analysis provided&lt;/p&gt;

&lt;p&gt;strong evidence of the pervasiveness of the issue. We leave such comprehensive&lt;/p&gt;

&lt;p&gt;study as future work.&lt;br&gt;
That sentence is an invitation, and as far as I can tell nobody took it. So I&lt;/p&gt;

&lt;p&gt;built the bench: &lt;a href="https://github.com/earbona23/http-parser-differentials" rel="noopener noreferrer"&gt;https://github.com/earbona23/http-parser-differentials&lt;/a&gt;&lt;br&gt;
Why a disagreement is the bug, not a wrong answer&lt;br&gt;
The instinct with a comparison table is to look for the loser. That is the&lt;/p&gt;

&lt;p&gt;wrong instinct.&lt;br&gt;
HTTP is parsed many times on the way to your code. A CDN parses it. A reverse&lt;/p&gt;

&lt;p&gt;proxy parses it. A WAF parses it. Your framework parses it. Each of those is&lt;/p&gt;

&lt;p&gt;free to be reasonable and still differ, because the grammars are old, widely&lt;/p&gt;

&lt;p&gt;extended, and full of cases the RFCs never pinned down.&lt;br&gt;
The security consequence does not need either parser to be wrong. It needs them&lt;/p&gt;

&lt;p&gt;to be different. A gateway that inspects a field and an application that reads&lt;/p&gt;

&lt;p&gt;another value for that same field are both behaving exactly as written. The gap&lt;/p&gt;

&lt;p&gt;between them is where the request you blocked and the request you served stop&lt;/p&gt;

&lt;p&gt;being the same request.&lt;br&gt;
That is the class the bench is built to make visible.&lt;br&gt;
What it does&lt;br&gt;
Two benches. Each one starts a small reflector program per implementation on&lt;/p&gt;

&lt;p&gt;127.0.0.1, sends byte-identical input to all of them over a raw socket, and&lt;/p&gt;

&lt;p&gt;prints what each one parsed.&lt;br&gt;
Cookie header, 39 vectors, five implementations: ASP.NET Core&lt;/p&gt;

&lt;p&gt;(RequestCookieCollection), Go (net/http), PHP (native $_COOKIE), the&lt;/p&gt;

&lt;p&gt;algorithm of the npm cookie package that Express and Koa use, and Python&lt;/p&gt;

&lt;p&gt;(http.cookies). Duplicate names, case, percent-decoding, quoting, separators,&lt;/p&gt;

&lt;p&gt;whitespace, control and high bytes, repeated Cookie headers.&lt;br&gt;
Multipart, 17 vectors, four implementations: ASP.NET Core (ReadFormAsync),&lt;/p&gt;

&lt;p&gt;Go (mime/multipart), PHP, Python (email with an HTTP policy). Duplicate&lt;/p&gt;

&lt;p&gt;field names, quoting of the name parameter, repeated and malformed&lt;/p&gt;

&lt;p&gt;Content-Disposition, bare LF line endings, boundary handling, RFC 2231&lt;/p&gt;

&lt;p&gt;encoded names, preamble and epilogue.&lt;br&gt;
Raw sockets matter here. An HTTP client library will refuse to send half of&lt;/p&gt;

&lt;p&gt;these vectors, and if it quietly normalises one of them you end up measuring&lt;/p&gt;

&lt;p&gt;your own client instead of the parser. I learned that the hard way on an&lt;/p&gt;

&lt;p&gt;unrelated piece of work, when a harness of mine encoded a header as ASCII&lt;/p&gt;

&lt;p&gt;before sending it and I spent an afternoon analysing a bug I had written.&lt;br&gt;
What came out&lt;br&gt;
Six of 39 cookie vectors and seven of 17 multipart vectors put one&lt;/p&gt;

&lt;p&gt;implementation on its own. The numbers in the repository are measured against&lt;/p&gt;

&lt;p&gt;.NET 10.0.14, Go 1.21, PHP 8, Node 24 and Python 3, and they will move. Rerun&lt;/p&gt;

&lt;p&gt;rather than trusting my table.&lt;br&gt;
Two results worth pulling out, because they point in opposite directions.&lt;br&gt;
Case. Cookie: A=1; a=2 is two cookies to Go, PHP, Node and Python. To&lt;/p&gt;

&lt;p&gt;ASP.NET Core it is one, because the collection is built with&lt;/p&gt;

&lt;p&gt;StringComparer.OrdinalIgnoreCase. RFC 6265 gives no case-folding rule and&lt;/p&gt;

&lt;p&gt;browsers treat the two as distinct entries. This one is already known upstream&lt;/p&gt;

&lt;p&gt;and tracked by the ASP.NET team themselves.&lt;br&gt;
A comma. Cookie: a=1,b=2 returns the cookie to the other four. ASP.NET&lt;/p&gt;

&lt;p&gt;Core returns nothing at all, and not just for that pair: a comma in a value&lt;/p&gt;

&lt;p&gt;discards the rest of the header from that point. That is fail-closed, which is&lt;/p&gt;

&lt;p&gt;the safe direction, but it is a surprising amount of silence for one character.&lt;br&gt;
Standing alone is not the same as being wrong&lt;br&gt;
This is the part I would want a reviewer to hold me to.&lt;br&gt;
Three of the seven multipart cases where ASP.NET Core differs from its peers&lt;/p&gt;

&lt;p&gt;are cases where it is stricter. It rejects a duplicated&lt;/p&gt;

&lt;p&gt;Content-Disposition, it rejects bare LF line endings, and it rejects a fake&lt;/p&gt;

&lt;p&gt;boundary planted inside a field value. Go and Python accept all three; PHP&lt;/p&gt;

&lt;p&gt;accepts two. On that evidence its multipart parser is in better shape than the&lt;/p&gt;

&lt;p&gt;others, and a table that only counts divergences would tell you the opposite.&lt;br&gt;
A flag in the output is a pointer. The verdict needs a human who checks the&lt;/p&gt;

&lt;p&gt;direction: a parser that drops a field fails closed, a parser that invents or&lt;/p&gt;

&lt;p&gt;merges one does not, and those are not the same finding.&lt;br&gt;
A bench is only as good as its weakest reflector&lt;br&gt;
An early version of my multipart reflector printed the file size for .NET and&lt;/p&gt;

&lt;p&gt;not for the other three. Four parsers agreed perfectly, and my comparison&lt;/p&gt;

&lt;p&gt;reported a divergence, because the signatures it compared included a field only&lt;/p&gt;

&lt;p&gt;one of them emitted. I was measuring my own code.&lt;br&gt;
I fixed the reflector rather than documenting the false positive, which I think&lt;/p&gt;

&lt;p&gt;is the right order, and the story is in the README so the next person building&lt;/p&gt;

&lt;p&gt;one of these checks their shapes first.&lt;br&gt;
What this is not&lt;br&gt;
It is not a vulnerability disclosure. Everything in it is public, documented&lt;/p&gt;

&lt;p&gt;behaviour of public parsers, exercised against local reflectors written for the&lt;/p&gt;

&lt;p&gt;repository. Nothing in it touches a system I do not own, and every byte stays on&lt;/p&gt;

&lt;p&gt;loopback.&lt;br&gt;
If you run it against a version I did not, and something in the output looks&lt;/p&gt;

&lt;p&gt;like a security bug in a product, take it to that product's security team before&lt;/p&gt;

&lt;p&gt;you write about it.&lt;br&gt;
MIT licensed. Issues and additional implementations welcome, particularly Java,&lt;/p&gt;

&lt;p&gt;Ruby and Rust, which I did not cover.&lt;/p&gt;

</description>
      <category>computerscience</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Detect the Illicit Consent Grant Attack in Microsoft Entra ID</title>
      <dc:creator>Eduard Arbona</dc:creator>
      <pubDate>Sat, 24 Oct 2026 01:02:31 +0000</pubDate>
      <link>https://dev.to/earbona23/how-i-detect-the-illicit-consent-grant-attack-in-microsoft-entra-id-4ioc</link>
      <guid>https://dev.to/earbona23/how-i-detect-the-illicit-consent-grant-attack-in-microsoft-entra-id-4ioc</guid>
      <description>&lt;p&gt;&lt;em&gt;Eduard Arbona — identity security engineer. Contributions merged into Maester and Azure Sentinel; author of EntraTripwire.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most people picture an account takeover as a stolen password. In Microsoft Entra ID, one of the quietest and most durable takeovers never touches the password at all. It is the illicit consent grant, and if you only watch for failed sign-ins you will miss it completely.&lt;/p&gt;

&lt;p&gt;Here is how it works, why multi-factor authentication does not save you, and the exact signal I use to catch it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attack, start to finish
&lt;/h2&gt;

&lt;p&gt;An attacker registers an application in their own tenant, usually multi-tenant so it can be consented from anywhere. They craft a normal OAuth 2.0 authorization URL that asks for delegated permissions such as &lt;code&gt;Mail.Read&lt;/code&gt;, &lt;code&gt;Mail.Send&lt;/code&gt;, &lt;code&gt;Files.Read.All&lt;/code&gt;, and critically &lt;code&gt;offline_access&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then they phish a link. The victim clicks it and lands on the real &lt;code&gt;login.microsoftonline.com&lt;/code&gt; page. They sign in, they pass MFA, everything looks legitimate, because it is. The only unusual part is a consent screen asking them to let "Some App" read their mail. People click through consent screens all day.&lt;/p&gt;

&lt;p&gt;The moment the victim consents, Entra ID creates a service principal and an OAuth permission grant in the victim's own tenant. The application receives an access token, and because of &lt;code&gt;offline_access&lt;/code&gt;, a refresh token. From that point the attacker reads the mailbox through Microsoft Graph using tokens, on their own schedule, from their own infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your usual defenses do not fire
&lt;/h2&gt;

&lt;p&gt;This is the part that surprises people:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The password was never captured&lt;/strong&gt;, so a password reset does not revoke access. The refresh token keeps working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MFA already succeeded&lt;/strong&gt; during the consent flow. Re-prompting for MFA later does nothing to a token refresh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no malware&lt;/strong&gt; on any endpoint, so EDR has nothing to find.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign-in logs look normal.&lt;/strong&gt; The victim had a legitimate interactive sign-in. The ongoing access shows up as app-only Graph calls, not as suspicious logins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Revoking this requires removing the grant and the service principal. Until you do, the attacker has quiet, persistent mailbox access that survives most of your incident response reflexes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal I actually watch
&lt;/h2&gt;

&lt;p&gt;The event you want lives in the Entra ID audit log, not the sign-in log. The operation is &lt;strong&gt;"Consent to application"&lt;/strong&gt; (and the related delegated permission grant events), available through Microsoft Graph at &lt;code&gt;auditLogs/directoryAudits&lt;/code&gt; and surfaced in Microsoft Sentinel as the &lt;code&gt;AuditLogs&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;A consent is not automatically malicious. What makes one worth an alert is the combination:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app is &lt;strong&gt;not from a verified publisher&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The granted delegated scopes include &lt;strong&gt;mail or file access plus &lt;code&gt;offline_access&lt;/code&gt;&lt;/strong&gt; (persistence is the tell).&lt;/li&gt;
&lt;li&gt;The consent was performed by an &lt;strong&gt;ordinary user&lt;/strong&gt;, not an admin, often to an app that was &lt;strong&gt;registered very recently&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The app's reply URL points to an &lt;strong&gt;unfamiliar domain&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the shape of the Sentinel KQL I use to pull the high-signal cases out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AuditLogs
| where OperationName == "Consent to application"
| mv-expand target = TargetResources
| mv-expand mod = target.modifiedProperties
| where tostring(mod.displayName) == "ConsentAction.Permissions"
| extend grantedPerms = tostring(mod.newValue)
| where grantedPerms has "offline_access"
    and (grantedPerms has "Mail.Read"
      or grantedPerms has "Mail.ReadWrite"
      or grantedPerms has "Mail.Send")
| extend actor = tostring(InitiatedBy.user.userPrincipalName)
| project TimeGenerated, actor, grantedPerms, Result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing to check first: the exact shape of &lt;code&gt;modifiedProperties&lt;/code&gt; varies between tenants and log versions, so treat the parsing as a starting point and confirm the field layout in your own &lt;code&gt;AuditLogs&lt;/code&gt; before you trust the rule. I validate my detections against the official KQL parser and map each one to a MITRE ATT&amp;amp;CK technique so I know what I am actually covering and what I am not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the door, not just watching it
&lt;/h2&gt;

&lt;p&gt;Detection is half the job. The configuration change that removes most of the risk is tightening the consent policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restrict user consent to &lt;strong&gt;verified publishers and low-risk permissions only&lt;/strong&gt;, and route everything else through an &lt;strong&gt;admin consent request workflow&lt;/strong&gt;. A human reviews before mail access is ever granted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review enterprise applications&lt;/strong&gt; and their granted permissions on a schedule. Old, forgotten, over-permissioned apps are exactly what an attacker hides behind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert on every new consent&lt;/strong&gt; to an app requesting mail or &lt;code&gt;offline_access&lt;/code&gt;, using the query above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also take a deception angle with a tool I published, EntraTripwire: plant decoy applications and credentials that nothing legitimate should ever consent to or touch, and score whoever does. A consent grant against a decoy app is a near-zero-false-positive signal that someone is probing your tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than it looks
&lt;/h2&gt;

&lt;p&gt;The illicit consent grant is attractive to attackers precisely because it is boring to defenders. It does not trip the alarms everyone tunes first. It is a configuration-and-token problem wearing the clothes of a normal user action.&lt;/p&gt;

&lt;p&gt;If you run Entra ID, two things are worth doing this week: pull your last ninety days of "Consent to application" events and look for mail plus &lt;code&gt;offline_access&lt;/code&gt; grants to unverified apps, and check whether user consent is still wide open in your tenant. Both take minutes. Both can surface a problem that your password policy and your MFA were never going to catch.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build read-only security tooling for Microsoft Entra ID and M365. The consent-monitoring work in this article is open source on my GitHub (earbona23), and a related analytic rule is merged into Microsoft's Azure Sentinel repository.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>azure</category>
      <category>entraid</category>
      <category>microsoftgraph</category>
    </item>
  </channel>
</rss>
