5

Problem: I have a web api which expose a method UploadFile, which will upload a file from a client to a specific directory of the server. The piece of code that handle the request and do the upload is the following:

var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit);

var reader = new MultipartReader(boundary, HttpContext.Request.Body);

try
{
    // Read the form data.
    var section = await reader.ReadNextSectionAsync();

    // This illustrates how to get the file names.
    while (section != null)
    {
        var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out ContentDispositionHeaderValue contentDisposition);
        if (hasContentDispositionHeader)
        {

            if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
            {
                targetFilePath = Path.Combine(root, contentDisposition.FileName.ToString());
                using (var targetStream = System.IO.File.Create(targetFilePath))
                {
                    await section.Body.CopyToAsync(targetStream);

                    //_logger.LogInformation($"Copied the uploaded file '{targetFilePath}'");
                }
            }

I always calledthis method using the following statement:

bool res = await importClient.UploadFileAsync(filePath);

where UploadFileAsync (which is on the client) build the request in this way:

var requestContent = new MultipartFormDataContent();
var array = File.ReadAllBytes(filePath);
var fileContent = new ByteArrayContent(array);
fileContent.Headers.ContentType =  MediaTypeHeaderValue.Parse("application/octet-stream");
requestContent.Add(fileContent, "file", Path.GetFileName(filePath));

As you can see, this method expect a file name/path to work, this means that the file must "exist" somewhere in the client machine. I've used this method without any problem until now. I have a very specific case in which i need to upload something needed on the server that the user don't want to save on his client.

Possible solutions:

  • The first thing that i thought was to manually create a file in the client, and after the upload delete it. However I'm not very happy with this solution cause i need to handle everything manually

  • I can use the System.IO.Path.GetTempFileName() method, which will create a file in the temporary directory, but i'm not quite sure how the cancellation of the files is handled

  • I can use the TempFileCollection, but it seems more or less a mix of the previous point. I can technically create this collection in a using statement to get rid of it when the upload is done

I'm inexperienced about these topics, so I'm not sure which solution could fit best this scenario

My requirements are that i need to be 100% sure that the file is deleted after the upload is done, and i would like the solution to be "async friendly", i.e. i need the whole process to keep going without problems.

EDIT: I see a little bit of confusion. My problem is not how to handle the files on the server. That part is not a problem. I need to handle "temporary" files on the client.

Daniele Sartori
  • 1,674
  • 22
  • 38
  • 5
    You could avoid writing it on disk, so there is no file to delete. Why dont you use a MemoryStream instead a FileStream?. IMHO It would be the best option, changing `using (var targetStream = System.IO.File.Create(targetFilePath))`. – Cleptus Sep 03 '19 at 11:13
  • @bradbury9 i want the solution with the less possible impact on the sorrounding environment. This web api is used also by other solutions, and i would prefer to avoid to modify it, and just to implement something that handle this situation. Also there is only this specific case where the file is not already saved on the client's disk – Daniele Sartori Sep 03 '19 at 11:17
  • 1
    Your requirement is trying to avoid trash, and your code creates a temporal file to the upload. You are trying to make sure you delete it after uploaded. This is an XY problem, if there is no file to delete, you would not have trash to delete. If you stick in your need of a physical temporary file, just do the delete in the `finally` of the `try-catch` statement – Cleptus Sep 03 '19 at 11:21
  • @bradbury9 can you elaborate your answer? Also if i change that part in my controller, will the previous solutions that use this specific method keep working or do i have to change anything? – Daniele Sartori Sep 03 '19 at 11:26
  • 2
    Unless you handle kinda big files, the best choice is using a `MemoryStream` instead doing `var targetStream = System.IO.File.Create(targetFilePath)`. Also you should make sure your `section.Body.CopyToAsync` accepts a `Stream` argument instead a `FileStream` (base class of both MemoryStream and FileStream is Stream). The only potential problem I see is having large files (MemoryStream stores data in memory, so OutOfMemoryException could happen) – Cleptus Sep 03 '19 at 11:30
  • Totally agree with @bradbury9, if you have to store it as a file, why not wrap the operation into a `try-finally` and just delete the file in the `finally` clause? – pneuma Sep 03 '19 at 11:35
  • @bradbury9 ok i didn't understood what exactly you meant before (my bad). That part can't be changed, cause i need the files to be saved on the server's disk to be elaborated later, so this using (var targetStream = System.IO.File.Create(targetFilePath)) must stay. My problem is ahed this, where i need to create in some case a "temporary" file on the clien'ts disk so that i can upload it on the server (i want the file to stay on the server and i handle it separately) – Daniele Sartori Sep 03 '19 at 11:36
  • Are you trying to delete some "client side 'temporary' file" from server side code? I would consider using a network share accesible from both server and clients with those temporary files so server code could delete the temp file if it is uploaded succesfully. That schema could be problematic cause the file could be opened because you know, network shares... – Cleptus Sep 03 '19 at 11:45
  • @bradbury9 no. this : bool res = await importClient.UploadFileAsync(filePath); is called on the client. I need to act before that and after that to handle these temporary files. The server doesn't know anything about the client and it must stay as it is – Daniele Sartori Sep 03 '19 at 11:57
  • "I need to act before that and after that to handle these temporary files" ...so what is stopping you doing that exactly? You seem to want there to be come kind of automatic mechanism which would clear up your file after the upload. There isn't, so you need to write some code. Then your code _is_ the automatic mechanism. Is is a problem to amend the client-side code? All it needs to do is wait for the upload to complete, and then delete the file. – ADyson Sep 03 '19 at 12:11
  • P.S. If you used temporary files, then you can easily google how windows handles files in the temp directory - basically it's down to the user's settings as to whether they get deleted automatically or not. If you are really concerned then it's probably better to do the deletion yourself as soon as you've finished with the file. – ADyson Sep 03 '19 at 12:13
  • 2
    Or another alternative is to stop using WebClient to upload files, and instead use something more sophisticated like HttpClient which would let you compose a multi-part request yourself and upload a stream of data, so you'd never have to create a file on the local disk to begin with. – ADyson Sep 03 '19 at 12:14

3 Answers3

2

Once you write something on the disk you can't be 100% that you will able to delete it. Moreover, even if you delete the file, you can't be sure that file can't be recovered.

So you have to ask why I need to delete the file. If it contains some secret, keep it in memory. If you can't fit the file into memory, write it encrypted on the disk and keep only key in the memory.

If you relax 100% to 99%, I would go for creating a file with Path.GetTempFileName and deleting it in finally block.

If 99% is not enough but 99.98% is, I would store names of created temporary files in persistent storage and regularly check if they are deleted.

Jakub Šturc
  • 35,201
  • 25
  • 90
  • 110
  • It's not about the secret, it's more about the fact that i don't want to leave trash on the client's disks. The 99% of the stuff i need to upload are already saved on the client. The remaining 1% represent this specific case i which i need something on the server for a "one shot" operation. These are files that won't be used anymore, that's why i call them temporary. – Daniele Sartori Sep 03 '19 at 12:00
  • 3
    @user234461 thank you for letting me know. Please tell me more and I will be happy to update/delete this answer. – Jakub Šturc Sep 03 '19 at 12:40
  • @DanieleSartori: In that case, I don't see reason why not to use `Path.GetTempFileName` and delete it in finally block. In the rare case when the client application is not executing finally block, it's an OS responsibility to purge temporary files. – Jakub Šturc Sep 03 '19 at 12:41
2

For completition i'm writing the solution i used based on the suggestions i received here. Also the filename written as i did grant that statistically you won't have 2 temporary file with the same name

 try
 {
     string file = System.IO.Path.GetTempPath() + Guid.NewGuid().ToString() + ".xml";
     tempFile = Path.GetFileName(file);
     using (FileStream fs = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))
     {
         XmlSerializer serializer = new XmlSerializer(typeof(FileTemplate));
         serializer.Serialize(fs, w.Template);
     }

 }
 catch(Exception ex)
 {

     logger.Error(ex.Message);
     //...
 }
 finally
 {
     //.... do stuff
     File.Delete(tempFile );
 }
Daniele Sartori
  • 1,674
  • 22
  • 38
  • 1
    What happens when your code fails somewhere in `Serialize;` method? In that case, I believe `File.Delete` fails on `ArgumentNullException` and a created file won't be deleted. – Jakub Šturc Sep 04 '19 at 09:28
  • @JakubŠturc the file is created in the first line. The next block write in that file, so the File.Delete shouldn't fail (unless i'm missing something) – Daniele Sartori Sep 04 '19 at 13:34
  • Let's assume serialization fails and throws some exception. In that case, the file is created and `tempFile` is null. In this state, we arrive to finally block and call `File.Delete(null)` won't delete the original file and moreover, it throws another exception. – Jakub Šturc Sep 05 '19 at 06:30
  • yeah you are right @JakubŠturc I just need to move tempFile = Path.GetFileName(file); before the using block. Thanks – Daniele Sartori Sep 05 '19 at 06:55
1

You clearly shouldn't be using a file, in fact you don't want your data to ever leave RAM. You need to use "secure" memory storage so that the data is "guaranteed" to be pinned to physical RAM, untouched by the garbage collector, "never" paged out to swap. I use the quotes, because all those terms are somewhat misleading: the implementation isn't secure in absolute sense, it's just more secure than writing stuff to a disk file. Absolute security is impossible.

There are no common mechanisms that guarantee deletion of anything: the machine could "die" at any point between the writing of the data to the file, and whatever deletion operation you'd use to wipe the file "clean". Then you have no guarantee that e.g. the SSD or the hard drive won't duplicate the data should e.g. a sector become bad and need to be reallocated. You seem to wish to deal with several layers of underdocumented and complex (and often subtly buggy) layers of software when you talk about files:

  1. The firmware in the storage device controller.
  2. The device driver for the storage device.
  3. The virtual memory system.
  4. The filesystem driver.
  5. The virtual filesystem layer (present in most OSes).
  6. The .net runtime (and possibly the C runtime, depending on implementation).

By using a file you're making a bet that all those layers will do exactly what you want them to do. That won't usually be the case unless you tightly control all of these layers (e.g. you deploy a purpose-made linux distribution that you audit, and you use your own flash storage firmware or use linux memory technology driver that you'd audit too).

Instead, you can limit your exposure to just the VM system and the runtime. See e.g. this answer; it's easy to use:

using (var secret = new SecureArray<byte>(secretLength))
{
    DoSomethingSecret(secret.Buffer);
}

SecureArray makes it likely that secret.Buffer stays in RAM - but you should audit that code as well, since, after all, you need it to do what it does, with your reputation possibly at stake, or legal liability, etc.

A simple test that can give you some peace of mind would involve a small test application that writes a short pseudorandom sequence to secret.Buffer, and then sleeps. Let this run in the background for a few days as you use your computer, then forcibly power it down (on a desktop: turn the on-off switch on the power supply to "off" position). Then boot up from a linux live CD, and run a search for some chunk of the pseudorandom sequence on the raw disk device. The expected outcome is that no identifiable part of the sequence has leaked to disk (say nothing larger than 48-64 bits). Even then you can't be totally sure, but this will thwart the majority of attempts at recovering the information...

...until someone takes the customer's system, dumps liquid nitrogen on the RAM sticks, shuts down the power, then transfers RAM to a readout device you can put together for

...or until they get malware on the system where the software runs, and it helpfully streams out RAM contents over internet, because why not.

...or until someone injects their certificate into the trust root on just one client machine, and MITM-s all the data elsewhere on the client's network.

And so on. It's all a tradeoff: how sure you wish to be that the data doesn't leak? I suggest getting the exact requirements from the customer in writing, and they must agree that they understand that it's not possible to be completely sure.

Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313