Я хочу создать изолированную программную среду <iframe>
(frame1), чтобы она могла запускать сценарии, но не имела доступа к своему родительскому элементу. Я также хочу, чтобы фрейм1 мог создавать внутренний изолированный iframe (frame2), который не может запускать какой-либо JavaScript. И я хочу, чтобы кадр 1 по-прежнему мог манипулировать кадром 2.
Я знаю, что, установив sandbox = "allow-same-origin"
для фрейма 1, я могу получить доступ к фрейму 2, но тогда он также сможет получить доступ к своему родительскому элементу.
Пробуя множество комбинаций между различными значениями sandbox
и способами установки источников элементов <iframe>
, я столкнулся с двумя основными сообщениями об ошибках:
Вот фрагмент кода, который я использую для проверки различных возможностей:
var inner = [0,0,0,0]
var outer = [0,0,0]
var focus = null
document.body.innerHTML = [
[0,0],[0,1],[0,2],
[1,0],[1,1],[1,2],
[2,0],[2,1],[2,2],
[3,0],[3,1],[3,2],
].map(([i,o]) => `<button>${i}-${o}</button>`).join('')
const container = document.createElement('div')
const buttons = [...document.body.children]
document.body.append(container)
buttons.map(btn => { btn.onclick = reload })
function reload () {
if (focus) focus.style = ''
const btn = focus = this
btn.style = 'background-color: pink;'
const text = btn.textContent
const [I,O] = text.split('-').map(Number)
inner = inner.map((x, i) => i === I)
outer = outer.map((x, o) => o === O)
container.replaceChildren()
spawn(container)
}
// -----------------------------------------------
async function spawn (element) {
const program = 'test'
const htmlsrc = `<!DOCTYPE html>
<html><head><meta charset = "utf-8"></head><body>333</body></html>`
const blobsrc = new Blob([htmlsrc], { type: "text/html" })
const hrefsrc = URL.createObjectURL(blobsrc)
const datauri = `data:text/html;charset=utf-8,${htmlsrc}`
const src_js = `
const iframe = document.createElement('iframe')
iframe.setAttribute('sandbox', 'allow-same-origin')
const html = \`<!DOCTYPE html>
<html><head><meta charset = "utf-8"></head><body>333</body></html>\`
const blob = new Blob([html], { type: "text/html" })
const href = URL.createObjectURL(blob)
const href2 = "${hrefsrc}"
const href3 = \`${datauri}\`
console.info({ href: href })
console.info({ href2: href2 })
console.info({ href3: href3 })
console.info({ lhref: location.href })
// not allowed to load resource
if (${inner[0]}) iframe.setAttribute('src', href2)
// no cross origin access:
if (${inner[1]}) iframe.setAttribute('src', href)
if (${inner[2]}) iframe.setAttribute('srcdoc', html)
if (${inner[3]}) iframe.setAttribute('src', href3)
iframe.onload = () => {
console.info("readonly iframe loaded")
const innerDoc = iframe.contentWindow.document
console.info(innerDoc.body.innerHTML)
}
document.body.appendChild(iframe)
`
const string = src_js
const sandbox = 'allow-scripts'
const html = index_html(wrap(string, program))
const src = _2href(_2blob(html), `#${program}`)
const srcuri = `data:text/html;base64,${btoa(html)}`
if (outer[0]) {
const { global, data, port } = await iframer(element, { srcdoc:html, sandbox })
}
if (outer[1]) {
const { global, data, port } = await iframer(element, { src: srcuri, sandbox })
}
if (outer[2]) {
const { global, data, port } = await iframer(element, { src, sandbox })
}
}
// ----------------------------------------------------------------------
function _2blob (html) { return new Blob([html], { type: "text/html" }) }
function _2href (blob, query = '') { return URL.createObjectURL(blob) + query }
function index_html (source) {
return `<!DOCTYPE html>
<html>
<head><meta charset = "utf-8"></head>
<body>${source}</body>
</html>`
}
function wrap (source_js, program) {
const filepath = `${program || '(anonymous)'}.js`
const bootloader_js = `;(async element => {
// document.currentScript.remove()
const source = element.textContent
element.remove()
console.info('load: ' + '${program}')
eval(source)
//# sourceURL=(iojs:bootloader)
//# ignoreList=(iojs:bootloader)
})(document.querySelector('[type = "text"]'))`
source_js = `;(async () => {
console.info('run: ' + "${filepath}");${source_js}
//# sourceURL=${filepath}
//# ignoreList=${filepath}
})()`
return `
<script type = "text">${source_js}</`+`script>
<script>${bootloader_js}</`+`script>
`
}
// ----------------------------------------------------------------------
function iframer (element, { src, srcdoc, sandbox = '', timeout } = {}) {
const el = document.createElement('div')
const sh = el.attachShadow({ mode: 'closed' })
sh.innerHTML = `<iframe sandbox = "${sandbox}"></iframe>`
const [iframe] = sh.children
const { promise, resolve, reject } = Promise.withResolvers()
iframe.onload = onload
window.addEventListener('message', onmessage)
const id = timeout !== undefined ? setTimeout(ontimeout, timeout) : null
if (src) iframe.src = src
else if (srcdoc) iframe.srcdoc = srcdoc
element.append(el)
return promise
function ontimeout () {
window.removeEventListener('message', onmessage)
iframe.onload = undefined
reject(new Error('iframe timeout'))
}
function onload () {
clearTimeout(id)
iframe.onload = undefined
window.removeEventListener('message', onmessage)
resolve({ global: iframe.contentWindow, data: null, port: null })
}
function onmessage (event) {
const { source, data = null, ports: [port = null] } = event
if (source === iframe.contentWindow) {
clearTimeout(id)
iframe.onload = undefined
window.removeEventListener('message', onmessage)
const sorigin = sandbox.includes('allow-same-origin')
resolve({ global: sorigin ? iframe.contentWindow : null, data, port })
}
}
}
Все, что я пробовал, можно найти в этом коде
@MisterJojo Я еще не читал вопрос, но этот комментарий звучит… просто неправильно. parent
и top
должны быть какими тогда?
@Kaiko, нет, они просто полезны для установления связи между родительской страницей и iFrame, это не отношения управления. см. Developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
Верхнее окно может создавать iframe с помощью sandbox = "allow-same-origin" и иметь доступ к iframe.contentWindow.document. Я хотел бы создать iframe, который может делать то же самое, но этот iframe не должен иметь возможности доступ к верхнему окну. По сути, я прошу iframe, который ведет себя как верхнее окно в отношении встроенного дочернего элемента без скрипта iframe.
Для простоты я назову верхний документ top, первый кадр — «frame1», а внутренний — «frame2».
Я не уверен на 100%, что именно происходит, но, судя по всему, я бы сказал, что атрибут sandbox
в кадре 1 придает ему собственное происхождение, однако подресурсы, которые он загружает, все равно будут совпадать. происхождение верхней-самой судоходной. Таким образом, фрейм2 и верх имеют одинаковое происхождение, но фрейм1 имеет собственное непрозрачное происхождение. И это будет сохраняться независимо от того, как вы загружаете кадр 2.
Единственный хак, который я сейчас могу придумать, — это использовать data:
URL1 для загрузки кадра 1 и не использовать атрибут sandbox
в его <iframe>
. Скрипты в кадре 1 по-прежнему не смогут получить доступ к началу, поскольку URL-адреса data:
также являются непрозрачными URL-адресами. Однако начало координат srcdoc
будет основано на их непрозрачном начале, а не на верхнем, поэтому мы можем загрузить кадр 2 из кадра 1 в srcdoc
с помощью sandbox = "allow-same-origin"
, чтобы кадр 1 мог получить доступ к кадру 2, но кадр 1 не мог получить доступ к верхнему, а кадр 2 не мог. выполнять скрипты.
Но это означает, что вам нужно получить содержимое как фрейма 1, чтобы создать его URL-версию data:
, так и фрейма 2, чтобы можно было установить его как srcdoc
. При этом помните, что фрейм1 теперь находится в непрозрачном источнике, и, следовательно, чтобы иметь возможность получать ресурсы на вашем сервере, вам нужно будет передать заголовок Access-Control-Allow-Origin: *
, что вам может не понадобиться. Поэтому вместо этого вам, возможно, придется получить оба документа сверху.
Это дало бы,
фрейм1.html
<h1>I am frame 1</h1>
<!-- Below src will be replaced by the top window -->
<iframe src = "frame2.html" sandbox = "allow-same-origin"></iframe>
<script>
const frame = document.querySelector("iframe");
onload = e => frame.contentDocument.body.append("frame 1 can access frame 2");
parent.document.body.append("Oops frame 1 can access parent"); // cross-origin, won't happen
</script>
фрейм2.html
<h1>I am frame 2</h1>
<script>
// Shouldn't happen
alert("OoPs, running script");
</script>
top.html
<iframe height=300 sandbox = "allow-same-origin allow-scripts"></iframe>
<script type=module>
const frame1Content = await fetch("./frame1.html").then(resp => resp.text());
const frame2Content = await fetch("./frame2.html").then(resp => resp.text());
const frame = document.querySelector("iframe");
// We can't have `"` in the content since we use it for the attribute
const cleanFrame2Content = frame2Content.replace(/"/g, "\\\"");
// Replace the src attribute by the actual content of frame2.html
const cleanFrame1Content = frame1Content.replace(`src = "frame2.html"`, `srcdoc = "${cleanFrame2Content}"`);
frame.src = `data:text/html,${encodeURIComponent(cleanFrame1Content)}`
</script>
Как JSFiddle, поскольку iframe StackSnippet с нулевым происхождением полностью блокирует любые подкадры для доступа к другим кадрам.
1. Using another domain would also achieve the same, it's basically what services like JSFiddle do to protect their main document but still allow their user embed same-origin iframes.
вау, ты это убиваешь :-) ИМЕННО этого я и добивался. Великолепно. Интересно, что песочница дает непрозрачный источник, который не работает с дочерним iframe srcdoc, но datauri дает непрозрачный источник, который работает, поэтому datauri больше похож на реальный источник на основе другого домена. Теперь мне нужно больше узнать о датаури, но вы помогли решить мою проблему. ура! с ума сошел :-)
iFrame похож на независимую HTML-страницу, но расположен внутри периметра вашей HTML-страницы. По определению, ни одна из HTML-страниц, открытых в одном браузере, не может осуществлять ни малейшего контроля над другой, даже если эта страница находится в iframe другой.