Skip to content

Commit 824b295

Browse files
gr2mclaude
andauthored
fix(provider-utils): prevent unicode escape bypass in secureJsonParse (#13079)
## Summary - Update `secureJsonParse` regex patterns to detect unicode-escaped variants of `__proto__` and `constructor` keys, preventing prototype pollution bypass - Aligns with the upstream fix in [fastify/secure-json-parse](https://github.com/fastify/secure-json-parse) - Add regression tests for partial and fully unicode-escaped key variants ## Security Resolves VULN-774. The previous regex fast-path only matched literal `"__proto__"` and `"constructor"` strings. Unicode escapes (e.g., `\u005f\u005fproto__`) bypassed the regex gate, skipping the `filter()` safety check, while `JSON.parse` still normalized them into dangerous keys. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7579667 commit 824b295

File tree

3 files changed

+46
-2
lines changed

3 files changed

+46
-2
lines changed

.changeset/silver-moles-roll.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/provider-utils': patch
3+
---
4+
5+
fix(provider-utils): prevent unicode escape bypass in secureJsonParse

packages/provider-utils/src/secure-json-parse.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ describe('secureJsonParse', () => {
4343
expect(secureJsonParse('"X"')).toStrictEqual(JSON.parse('"X"'));
4444
});
4545

46+
it('allows constructor property with non-object value', () => {
47+
expect(secureJsonParse('{ "constructor": "string value" }')).toStrictEqual({
48+
constructor: 'string value',
49+
});
50+
});
51+
52+
it('allows constructor property with null value', () => {
53+
expect(secureJsonParse('{ "constructor": null }')).toStrictEqual({
54+
constructor: null,
55+
});
56+
});
57+
4658
it('errors on constructor property', () => {
4759
const text =
4860
'{ "a": 5, "b": 6, "constructor": { "x": 7 }, "c": { "d": 0, "e": "text", "__proto__": { "y": 8 }, "f": { "g": 2 } } }';
@@ -56,4 +68,27 @@ describe('secureJsonParse', () => {
5668

5769
expect(() => secureJsonParse(text)).toThrow(SyntaxError);
5870
});
71+
72+
it('errors on unicode-escaped __proto__ property', () => {
73+
const text = '{ "\\u005f\\u005fproto__": { "isAdmin": true } }';
74+
expect(() => secureJsonParse(text)).toThrow(SyntaxError);
75+
});
76+
77+
it('errors on fully unicode-escaped __proto__ property', () => {
78+
const text =
79+
'{ "\\u005f\\u005f\\u0070\\u0072\\u006f\\u0074\\u006f\\u005f\\u005f": { "isAdmin": true } }';
80+
expect(() => secureJsonParse(text)).toThrow(SyntaxError);
81+
});
82+
83+
it('errors on unicode-escaped constructor property', () => {
84+
const text =
85+
'{ "\\u0063\\u006fnstructor": { "prototype": { "isAdmin": true } } }';
86+
expect(() => secureJsonParse(text)).toThrow(SyntaxError);
87+
});
88+
89+
it('errors on fully unicode-escaped constructor property', () => {
90+
const text =
91+
'{ "\\u0063\\u006f\\u006e\\u0073\\u0074\\u0072\\u0075\\u0063\\u0074\\u006f\\u0072": { "prototype": { "isAdmin": true } } }';
92+
expect(() => secureJsonParse(text)).toThrow(SyntaxError);
93+
});
5994
});

packages/provider-utils/src/secure-json-parse.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
//
2222
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2323

24-
const suspectProtoRx = /"__proto__"\s*:/;
25-
const suspectConstructorRx = /"constructor"\s*:/;
24+
const suspectProtoRx =
25+
/"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*:/;
26+
const suspectConstructorRx =
27+
/"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;
2628

2729
function _parse(text: string) {
2830
// Parse normally
@@ -58,6 +60,8 @@ function filter(obj: any) {
5860

5961
if (
6062
Object.prototype.hasOwnProperty.call(node, 'constructor') &&
63+
node.constructor !== null &&
64+
typeof node.constructor === 'object' &&
6165
Object.prototype.hasOwnProperty.call(node.constructor, 'prototype')
6266
) {
6367
throw new SyntaxError('Object contains forbidden prototype property');

0 commit comments

Comments
 (0)