Как получить доступ к iframe.contentWindow.document вложенного iframe внутри изолированного iframe?

Я хочу создать изолированную программную среду <iframe> (frame1), чтобы она могла запускать сценарии, но не имела доступа к своему родительскому элементу. Я также хочу, чтобы фрейм1 мог создавать внутренний изолированный iframe (frame2), который не может запускать какой-либо JavaScript. И я хочу, чтобы кадр 1 по-прежнему мог манипулировать кадром 2.

Я знаю, что, установив sandbox = "allow-same-origin" для фрейма 1, я могу получить доступ к фрейму 2, но тогда он также сможет получить доступ к своему родительскому элементу.

Пробуя множество комбинаций между различными значениями sandbox и способами установки источников элементов <iframe>, я столкнулся с двумя основными сообщениями об ошибках:

  1. «Не разрешено загружать локальный ресурс: blob:...»
  2. «Неперехваченное исключение DOMException: не удалось прочитать именованное свойство «документ» из «Окна»: заблокирован доступ кадра с исходным значением «null» к кадру с перекрестным происхождением»

Вот фрагмент кода, который я использую для проверки различных возможностей:

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 })
    }
  }
}

Все, что я пробовал, можно найти в этом коде

iFrame похож на независимую HTML-страницу, но расположен внутри периметра вашей HTML-страницы. По определению, ни одна из HTML-страниц, открытых в одном браузере, не может осуществлять ни малейшего контроля над другой, даже если эта страница находится в iframe другой.

Mister Jojo 19.05.2024 05:50

@MisterJojo Я еще не читал вопрос, но этот комментарий звучит… просто неправильно. parent и top должны быть какими тогда?

Kaiido 19.05.2024 13:28

@Kaiko, нет, они просто полезны для установления связи между родительской страницей и iFrame, это не отношения управления. см. Developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

Mister Jojo 19.05.2024 13:42

Верхнее окно может создавать iframe с помощью sandbox = "allow-same-origin" и иметь доступ к iframe.contentWindow.document. Я хотел бы создать iframe, который может делать то же самое, но этот iframe не должен иметь возможности доступ к верхнему окну. По сути, я прошу iframe, который ведет себя как верхнее окно в отношении встроенного дочернего элемента без скрипта iframe.

serapath 20.05.2024 00:19
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
5
85
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Для простоты я назову верхний документ 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 больше похож на реальный источник на основе другого домена. Теперь мне нужно больше узнать о датаури, но вы помогли решить мою проблему. ура! с ума сошел :-)

serapath 20.05.2024 22:04

Другие вопросы по теме