CVE-2022-22620: Use-after-free in Safari
Maddie Stone
The Basics
Disclosure or Patch Date: 10 February 2022
Product: Apple Safari/WebKit
Advisory: https://support.apple.com/en-us/HT213093
Affected Versions: Safari 15.3, iOS 15.3, macOS 12.2 and earlier
First Patched Version: Safari 15.3 (v. 16612.4.9.1.8 and 15612.4.9.1.8), iOS 15.3.1, macOS 12.2.1
Issue/Bug Report: https://bugs.webkit.org/show_bug.cgi?id=235551
Patch CL: https://github.com/WebKit/WebKit/commit/486816dc355c19f1de1b8056f85d0bbf7084dd6e
Bug-Introducing CL: https://github.com/WebKit/WebKit/commit/aa31b6b4d09b09acdf1cec11f2f7f35bd362dd0e
Reporter(s): Anonymous
The Code
Proof-of-concept:
input = document.body.appendChild(document.createElement("input"));
foo = document.body.appendChild(document.createElement("a"));
foo.id = "foo";
// Go to state1 when history.back is called
// The URL needs to be <currentPage+hash> to trigger loadInSameDocument during the call to back()
// Since the foo's element id="foo", focus will change to that element
history.pushState("state1", "", location + "#foo");
// Current state = state2
history.pushState("state2", "");
setTimeout(() => {
// Set the focus on the input element.
// During the call to back() the focus will change to the foo element
// and therefore triggering the blur event on the input element
input.focus();
input.onblur = () => history.replaceState("state3", "");
setTimeout(() => history.back(), 1000);
}, 1000);
Exploit sample: N/A
Did you have access to the exploit sample when doing the analysis? No
The Vulnerability
Bug class: Use-after-free
Vulnerability details:
The History API allows access to (and modification of) a stack of the pages visited in the current frame, and these page states are stored as a SerializedScriptValue
. The History API exposes a getter for state
, and a method replaceState
which allows overwriting the "most recent" history entry.
The bug is that FrameLoader::loadInSameDocument
takes the state
as an argument (stateObject
), but doesn't increase its reference count. Only a HistoryItem
object holds a reference to the stateObject
. loadInSameDocument
can trigger a callback into user JavaScript through the onblur
event. The user's callback can call replaceState
to replace the HistoryItem
's state
with a new object, therefore dropping the only reference to the stateObject
. When the callback returns, loadInSameDocument
will still use this free'd object in its call to statePopped
, leading to the use-after-free.
When loadInSameDocument
is called it changes the focus to the element its scrolling to. If we set the focus on a different element prior to loadInSameDocument
running, the blur
event will be fired on that element. Then we can free the stateObject
by calling replaceState
in the onblur
event handler.
Patch analysis:
The patch changes the stateObject
argument to loadInSameDocument
from a raw pointer, SerializedScriptValue*
, to a reference-counted pointer, RefPtr<SerializedScriptValue>
, so that loadInSameDocument
now increments the reference count on the object.
Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.):
It seems reasonable that the vulnerability could have been found through watching the commits and seeing the initial fix from 2013 reverted in 2016, code auditing, or fuzzing. Fuzzing seems slightly less likely due to needing to support "navigation" which many fuzzers explicitly try to exclude.
(Historical/present/future) context of bug:
This bug was actually reported and initially fixed in 2013. In 2016 the fix was regressed during (it seems) refactoring. A full write-up is available here.
The Exploit
(The terms exploit primitive, exploit strategy, exploit technique, and exploit flow are defined here.)
Exploit strategy (or strategies): N/A, no access to exploit sample
Exploit flow:
Known cases of the same exploit flow:
Part of an exploit chain?
The Next Steps
Variant analysis
Areas/approach for variant analysis (and why):
- Look for any commits where a reference-counted pointer was changed to a raw pointer.
- Update fuzzer so that it could find this bug and therefore also hopefully provide coverage for other similar bugs too.
Found variants: N/A
Structural improvements
What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.?
Ideas to kill the bug class:
By default, arguments to functions should be reference-counted. Raw pointers should only be used in rare exceptions.
Ideas to mitigate the exploit flow: N/A
Other potential improvements:
- The bug was killed in 2013 and re-introduced in 2016. It seems that this likely occured due to the large issues affecting most software dev teams: legacy code, short reviewer turn-around expectations, refactoring and security efforts are generally under-appreciated and under-rewarded, and lack of memory safety mitigations. Steps towards any of these would likely make a difference.
- The two commits that reverted the 2013 fix were very, very large commits: 40 and 94 files changed. While some large commits may include exclusively no-ops, these commits included many changes affecting lifetime semantics. This seems like it would make it very difficult for any developer or reviewer to be able to truly audit and understand the security impacts of all the changes being made.
0-day detection methods
What are potential detection methods for similar 0-days? Meaning are there any ideas of how this exploit or similar exploits could be detected as a 0-day?
The code to trigger this vulnerability is pretty generic. I think any detection would have to be around exploitation method, which we couldn't analyze in this case since we didn't have access to the exploit sample.
Other References
- "An Autopsy on a Zombie In-the-Wild 0-day" Project Zero blog post