The mismatch
The parser created a UTF-8 buffer, then used the original string length as its loop limit:
const bytes = Buffer.from(session, "utf8");
for (let i = 0; i < session.length; i++) {
// validate bytes[i]
}
For ASCII, one character is one byte. A character such as Ç occupies one
JavaScript string position but two UTF-8 bytes. Enough multi-byte characters make the
loop end before it reaches later ASCII bytes in the buffer.
Why the skipped bytes mattered
The application built JSON by placing the session value directly inside a template string:
JSON.parse(
`{"id":${id}, "session":{"session":"${session}"}}`
)
Once quotation marks and braces reached this expression, they could end the original
string value and add another session key. JavaScript’s JSON parser keeps the
last value when an object contains duplicate keys.
The query bypass
The parsed value was then used directly as a Sequelize filter:
Users.findOne({ where: cookie["session"] })
I padded the session with multi-byte characters and followed it with JSON that replaced the expected session lookup with an ID lookup. The injected bytes sat beyond the part of the UTF-8 buffer examined by the filter, but they were still present when the JSON was parsed.
1:[multi-byte padding]"}, "session": {"id": "1
The resulting query returned the privileged challenge record and revealed the flag.
Result
What should change?
- When validating bytes, iterate over
bytes.length. - Do not construct JSON by concatenating untrusted strings.
- Build the object directly and pass an explicit allowlist of fields to the ORM.