<?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: Zahin Shahriar</title>
    <description>The latest articles on DEV Community by Zahin Shahriar (@zahin_shahriar_eb7dcd62a8).</description>
    <link>https://dev.to/zahin_shahriar_eb7dcd62a8</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%2F4112674%2F79736ad6-4ebe-442e-b667-74b1018bac38.png</url>
      <title>DEV Community: Zahin Shahriar</title>
      <link>https://dev.to/zahin_shahriar_eb7dcd62a8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zahin_shahriar_eb7dcd62a8"/>
    <language>en</language>
    <item>
      <title>How I Broke a "Letters Only" Filter Using Invisible Bytes (YesWeHack Dojo #54 Write-up)</title>
      <dc:creator>Zahin Shahriar</dc:creator>
      <pubDate>Tue, 06 Oct 2026 17:33:45 +0000</pubDate>
      <link>https://dev.to/zahin_shahriar_eb7dcd62a8/how-i-broke-a-letters-only-filter-using-invisible-bytes-yeswehack-dojo-54-write-up-3pa4</link>
      <guid>https://dev.to/zahin_shahriar_eb7dcd62a8/how-i-broke-a-letters-only-filter-using-invisible-bytes-yeswehack-dojo-54-write-up-3pa4</guid>
      <description>&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;I found a security bug in a small Node.js game (Dojo #54 - "Highscore"). The app checks your "session" value and only allows letters in it. I found a trick to sneak in symbols the filter should have blocked — quotes, curly braces, commas — and used them to rewrite the database query the app was about to run. That let me log in as ANY user without ever having a real password or session, and grab the flag.&lt;/p&gt;

&lt;p&gt;If you've never done a CTF or bug bounty before, don't worry — I'll explain everything with simple examples, no jargon dump.&lt;/p&gt;




&lt;h2&gt;
  
  
  Some background you need first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is "the app" actually doing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine a bouncer at a club door. You hand him a card that says something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1:mysecretsession
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bouncer does two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks that everything after the &lt;code&gt;1:&lt;/code&gt; is made only of letters (no funny symbols allowed).&lt;/li&gt;
&lt;li&gt;Uses that value to look you up in a guest list (a database).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your name is on the guest list, you get in. If your name matches a VIP's name, you might even get to see the VIP's stuff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The bouncer's "letters only" check had a blind spot. And the way your card gets turned into a database lookup was also sloppy — sloppy enough that if you could sneak in the right symbols, you could rewrite the guest list check itself to say "let everyone in."&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #1: The bouncer counts wrong
&lt;/h2&gt;

&lt;p&gt;Here's the actual filter code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isLetter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="mh"&gt;0x41&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="mh"&gt;0x5a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="mh"&gt;0x61&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="mh"&gt;0x7a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="mh"&gt;0x80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isLetter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid game session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain English: "Take my session text, turn it into raw computer bytes, and check that every byte is a letter (or a special foreign-looking byte)."&lt;/p&gt;

&lt;p&gt;Here's the catch. Computers store text as "code units," but they store &lt;em&gt;bytes&lt;/em&gt; differently depending on the character. A plain English letter like &lt;code&gt;a&lt;/code&gt; is exactly 1 byte. But an accented letter, or a symbol like &lt;code&gt;¡&lt;/code&gt;, needs &lt;strong&gt;2 bytes&lt;/strong&gt; to store — even though it still only counts as "1 character" in the text.&lt;/p&gt;

&lt;p&gt;So if I write:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10 letters → 10 characters → 10 bytes. Everything lines up. Easy to check.&lt;/li&gt;
&lt;li&gt;10 special 2-byte symbols → 10 characters → but 20 bytes! The counting is now out of sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The filter loop only checks as many bytes as there are &lt;em&gt;characters&lt;/em&gt; — not as many bytes as there actually &lt;em&gt;are&lt;/em&gt;. So if I stack up enough 2-byte symbols at the front, the "extra" bytes hiding at the end of the buffer just... never get checked. They slip through completely unexamined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy:&lt;/strong&gt; Imagine a security guard who's told "check the first 10 people in line." If I sneak 10 people in wearing backpacks that are secretly stuffed with two people each, the guard checks the first 10 backpacks and waves everyone through — never noticing that 20 actual people just walked in, and the last 10 were never looked at.&lt;/p&gt;

&lt;p&gt;That's exactly what I did — except my "people in backpacks" were harmless-looking symbols (&lt;code&gt;¡¡¡¡¡¡...&lt;/code&gt;), and the "people who snuck through unchecked" were forbidden characters like &lt;code&gt;"&lt;/code&gt;, &lt;code&gt;{&lt;/code&gt;, &lt;code&gt;}&lt;/code&gt;, and &lt;code&gt;,&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #2: Copy-pasting text straight into a database question
&lt;/h2&gt;

&lt;p&gt;Once my forbidden symbols made it through, here's what the app did next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`{"id":&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, "session":{"session":"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"}}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8peb24h7rhe3wu4f207f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8peb24h7rhe3wu4f207f.png" alt="Challenge input box showing the JSON.parse template code" width="799" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This builds a little data package and hands it to the database. But look closely — it's just gluing my raw text straight into that package, with no safety checks. It's like a form letter that says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Dear zahin, welcome to the club."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If I fill in &lt;code&gt;zahin&lt;/code&gt; with &lt;code&gt;Bob, and also delete everyone's membership. Sincerely, Bob&lt;/code&gt;, and the system copies it in &lt;em&gt;without checking what I wrote&lt;/em&gt;, I've just turned a harmless form letter into a command.&lt;/p&gt;

&lt;p&gt;That's what I did with the JSON package. Instead of putting in a normal session value, I put in text that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Closed off the sentence early,&lt;/li&gt;
&lt;li&gt;Added a brand new instruction,&lt;/li&gt;
&lt;li&gt;And that new instruction said: "the database check should just be &lt;code&gt;{}&lt;/code&gt;" — which, to a database, basically means &lt;strong&gt;"no check at all, let it through."&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The final package looked something like this (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...junk...&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"session"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the app then ran a database lookup like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which basically says: "find me a user... any user, I don't care which." The database happily returned the very first user in its list — and it turned out that user's data was the flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together — the actual "key"
&lt;/h2&gt;

&lt;p&gt;Instead of hand-typing a wall of &lt;code&gt;¡&lt;/code&gt; symbols, I generated the exact payload with a one-liner:&lt;/p&gt;

&lt;p&gt;​Command:&lt;br&gt;
&lt;code&gt;python3 -c "print('1:'+'¡'*28+'x\"},\"session\":{},\"id\":{\"z\":\"')"&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Output:&lt;/p&gt;

&lt;p&gt;​&lt;code&gt;1:¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡x"},"session":{},"id":{"z":"&lt;br&gt;
​&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhq457p3f5lujzj9i0d9p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhq457p3f5lujzj9i0d9p.png" alt=" " width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The wall of &lt;code&gt;¡&lt;/code&gt; symbols is the "decoy backpacks" — they eat up the filter's attention.&lt;/li&gt;
&lt;li&gt;Everything after that (&lt;code&gt;x"},"session":{},"id":{"z":"&lt;/code&gt;) is the actual forbidden content, riding through unchecked.&lt;/li&gt;
&lt;li&gt;I needed &lt;em&gt;exactly enough&lt;/em&gt; decoy symbols (28 of them) to match the length of my real payload — no more, no less — so the math lined up perfectly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I pasted that output straight into the challenge's input field and submitted it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5phc7xl0npymrohu019.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5phc7xl0npymrohu019.png" alt="discryption" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Result: &lt;code&gt;FLAG{N3w_L3v31_Unl0cked}&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters in real life
&lt;/h2&gt;

&lt;p&gt;This isn't just a fun puzzle. This exact combination of mistakes — (1) a length check that doesn't match how the data is actually measured, and (2) blindly gluing user input into a database query — shows up in real production apps too. If it did, an attacker could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Log in as any user without a password.&lt;/li&gt;
&lt;li&gt;Read another person's private data.&lt;/li&gt;
&lt;li&gt;In worse cases, even change or delete data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to actually fix it (for developers reading this)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;When checking bytes, count actual bytes — not "characters." (&lt;code&gt;for (let i = 0; i &amp;lt; bytes.length; i++)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Never hand-glue user text into a JSON string. Use safe tools that escape everything properly (like &lt;code&gt;JSON.stringify&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Never let raw user input become a database filter directly. Always double-check what shape and type it's allowed to be.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;This was a fun one because the bug wasn't some obscure library flaw — it was really just "the code counted two different things as if they were the same thing," and that one small mismatch cracked the whole filter wide open. A good reminder that security bugs are often just... a math mistake in disguise.&lt;/p&gt;

&lt;p&gt;Thanks for reading — feel free to try Dojo #54 yourself before reading spoilers like this one! 🚩&lt;/p&gt;

</description>
      <category>security</category>
      <category>ctf</category>
      <category>dojo</category>
      <category>cybersecurity</category>
    </item>
  </channel>
</rss>
