From ecf5150a60e9de9436ca9db3604095d4e23fa682 Mon Sep 17 00:00:00 2001 From: souvlakia Date: Sat, 19 Jul 2025 16:28:10 +0300 Subject: [PATCH 1/4] valid object keys --- src/helpers.ts | 11 +++++++++++ src/unserialize.ts | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/helpers.ts b/src/helpers.ts index dfac809..8fb7d22 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -19,6 +19,17 @@ export function isInteger(value: any): boolean { return typeof value === 'number' && Number.parseInt(value.toString(), 10) === value } +export function isValidKey(key: any): [boolean, string] { + if (!(typeof key === 'string' || typeof key === 'number')) { + return [false, `Invalid key type '${typeof key}' encountered while unserializing`] + } + if (key === '__proto__') { + // Prevent prototype pollution + return [false, 'Key "__proto__" is not allowed'] + } + return [true, ''] +} + export function getIncompleteClass(name: string) { return new __PHP_Incomplete_Class(name) } diff --git a/src/unserialize.ts b/src/unserialize.ts index a76dacf..9004abf 100644 --- a/src/unserialize.ts +++ b/src/unserialize.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line import/no-cycle import Parser from './parser' -import { isInteger, getClass, getIncompleteClass, __PHP_Incomplete_Class, invariant } from './helpers' +import { isInteger, getClass, getIncompleteClass, __PHP_Incomplete_Class, invariant, isValidKey } from './helpers' export type Options = { strict: boolean @@ -71,6 +71,8 @@ function unserializeItem(parser: Parser, scope: Record, options: Op const isArray = pairs.every((item, idx) => isInteger(item.key) && idx === item.key) const result = isArray ? [] : {} pairs.forEach(({ key, value }) => { + const [isValid, errorMessage] = isValidKey(key) + invariant(isValid, errorMessage) result[key] = value }) return result @@ -80,10 +82,13 @@ function unserializeItem(parser: Parser, scope: Record, options: Op parser.seekExpected(':') const pairs = parser.getByLength('{', '}', length => unserializePairs(parser, length, scope, options)) const result = getClassReference(name, scope, options.strict) + invariant(!result.unserialize, `Found unserialize method on class ${name} but expected notserializable-class`) const PREFIX_PRIVATE = `\u0000${name}\u0000` const PREFIX_PROTECTED = `\u0000*\u0000` pairs.forEach(({ key, value }) => { + const [isValid, errorMessage] = isValidKey(key) + invariant(isValid, errorMessage) if (key.startsWith(PREFIX_PRIVATE)) { // Private field result[key.slice(PREFIX_PRIVATE.length)] = value From 1c5f4a0f895dcd40de69dfea2cc553cf515d37ae Mon Sep 17 00:00:00 2001 From: souvlakia Date: Sat, 19 Jul 2025 17:24:55 +0300 Subject: [PATCH 2/4] better syntax --- src/unserialize.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/unserialize.ts b/src/unserialize.ts index 9004abf..6a4d4a5 100644 --- a/src/unserialize.ts +++ b/src/unserialize.ts @@ -71,8 +71,7 @@ function unserializeItem(parser: Parser, scope: Record, options: Op const isArray = pairs.every((item, idx) => isInteger(item.key) && idx === item.key) const result = isArray ? [] : {} pairs.forEach(({ key, value }) => { - const [isValid, errorMessage] = isValidKey(key) - invariant(isValid, errorMessage) + invariant(...isValidKey(key)) result[key] = value }) return result @@ -87,8 +86,7 @@ function unserializeItem(parser: Parser, scope: Record, options: Op const PREFIX_PRIVATE = `\u0000${name}\u0000` const PREFIX_PROTECTED = `\u0000*\u0000` pairs.forEach(({ key, value }) => { - const [isValid, errorMessage] = isValidKey(key) - invariant(isValid, errorMessage) + invariant(... isValidKey(key)); if (key.startsWith(PREFIX_PRIVATE)) { // Private field result[key.slice(PREFIX_PRIVATE.length)] = value From c65d1afd1e0c78e9560428e95d10d803f108216b Mon Sep 17 00:00:00 2001 From: souvlakia Date: Mon, 21 Jul 2025 22:11:34 +0300 Subject: [PATCH 3/4] add tests --- test/unserialize-test.ts | Bin 5018 -> 6492 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/unserialize-test.ts b/test/unserialize-test.ts index b1f7c696160cbad6e29293cffb9abb5dbbd968d4..1a9d0408a5f0a80dc1ecad29db35d23658c33cdd 100644 GIT binary patch delta 1379 zcmb7^&u$Yj5XLV^pp+^HBqUCZRtRUMG$kn)9JL36A`pLiLFzHu?7E3zYe%-XrL0!_ z5PRb(kaz%Ig98u4*xn>t644MvQFd(4eBW=z`;Gh_d_UN0Cz5Fp(m)i)Y+^#a+Lt!1#v0N}XhP-IRjNfL=*z1wd;!2hV8yk=Md&ZJ5cQvJO1v@~T>!UJ%?y>w&l9kr&%QlO2>5;U6>5veW Date: Tue, 22 Jul 2025 12:11:44 +0300 Subject: [PATCH 4/4] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e0eb43..edc57fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +### Upcoming + +- Unserialize bug/security fixes: (by @souvlakias) + - Ensures object keys are either `number` or `string`, and not equal to `__proto__`. + - Ensures serializable classes are not unserialized as `O:notserializable-class`. + ### 5.1.3 - Maintenance release with updated homepage in manifest