Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Christian Ready
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Christian Ready@christianready )

Uploading Files With HttpClient In Angular 7.2.11

By Ben Nadel on

For the last 7 years, I've been using Plupload to manage file uploads in my AngularJS application. I've even used Plupload to upload files directly from the browser to an Amazon S3 bucket. And, because I've been using Plupload, I've always viewed JavaScript-based file-management as a somewhat "magical" black-box. This is why I just assumed that Angular's HttpClient didn't support File uploads (as demonstrated in yesterday's post on using Netlify Functions with Angular). Thankfully, Charles Robertson came along and taught me the error of my ways. It turns out, Angular's HttpClient can easily support File uploads. And, since I didn't know this, it's possible that there other people out there that didn't know it either. As such, I wanted to put together a quick file upload demo in Angular 7.2.11.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub (broken).

View this code in my JavaScript Demos project on GitHub.

In order to facilitate this file upload demo in Angular, I had to create a very simple server-side file upload handler. Since I already have Lucee CFML installed on my development machine, I went ahead and created a naive Lucee-based file processor. The following Lucee CFML takes the binary content posted to the server and naively saves it to a web-accessible location.

CAUTION: Never save user-provided content to a web-accessible location. This is an absurdly high security vulnerability and allows for remote execution of user-provided code. I am only doing this in order to keep the demo as simple as possible.

  • <cfscript>
  •  
  • // ******************************************************************************* //
  • // ******************************************************************************* //
  • // CAUTION: WRITING USER-PROVIDED FILES TO AN ACCESSIBLE WEB LOCATION IS EXTREMELY
  • // DANGEROUS AND SHOULD NEVER EVER EVER EVER BE DONE IN A PRODUCTION APPLICATION.
  • // --
  • // I am doing this only because the server-side file-handling is not the point of
  • // the demo - it is here only to facilitate the Angular code. In reality, allowing
  • // a user to upload a file and then reference it directly allows for REMOTE CODE
  • // EXECUTION which is a critical security vulnerability.
  • // ******************************************************************************* //
  • // ******************************************************************************* //
  •  
  • try {
  •  
  • // Enforce URL parameters.
  • param name="url.clientFilename" type="string";
  • param name="url.mimeType" type="string";
  •  
  • fileWrite(
  • expandPath( "/uploads/#url.clientFilename#" ),
  • getHttpRequestData().content
  • );
  •  
  • // Return the web-accessible file location of the upload (for the demo).
  • response = {
  • "url": "./api/uploads/#url.clientFilename#"
  • };
  •  
  • cfcontent(
  • type = "application/json",
  • variable = charsetDecode( serializeJson( response ), "utf-8" )
  • );
  •  
  • } catch ( any error ) {
  •  
  • cfheader( statusCode = 500 );
  • cfcontent(
  • type = "application/json",
  • variable = charsetDecode( serializeJson( error ), "utf-8" )
  • );
  •  
  • }
  •  
  • </cfscript>

With this server-side uploader in place (which obviously won't wort on my GitHub pages demo), we can now create the Angular 7.2.11 code that will upload File objects. To do this, let's create an UploadService that accepts a File object and then encapsulates the consumption of the HttpClient.

As you will see in the code below, posting a single File is exactly like posting any other kind of data with the HttpClient: you just provide the File Blob as the "body" of the POST. The biggest difference when posting a File is that we have to explicitly provide the "Content-Type" HTTP Header indicating the type of Blob we are posting. If we don't do this, the server will try to process the HTTP Post body in unexpected ways.

  • // Import the core angular services.
  • import { HttpClient } from "@angular/common/http";
  • import { Injectable } from "@angular/core";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • interface ApiUploadResult {
  • url: string;
  • }
  •  
  • export interface UploadResult {
  • name: string;
  • type: string;
  • size: number;
  • url: string;
  • }
  •  
  • @Injectable({
  • providedIn: "root"
  • })
  • export class UploadService {
  •  
  • private httpClient: HttpClient;
  •  
  • // I initialize the upload service.
  • constructor( httpClient: HttpClient ) {
  •  
  • this.httpClient = httpClient;
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I upload the given file to the remote server. Returns a Promise.
  • public async uploadFile( file: File ) : Promise<UploadResult> {
  •  
  • var result = await this.httpClient
  • .post<ApiUploadResult>(
  • "./api/upload.cfm",
  • file, // Send the File Blob as the POST body.
  • {
  • // NOTE: Because we are posting a Blob (File is a specialized Blob
  • // object) as the POST body, we have to include the Content-Type
  • // header. If we don't, the server will try to parse the body as
  • // plain text.
  • headers: {
  • "Content-Type": file.type
  • },
  • params: {
  • clientFilename: file.name,
  • mimeType: file.type
  • }
  • }
  • )
  • .toPromise()
  • ;
  •  
  • return({
  • name: file.name,
  • type: file.type,
  • size: file.size,
  • url: result.url
  • });
  •  
  • }
  •  
  • }

If you've used the HttpClient in the past to communicate with the back-end, this is surprisingly simple. I am delighted to see how easy this is!

Now, we need to create a demo that uses the UploadService class to upload a user-provide file. To keep things simple, I've added an Input[type=file] to my App Component that, upon (change), takes the provided files and sends them to the .uploadFile() method. The results are then rendered in the View where they user can click to download them:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { UploadResult } from "./upload.service";
  • import { UploadService } from "./upload.service";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • <div class="upload">
  • <span class="upload__label">
  • Select File(s) to Upload
  • </span>
  •  
  • <input
  • #fileInput
  • type="file"
  • [multiple]="true"
  • class="upload__input"
  • (change)="uploadFiles( fileInput.files ) ; fileInput.value = null;"
  • />
  • </div>
  •  
  • <h2>
  • Uploads
  • </h2>
  •  
  • <ul class="uploads">
  • <li *ngFor="let upload of uploads" class="uploads__item">
  •  
  • <a [href]="upload.url" target="_blank" class="uploads__link">
  • {{ upload.name }}
  • </a>
  • <span class="uploads__size">
  • ( Size: {{ upload.size | number }} bytes )
  • </span>
  •  
  • </li>
  • </ul>
  • `
  • })
  • export class AppComponent {
  •  
  • public uploads: UploadResult[];
  •  
  • private uploadService: UploadService;
  •  
  • // I initialize the app component.
  • constructor( uploadService: UploadService ) {
  •  
  • this.uploadService = uploadService;
  • this.uploads = [];
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I upload the given files to the remote API.
  • public async uploadFiles( files: File[] ) : Promise<void> {
  •  
  • // The given files collection is actually a "live collection", which means that
  • // it will be cleared once the Input is cleared. As such, we need to create a
  • // local copy of it so that it doesn't get cleared during the asynchronous file
  • // processing within the for-of loop.
  • for ( var file of Array.from( files ) ) {
  •  
  • try {
  •  
  • this.uploads.push(
  • await this.uploadService.uploadFile( file )
  • );
  •  
  • } catch ( error ) {
  •  
  • console.warn( "File upload failed." );
  • console.error( error );
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  • }

As you can see, when the file Input is changed, we grab the Files collection, loop over it, and passed each one to the UploadService where we use the HttpClient to upload the file to the server. And, when we run this in the browser (with my Lucee Server running in the background) and select some files, we get the following output:


 
 
 

 
 Uploading files with the HttpClient in Angular 7.2.11. 
 
 
 

As you can see, the selected files are successfully uploaded to the server using the underlying HttpClient service.

I am sure that for many Angular developers out there, this is not surprising. After all, file uploads are touched-upon right in the HTTP Guide of the Angular docs site. However, the Angular landscape is a lot to keep in your head. So, if you didn't know this - or you forgot that the HttpClient supports file uploads - hopefully this post has been somewhat helpful.



Reader Comments

It's great to see some CF & Angular code in the same post!
Anyway, I am wondering. If a user logs in and then uploads a file. Theoretically, the user is the only person who would know the location of that file. I am interested to know, how this might be a security vulnerability, unless you are saying that somehow that user could maliciously delete other files in that folder. But, this could be rectified, if one uses a JWE token to identify the user & whether the user owns the files that he/she might be trying to delete. Plus, if one uses a UUID for naming the files, it makes it pretty difficult for a user to guess the names of another user's files.

Reply to this Comment

@Charles,

So, the issue isn't that one user can know where another user's file is -- the issue is more that the user doing the uploading might be malicious. So, if they were uploading to a ColdFusion server, they might be able to upload a file like, malicious.cfm, and then try to execute that file one the server, /uploads/malicious.cfm. At that point, they could have run of the server since it would likely be assumed that any ColdFusion file on the server is "trusted".

By changing the file-name, that is definitely helpful. But, only if the file-name is not predictable. There was an exploit some years ago were a file was changed, but only after it was written to disk. This was still exploitable because the attacker used a load tester to hammer the server during the file-upload. As such, the attacker was able to invoke the known file-location in the few milliseconds in between the file-write and the file-rename.

So, you can save files where it makes sense ... the caution is just a warning to proceed with caution :D

Reply to this Comment

@Ben,

Would black-listing, or more effectively white-listing, file types alleviate this security concern?

Reply to this Comment

Ben. Just out of interest. I have the following set up. User uploads image, but nothing gets returned to the user [in other words, the response does not return a share link to the user]. The image is then added to a public gallery. Do I still need to take the precautions, you are talking about?

Reply to this Comment

@JC, @Charles,

Ha ha, sorry to make everyone so nervous. I probably shouldn't have been so adamant in my warning :D Really, I was only intending to say that people should be cautious about allowing users to upload files to a public space with a predictable name that may be executable. If the file isn't executable, then there's really no security threat. For example, you can upload anything to Amazon S3 because S3 won't "execute" files that you request - it will only return them.

For something like white-listing, which Charles eluded to on Twitter with the so-called "magic bytes" / "magic numbers", that's a good approach and one that I have personally used in the past. The only caution there is whether or not you write the file to a public disk location before you perform the white-list check. If so, you still provide an attacker with a moment in which they could theoretically access the after you've persisted it, but before you've validated it.

To get around this, all you really have to do is write the file to a temp location first, like in ColdFusion's, getTempDirectory(), perform the validation, and then - if valid - move the file to a public location.

So, storing files is totally possible -- I didn't mean to scare everyone :D

Reply to this Comment

Yes. Absolutely. Exploring this issue has been really valuable.

I have now updated my upload routine, so that CF writes the file to a folder above the webroot with a UUID folder name. I then do the file type check and if valid, I move it to my public image gallery folder.

But, I am really glad you highlighted this.

In the past, I have always used

<cffile action="upload" />

But, now that I am using an Angular front end, I no longer require CF to upload my files.

I now use CF to parse the binary data object sent by Angular. This requires a slightly more complex security routine. Previously, 'cffile action=upload' had its own built in security feature, namely the attribute 'accept'. When combined with the attribute 'strict', CF actually checks the file type, not just the extension.

I reckon I have a pretty rock solid file checking routine now.

And I also discovered that you can specify the HTML input file accept parameter, so that it only shows files of a certain type in the file picker. This adds yet another layer of security, although I wouldn't rely on this alone. And I am not sure how this works in every different browser type, either?

Reply to this Comment

@Charles,

Security is a "fun", never-ending adventure :D Unfortunately, it's not a "one and done" kind of thing. At work, we participate in a "Bug Bounty" program, where we have community members constantly trying to hack the application to help us find and eliminate security concerns. It's crazy what some of these people are able to find.

As far as the input[type=file] having the accept feature, I believe that's part of the original implementation, so I think it is likely to be widely supported.

Reply to this Comment

@All,

As I've been exploring file-uploads in Angular, one thing that I kept running into is the fact that using the [ngModel] directive with input[type="file"] will consume the .value property in the two-way data binding, not the .files property. As such, I wanted to see if I could create a custom ControlValueAccessor that would consume the .files in the two-way data-binding:

https://www.bennadel.com/blog/3597-using-ngmodel-with-input-type-file-and-a-custom-controlvalueaccessor-in-angular-7-2-12.htm

I love how powerful and flexible Angular is!

Reply to this Comment

@Robert,

Theoretically, Yes. After all, Angular exposes all the Classes. All you would have to do is import them and then instantiate them yourself. Which means that you have to do - manually - what the Dependency Injection is normally doing for you automatically.

I'm not saying it's easy, I'm just saying it's possible.

What are you trying to do? Why try to use this in a static context? If you just need an HTTP client outside of the normal bootstrapping, you could try just using another library, like Axios.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.