Last active
August 16, 2023 09:09
-
-
Save XieJiSS/470b405869692f85972ee14f11d28133 to your computer and use it in GitHub Desktop.
MWE: Electron's Protocol Interceptor Likes Eating Cookies. Requires `set-cookie-parser`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<script> | |
window.location.replace("https://dash.cloudflare.com/") | |
</script> | |
</head> | |
<body></body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Modules to control application life and create native browser window | |
const { app, /* net, */ protocol, session, BrowserWindow } = require('electron') | |
const path = require('path') | |
const cookieParser = require("set-cookie-parser") | |
function createWindow() { | |
// Clear existing cookies, if any | |
session.defaultSession.clearStorageData({ storages: ["cookies"]}) | |
// Register cookies.on("changed") event handler. You can remove it. Removing it does not affect the bug. | |
session.defaultSession.cookies.on("changed", async (_, cookie, cause, removed) => { | |
console.log("[event] Cookie change event triggered:", cookie.name, cookie.value, cookie.domain, cause, removed) | |
const cookies = await session.defaultSession.cookies.get({}); | |
console.log("[event] cookies retrieved via electron api:", cookies.map(c => c.name)) | |
}) | |
// Create the browser window. | |
const mainWindow = new BrowserWindow({ | |
width: 800, | |
height: 600, | |
webPreferences: { | |
preload: path.join(__dirname, "preload.js"), | |
}, | |
}) | |
// set https interceptor. | |
if (!protocol.isProtocolHandled("https")) { | |
protocol.handle("https", async req => { | |
console.log("[intercept] intercepting", req.url.slice(0, 100)) | |
// BUG: THIS IS ALWAYS NEGATIVE, EVEN IF THERE ARE COOKIES MANUALLY SET BY JS (IN BROWSER) OR | |
// BY HAND (DEVTOOLS) | |
if(req.headers.has("Cookie") || req.headers.has("cookie")) { | |
// THIS WILL NEVER GET LOGGED | |
console.log("[outbound] cookie header sent:", req.headers.get("Cookie") || req.headers.get("cookie")) | |
} | |
// Get rid of Electron/x.x.x user agent. Some websites have anti-crawler | |
req.headers.set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36") | |
// If we use net.fetch, it (as I understood) invokes the `fetch` function inside the browser (renderer), | |
// which can handle `Set-Cookie:` header correctly (yeah, because it happens inside the browser!). That | |
// will hide the real bug lying inside the interceptor, so we will not use net.fetch here. | |
// const resp = await net.fetch(req, { | |
// bypassCustomProtocolHandlers: true, | |
// }) | |
try { | |
// Instead, we will use the Node.js built-in fetch (available since Node.js 18). Any fetch utility is | |
// ok, e.g. node-fetch, undici, whatever. And you can even call resp.headers.set("Set-Cookie", "a=b") | |
// manually, which is also not producing any effect. Note that the following lines are totally legit | |
// even if you type-check with TypeScript. And `fetch(req.url, req)` is also legit. | |
const resp = await fetch(req) | |
// BUG: THIS GETS LOGGED BUT electron.js DOES NOT RESPECT IT | |
if (resp.headers.has("Set-Cookie")) { | |
// For logging purpose, we have to do this to extract all Set-Cookie into an array. Using plain | |
// resp.headers.get("Set-Cookie") will give us a string, which is comma separated, but as cookie | |
// may also contains comma, we cannot parse it easily. (The Fetch API states that headers.get("") | |
// should always return a string, so for headers like Set-Cookie that may be presented multi-times, | |
// headers.get will return `[cookie1, cookie2].join(", ")`. This is SO MUCH PAIN) | |
// Just extract the original values from symbol attributes anyway, so that we can log them. | |
const sym = Object.getOwnPropertySymbols(resp.headers).find(s => s.description === "headers list") | |
const names = cookieParser(resp.headers[sym].cookies).map(c => c.name) | |
console.log("[inbound] Set-Cookie header:", names) | |
setTimeout(async () => { | |
const cookies = await session.defaultSession.cookies.get({}); | |
console.log(`[inbound] After Set-Cookie ${names}, cookies retrieved via electron api:`, cookies.map(c => c.name)) | |
}, 200); | |
} | |
return resp | |
} catch (e) { | |
console.error("[error]", req.url.slice(0, 100), e); | |
// Sometimes there are connection timeouts. We provide a naive 500 response as fallback | |
return new Response("500 Internal Server Error", { | |
status: 500, | |
}) | |
} | |
}) | |
} | |
// Load cloudflare dashboard. Of course, the server will give us some cookies! | |
// Due to the inability to set `Cookie:` header for outgoing requests, and the inability to set cookies | |
// corresponding to received `Set-Cookie:` headers, passing cloudflare's captcha test is really hard. And of | |
// course you cannot login to the dashboard. | |
mainWindow.loadFile('index.html') | |
// For debug. You can view cookie status in the Application Tab's storage area. Only cookies set by in-browser | |
// JS, e.g. document.cookie += "a=b" and fetch(url) will exist there. Also, although they can be set, they | |
// won't be present in outgoing requests' headers. | |
mainWindow.webContents.openDevTools(); | |
} | |
app.whenReady().then(() => { | |
createWindow() | |
app.on('activate', function () { | |
if (BrowserWindow.getAllWindows().length === 0) createWindow() | |
}) | |
}) | |
app.on('window-all-closed', function () { | |
if (process.platform !== 'darwin') app.quit() | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "nostalgic-ratio-desert-shjml", | |
"productName": "nostalgic-ratio-desert-shjml", | |
"description": "My Electron application description", | |
"keywords": [], | |
"main": "./main.js", | |
"version": "1.0.0", | |
"author": "xiejiss", | |
"scripts": { | |
"start": "electron ." | |
}, | |
"dependencies": { | |
"set-cookie-parser": "2.6.0", | |
"@types/set-cookie-parser": "2.4.2" | |
}, | |
"devDependencies": { | |
"electron": "25.5.0" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* The preload script runs before. It has access to web APIs | |
* as well as Electron's renderer process modules and some | |
* polyfilled Node.js functions. | |
* | |
* https://www.electronjs.org/docs/latest/tutorial/sandbox | |
*/ | |
window.addEventListener('DOMContentLoaded', () => { | |
if(window.location.protocol.startsWith("http")) { | |
console.log("After visiting this site, you got non-httpOnly cookies:", document.cookie); | |
} | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This file is loaded via the <script> tag in the index.html file and will | |
* be executed in the renderer process for that window. No Node.js APIs are | |
* available in this process because `nodeIntegration` is turned off and | |
* `contextIsolation` is turned on. Use the contextBridge API in `preload.js` | |
* to expose Node.js functionality from the main process. | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment