U4-9086 - After changing configuration inside FileSystemProviders.config, it's impossibile to edit pre-existing files, I get exception "File is outside this filesystem's root"

Created by simone chiaretta 20 Oct 2016, 15:08:46 Updated by Alessandro Calzavara 06 Jun 2017, 10:28:46

I had an umbraco project with the default configuration <add key="virtualRoot" value="~/media/" /> I started using the site and adding some media files. Then I realized it was a better idea to store media files on a NAS as it would have been backed-up and not take up space on the application server. So I changed the configuration to be

      <add key="rootPath" value="\\ServerName\Storage\UmbracoMedia" />
      <add key="rootUrl" value="/UmbracoMedia" />

Now I cannot edit the pre-existing files anymore as I get the following error when pressing the save button:

An error occured

File '/media/1051/1010562_10151560546423881_2024154195_n.jpg' is outside this filesystem's root.

EXCEPTION DETAILS

Umbraco.Core.IO.FileSecurityException: File '/media/1051/1010562_10151560546423881_2024154195_n.jpg' is outside this filesystem's root.
STACKTRACE

at Umbraco.Core.IO.PhysicalFileSystem.GetFullPath(String path)
   at Umbraco.Web.MediaPropertyExtensions.PopulateFileMetaDataProperties(IContentBase content, IImagingAutoFillUploadField uploadFieldConfigNode, String relativeFilePath)
   at Umbraco.Web.PropertyEditors.ImageCropperPropertyEditor.AutoFillProperties(IContentBase model)
   at Umbraco.Web.PropertyEditors.ImageCropperPropertyEditor.MediaServiceSaving(IMediaService sender, SaveEventArgs`1 e)
   at Umbraco.Core.Events.TypedEventHandler`2.Invoke(TSender sender, TEventArgs e)
   at Umbraco.Core.Events.EventExtensions.IsRaisedEventCancelled[TSender,TArgs](TypedEventHandler`2 eventHandler, TArgs args, TSender sender)
   at Umbraco.Core.Services.MediaService.Umbraco.Core.Services.IMediaServiceOperations.Save(IMedia media, Int32 userId, Boolean raiseEvents)
   at Umbraco.Web.Editors.MediaController.PostSave(MediaItemSave contentItem)
   at lambda_method(Closure , Object , Object[] )
   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass10.<GetExecutor>b__9(Object instance, Object[] methodParameters)
   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments)
   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync(HttpControllerContext controllerContext, IDictionary`2 arguments, CancellationToken cancellationToken)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Controllers.ApiControllerActionInvoker.<InvokeActionAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.ActionFilterAttribute.<ExecuteActionFilterAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.ActionFilterAttribute.<ExecuteActionFilterAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.ActionFilterAttribute.<ExecuteActionFilterAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.ActionFilterAttribute.<ExecuteActionFilterAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.ActionFilterAttribute.<ExecuteActionFilterAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.ActionFilterAttribute.<ExecuteActionFilterAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.AuthorizationFilterAttribute.<ExecuteAuthorizationFilterAsyncCore>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.AuthorizationFilterAttribute.<ExecuteAuthorizationFilterAsyncCore>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.AuthorizationFilterAttribute.<ExecuteAuthorizationFilterAsyncCore>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()

The only way to fix the problem is by manually updating the DB tables and changing the url of the media file.

Not sure how this should be handled: I think all files should be moved to the new url automatically, not keeping pre-existing files on a URL and new files in another. This error happens even after moving the files manually to the new rootPath.

Comments

simone chiaretta 20 Oct 2016, 17:17:58

I think the exception is correctly thrown, but there must be an easier way to re-evaluate the url of media files after a change of location


Shannon Deminick 21 Oct 2016, 07:18:36

Hi Simone,

I think what you are trying to do is actually not possible. The default file system provider: https://github.com/umbraco/Umbraco-CMS/blob/dev-v7/src/Umbraco.Web.UI/config/FileSystemProviders.config is only meant to be used with paths that exist within the web application. This means that the URLs returned for media are the web file locations of where they exists. If you point this to somewhere outside of your web root, then there is no way that the URL will work - these are not proxied URLs, these are just the locations of your media on disk that are served by IIS.

The source for this is here: https://github.com/umbraco/Umbraco-CMS/blob/dev-v7/src/Umbraco.Core/IO/PhysicalFileSystem.cs

So even if you specify an absolute location, that location would still need to exist within your web root so that it can return a URL to fetch your media that IIS knows about.

If you want to continue down this path, you have 2 choices:

  • Change your IIS setup and just create a media virtual folder for your website that points to your NAS and revert your changes to the file system provider config and use the defaults
  • Create a new IFileSystem to deal with this, this would mean that the URL you return would need to be a proxy URL to return the actual media from outside of your web root.


simone chiaretta 21 Oct 2016, 07:32:05

Well.. actually, apart from the problem with the pre-existing files, specifying a folder on a NAS and have it served from another url works fine. What would be reason to have the additional 2 properties (rootPath and rootUrl) otherwise? I think it just needs better documentation :) I've sent a PR on the configuration for the filesystemprovider.config


Shannon Deminick 21 Oct 2016, 07:36:51

So if you have a rootPath outside of the web root, how is it serving media files via IIS? i.e. what is the URL it uses?


simone chiaretta 21 Oct 2016, 07:42:26

At the moment I tried with just another path (/UmbracoMedia) configured as virtual folder, but the way it works I suppose it can work also when specifying a complete different domain. I'll give it a try and let you know.


simone chiaretta 21 Oct 2016, 09:30:07

Just made the test:

  <Provider alias="media" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
    <Parameters>
      <add key="rootPath" value="C:\Temp\UmbracoMedia" />
      <add key="rootUrl" value="//media.whatever.com" />
    </Parameters>
  </Provider>

With http://media.whatever.com configured as separate website on top of C:\Temp\UmbracoMedia. Can add new files and edit them, and media files are served correctly from http://media.whatever.com/1058/summits.png

If I also go back to the DB and edit the path of pre-existing media, I can edit them without exception happening and they are displayed from the new absolute url

So everything works even when the path is outside of root of the website


Wael 07 Dec 2016, 20:21:43

In my scenario, I really want just bypass this exception and just replace the broken media file (upload field), so why not give us the ability to replace the broken media even if Umbraco can't delete the file.


Stephan 08 Dec 2016, 10:48:41

@simonech quick Q, when you "edit the path of pre-existing media" can you give me an example of what you changed exactly? The error ("outside of the filesystem root") indicates that the physical file system's root is eg "c:/foo/bar" and we are trying to read a file at eg "c:/some/place" - which should obviously be illegal for security reasons. But I'm not sure why changing the file system's root would case an issue.


simone chiaretta 08 Dec 2016, 11:08:11

@zpqrtbnk The error happens before I edit the path of pre-existing media. If I just change the config of the provider, the full relative path is stored in the imagesrc property, so, in my example, they where /media/... while the provider now points to /UmbracoMedia. If I manually update the property to reflect the new root, it works fine. Is that what you were asking?


Stephan 08 Dec 2016, 11:58:21

More or less ;-) The original property value would be "/media/1234/image.jpg" - what do you need to change this into so it works?


simone chiaretta 08 Dec 2016, 12:29:01

I had changed the config to

 <add key="rootPath" value="\\ServerName\Storage\UmbracoMedia" />
 <add key="rootUrl" value="/UmbracoMedia" />

So /media/1234/image.jpg had to become /UmbracoMedia/1234/image.jpg

Actually I didn't try setting rootUrl to media... could have worked indeed


Stephan 08 Dec 2016, 13:21:55

The change from "/media/1234/..." to "/UmbracoMedia/1234/..." was what I was looking for - now need to check but yes, since "/media" is there, it cannot go away so easily so maybe \Server\Storage\media and /media would do the trick.


simone chiaretta 08 Dec 2016, 13:53:16

If fixed it by just mapping the remote folder as virtual folder under IIS. Everything works fine.


Stephan 08 Mar 2017, 09:59:45

Root cause would be:

The MediaFileSystem handles virtual file paths eg "1234/image.jpg" and depending on how it is configured, it maps those virtual paths to a physical path eg "c:/path/to/root/media/1234/image.jpg" which is where the file is actually stored, and to a url eg "/media/1234/image.jpg". In ''theory'' changing the "/media" part in the url in config ''should'' work. However, what we store in eg property values is the full url "/media/1234/image.jpg" and not the virtual path "1234/image.jpg" -- meaning that if you change the configuration, the "/media" part is still there and breaks everything.

Ideally we should only store the virtual path in property values, but that's not going to change in the near future due to backward compatibility.

So... whenever someone ''changes'' the configuration ... either make sure the "/media" part does not change, or use a virtual folder in IIS, or update all the media paths in property values.


simone chiaretta 08 Mar 2017, 10:20:22

I had sent a PR to update the documentation, so now there is a note in the filesysemprovider doc page that explains what you need to do to avoid the issue :) https://our.umbraco.org/Documentation/Reference/Config/fileSystemProviders/


andrew shearer 06 Jun 2017, 06:03:29

hi simone - what version is your documentation compatible with? Should it work for 7.4.3? thanks


andrew shearer 06 Jun 2017, 06:09:53

is this earlier option mentioned by Shannon the simplest? thanks "Change your IIS setup and just create a media virtual folder for your website that points to your NAS and revert your changes to the file system provider config and use the defaults"


Alessandro Calzavara 06 Jun 2017, 10:28:46

@shearer3000 Yes it's really easy to set the virtual folder and it just works, even with the old media (if you moved them to the NAS). Just check that the permissions are right (or set them via pass-through authentication)


Priority: Normal

Type: Bug

State: Workaround posted

Assignee:

Difficulty: Normal

Category:

Backwards Compatible: True

Fix Submitted:

Affected versions: 7.4.2, 7.4.3, 7.5.1, 7.5.2, 7.5.3

Due in version:

Sprint: Sprint 57

Story Points:

Cycle: