fix(js): do not redefine img src when a page made it non-configurable (#418)
Booking.com's anti-bot script pre-defines a non-configurable own `src` on <img> elements. The Image shim's load emulation then threw "TypeError: Cannot redefine property: src" out of the constructor, crashing any page that calls new Image() after that instrumentation runs. Check the own descriptor first and skip the load emulation when `src` is already non-configurable: a page that owns `src` is instrumenting loads itself. The normal path (configurable src, emulated load event) is unchanged and now covered by a test. Fixes the crash half of #394.
This commit is contained in:
1 parent
113b492ed5
commit
891d850d19
2 files changed
+109
No files matched your search
Vendored
+7
@@ -7398,6 +7398,12 @@ if (typeof Image === 'undefined') {
|
|||||||
// `.src` flips `complete` and fires `load` on a microtask-later tick. Lazy
|
// `.src` flips `complete` and fires `load` on a microtask-later tick. Lazy
|
||||||
// loaders and preloaders that create `new Image()`, set `.src`, and wait for
|
// loaders and preloaders that create `new Image()`, set `.src`, and wait for
|
||||||
// `onload` (or addEventListener('load')) would hang forever otherwise.
|
// `onload` (or addEventListener('load')) would hang forever otherwise.
|
||||||
|
// Anti-bot scripts (Booking.com, issue #394) pre-define a non-configurable
|
||||||
|
// own `src` on <img> elements; redefining it throws "Cannot redefine
|
||||||
|
// property: src" and kills the constructor. Skip the load emulation then:
|
||||||
|
// a page that owns `src` is instrumenting loads itself.
|
||||||
|
const ownSrc = Object.getOwnPropertyDescriptor(img, 'src');
|
||||||
|
if (!ownSrc || ownSrc.configurable) {
|
||||||
Object.defineProperty(img, 'src', {
|
Object.defineProperty(img, 'src', {
|
||||||
configurable: true, enumerable: true,
|
configurable: true, enumerable: true,
|
||||||
get() { return _imgSrcDesc.get.call(img); },
|
get() { return _imgSrcDesc.get.call(img); },
|
||||||
@@ -7413,6 +7419,7 @@ if (typeof Image === 'undefined') {
|
|||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return img;
|
return img;
|
||||||
};
|
};
|
||||||
globalThis.Image.prototype = globalThis.HTMLImageElement.prototype;
|
globalThis.Image.prototype = globalThis.HTMLImageElement.prototype;
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
//! Regression test for issue #394 (crash half): `new Image()` must survive a
|
||||||
|
//! page that pre-defines a non-configurable own `src` on `<img>` elements, the
|
||||||
|
//! way Booking.com's anti-bot instrumentation does. The Image shim used to
|
||||||
|
//! unconditionally redefine `src` on the element it just created, throwing
|
||||||
|
//! `TypeError: Cannot redefine property: src`.
|
||||||
|
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use obscura::Browser;
|
||||||
|
|
||||||
|
/// Minimal HTTP/1.1 server returning HTML that wraps document.createElement to
|
||||||
|
/// plant a non-configurable `src` on every <img>, then calls `new Image()`.
|
||||||
|
fn spawn_server(html: &'static str) -> String {
|
||||||
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
for incoming in listener.incoming() {
|
||||||
|
let mut s = match incoming {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let mut buf = [0u8; 2048];
|
||||||
|
let _ = s.read(&mut buf);
|
||||||
|
let resp = format!(
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||||
|
html.len(),
|
||||||
|
html
|
||||||
|
);
|
||||||
|
let _ = s.write_all(resp.as_bytes());
|
||||||
|
let _ = s.shutdown(std::net::Shutdown::Both);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
format!("http://{}", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn new_image_survives_non_configurable_src() {
|
||||||
|
std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1");
|
||||||
|
let base = spawn_server(
|
||||||
|
r#"<!doctype html><html><body><div id="r">waiting</div>
|
||||||
|
<script>
|
||||||
|
var origCreate = document.createElement.bind(document);
|
||||||
|
document.createElement = function (tag) {
|
||||||
|
var el = origCreate(tag);
|
||||||
|
if (String(tag).toLowerCase() === 'img') {
|
||||||
|
Object.defineProperty(el, 'src', { value: '', writable: true, configurable: false });
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
var img = new Image(10, 20);
|
||||||
|
document.getElementById('r').textContent =
|
||||||
|
'survived w=' + img.width + ' h=' + img.height;
|
||||||
|
</script>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let browser = Browser::new().unwrap();
|
||||||
|
let mut page = browser.new_page().await.unwrap();
|
||||||
|
page.goto(&base).await.unwrap();
|
||||||
|
|
||||||
|
let text = page.evaluate("document.getElementById('r').textContent");
|
||||||
|
assert_eq!(
|
||||||
|
text.as_str().unwrap_or(""),
|
||||||
|
"survived w=10 h=20",
|
||||||
|
"new Image() threw instead of degrading when src is non-configurable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn new_image_still_emulates_load_when_src_is_configurable() {
|
||||||
|
std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1");
|
||||||
|
let base = spawn_server(
|
||||||
|
r#"<!doctype html><html><body><div id="r">waiting</div>
|
||||||
|
<script>
|
||||||
|
var img = new Image();
|
||||||
|
img.onload = function () {
|
||||||
|
document.getElementById('r').textContent = 'loaded complete=' + img.complete;
|
||||||
|
};
|
||||||
|
img.src = '/pixel.png';
|
||||||
|
</script>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let browser = Browser::new().unwrap();
|
||||||
|
let mut page = browser.new_page().await.unwrap();
|
||||||
|
page.goto(&base).await.unwrap();
|
||||||
|
// The shim fires `load` on a setTimeout(0); pump the event loop.
|
||||||
|
for _ in 0..10 {
|
||||||
|
page.settle(500).await;
|
||||||
|
let text = page.evaluate("document.getElementById('r').textContent");
|
||||||
|
if text.as_str().unwrap_or("").starts_with("loaded") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = page.evaluate("document.getElementById('r').textContent");
|
||||||
|
assert_eq!(
|
||||||
|
text.as_str().unwrap_or(""),
|
||||||
|
"loaded complete=true",
|
||||||
|
"load emulation regressed for the normal (configurable src) path"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user