Skip to content

Instantly share code, notes, and snippets.

@XieJiSS
Last active August 16, 2023 09:09
Show Gist options
  • Save XieJiSS/470b405869692f85972ee14f11d28133 to your computer and use it in GitHub Desktop.
Save XieJiSS/470b405869692f85972ee14f11d28133 to your computer and use it in GitHub Desktop.
MWE: Electron's Protocol Interceptor Likes Eating Cookies. Requires `set-cookie-parser`
<html>
<head>
<script>
window.location.replace("https://dash.cloudflare.com/")
</script>
</head>
<body></body>
</html>
// 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()
})
{
"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"
}
}
/**
* 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 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