Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Electron should be able to load local resources with enabled webSecurity #23393

Closed
3 tasks done
aleksey-hoffman opened this issue May 3, 2020 · 8 comments
Closed
3 tasks done

Comments

@aleksey-hoffman
Copy link

aleksey-hoffman commented May 3, 2020

Preflight Checklist

  • I have read the Contributing Guidelines for this project.
  • I agree to follow the Code of Conduct that this project adheres to.
  • I have searched the issue tracker for an issue that matches the one I want to file, without success.

Issue Details

An Electron app can only display local images (located on the computer) with webSecurity: false:

Renderer process

<img src="file://C:/test.jpg">

Main process

mainWindow = new BrowserWindow({
  ...
  webPreferences: {
    webSecurity: false
  }
}

It doesn't make sense to completely disable all security features just to be able to load local images / videos, etc. There's should be a property that allows the app to load local resources without disabling all security features.

  • Electron Version: All

  • Operating System: All

Expected Behavior

Electron should be able to load local resources without disabling all security features.

Actual Behavior

Electron cannot load local resources without disabling all security features.

To Reproduce

Create an app with https://github.com/nklayman/vue-cli-plugin-electron-builder

vue create my-app
cd my-app
vue add electron-builder
yarn

Then add an image, e.g. in App.vue

<img src="file://C:/test.png">
yarn electron:build

Open /dist_electron and install the app. Then open the console and see the error:

image

@MarshallOfSound
Copy link
Member

Electron should be able to load local resources without disabling all security features.

Strong disagree, loading arbitrary local resources from a remote endpoint is a massive security violation and you should be aware of what you are doing if you intend for that to be possible.

The correct way to handle this kind of thing is to expose a custom protocol handler app:// that can serve specific files from local disk for your remote endpoint to use.

Docs: https://www.electronjs.org/docs/api/protocol#protocolregisterfileprotocolscheme-handler-completion

@aleksey-hoffman
Copy link
Author

aleksey-hoffman commented May 4, 2020

@MarshallOfSound sorry but that link doesn't help at all. There's no examples anywhere.

Please, help us to figuire this out. What do you mean by specific files? Let's say I'm building a "Media player" app or an "Image viewer app" that dispalys all images on all local disks. How is the app supposed to do that?

The electron-builder docs says that it already uses app:// protocol by default, so why do I get the error mentioned in the issue?
image

So if the app:// protocol is already registered, I should be able to change file:// to app:// and all renderer windows will display that local image without throwing the error?:

<img src="app://C:/test.jpg">

That doesn't work.

Is it because I didn't explicitly allow the app to serve specific file called C:/test.jpg? But that's impossible since you don't know file names in advance, you are getting them at runtime with something like fs.readdir.

Are you saying that it's actually impossible to display arbitrary local images with webSecurity: true ?

@MarshallOfSound
Copy link
Member

@aleksey-hoffman I strongly recommend you read the documentation on that protocol module I linked to fully understand what it does. It probably doesn't help that electron-builder has done this magically for you.

The electron-builder docs says that it already uses app:// protocol by default, so why do I get the error mentioned in the issue?

This is good from a security perspective but not from an developer-understanding perspective

So if the app:// protocol is already registered, I should be able to change file:// to app:// and all renderer windows will display that local image now without throwing the error?:

No, see above, read the documentation on the protocol module. app doesn't just "become" the file protocol that would defeat the purpose of registering a separate protocol. Each custom protocol has it's own handler that will serve up file content depending on the incoming request. My guess is the app protocol that electron-builder auto-registers is strongly scoped to your apps directory not the entire hard drive.

Is it because I didn't explicitly allow the app to serve specific file called C:/test.jpg? But that's impossible since you don't know file names in advance, you are getting them at runtime with something like fs.readdir.

Kind of, see above answer RE each protocol having a handler which decides this kind of thing

Are you saying that it's impossible to display arbitrary local images with webSecurity: true ?

No, I explicitly told you how to do it without turning off webSecurity.

To do this securely you want to do some kind of routing thing, pseudo code included below. This code is not supposed to run.

const files = {};
registerFileProtocol('my-magic-protocol', (req, cb) => {
  const fileId = req.path;
  if (files[fileId]) return cb(files[fileId]);
  return cb(404);
});

ipcMain.handle('open-file', () => {
  const filePath = await dialog.showOpenDialog();
  const id = randomID();
  files[id] = filePath;
  return id;
});

// In Renderer
openFileButton.addEventListener('click', () => {
  const id = await ipcRenderer.invoke('open-file');
  mediaElem.src = `my-magic-protocol://anystringhere/${id}`;
});

@aleksey-hoffman
Copy link
Author

aleksey-hoffman commented May 4, 2020

@MarshallOfSound thank you very much for taking time and claifying it for me and especially for the example. I think I get it now. For someone who never worked with custom protocols, there was just too many layers of abstraction there to be able to understand how it all works.

I did what you told me, and finally managed to make it work with webSecurity: true.

I didn't find a single example on the internet on how to do this properly, every answer I ever found suggested to disable webSecurity. So I just want to leave my code here in case it helps someone in the future.

Please tell me if I'm doing something wrong here:

Step 1: Create custom protocol

Main process:

app.on('ready', async () => {
  protocol.registerFileProtocol('my-magic-protocol', (request, callback) => {
    const url = request.url.replace('my-magic-protocol://getMediaFile/', '')
    try {
      return callback(url)
    }
    catch (error) {
      console.error(error)
      return callback(404)
    }
  })
  ...

Step 2: get media file using the protocol:

Renderer process | Home.vue

<img class="file-thumb" data-path="C:/test.jpg">
function setImageSrc () {
  const thumbHTMLElements = document.querySelectorAll('.file-thumb')
  thumbHTMLElements.forEach(element => {
    const path = element.dataset.path
    element.src = `my-magic-protocol://getMediaFile/${element.dataset.path}`
  })
}

That's it. The image will be load even with webSecurity: true.


@MarshallOfSound Could you please clarify 1 more thing, why do I need to put that anystringhere after the protocol (in your example)? The following format works for me just fine:
my-magic-protocol://${path}.

I would imagine that anystringhere is used for if conditions? For something like this:

protocol.registerFileProtocol('my-magic-protocol', (request, callback) => {
    if (request.url.includes('my-magic-protocol://OPEN_PREVIEW') {
       ...
    }
    else if (request.url.includes('my-magic-protocol://OPEN_IN_NEW_WINDOW') {
       ...
    }
})
app.setAsDefaultProtocolClient('my-magic-protocol')

And then:

In a browser:
my-magic-protocol://OPEN_PREVIEW/path

Is that what it's for? So you could open some file in the app from a URL in a specific way?

@cloverich
Copy link

@aleksey-hoffman hazarding a guess for the anystringhere -- you might want to whitelist certain directories for explicit access. Having the custom protocol accept ${path} -- i.e. any location on the hard-drive, is a potential security risk. So he may have been implying you should have your backend process validate the location is allowable before proceeding to load the file.

Thank you for including your example btw, very helpful.

@aleksey-hoffman
Copy link
Author

@cloverich yeah, I'm not sure. I'm using this in my file manger app, so I cannot define an allowlist, it has to load any specified image on the user's drive. I'm not sure how I would check whether a path is safe or not

@Haaxor1689
Copy link

protocol.registerFileProtocol seems to be deprecated, what is the correct approach now?

@dev2820
Copy link

dev2820 commented Feb 11, 2024

@Haaxor1689

use protocol.handle and protocol.registerSchemesAsPrivileged(customSchemes) instead,

docs: https://www.electronjs.org/docs/latest/api/protocol#protocolregisterschemesasprivilegedcustomschemes

/**
 * First, you need to register your scheme before the app starts.
 */
protocol.registerSchemesAsPrivileged([
  {
    scheme: 'media',
    privileges: {
      secure: true,
      supportFetchAPI: true,
      bypassCSP: true
    }
  }
]);

// ...
app.whenReady().then(() => {
  // ...
  /**
   * If a request is made over the media protocol, you can hook it here. 
   * In my case, I'm creating a new request by switching to the file protocol to call a local file.    
   */
  protocol.handle('media', (req) => { 
    const pathToMedia = new URL(req.url).pathname;
    return net.fetch(`file://${pathToMedia}`);
  });
  // ...
});

result)
스크린샷 2024-02-11 오후 8 11 00

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants