Skip to main content
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Mike Kingery and Tim Cunningham
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Mike Kingery Tim Cunningham ( @TimCunningham71 )

Uploading Files With HttpClient In Angular 7.2.11

By 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.

Want to use code from this post? Check out the license.

Reader Comments

426 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.

15,640 Comments

@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

18 Comments

@Ben,

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

426 Comments

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?

15,640 Comments

@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

426 Comments

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?

15,640 Comments

@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.

15,640 Comments

@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:

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!

15,640 Comments

@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.

3 Comments

If I have a reactive form with validation (Angular 7). If the user does not complete the entire form, what is the best to to save/store/persist or capture the data?

I've tested @output with event emitters. I've read saving to local storage may not be the best idea.

Creating a service or service state may be the way to go but I'm new to this which is why I'm asking.

15,640 Comments

@Michael,

To be honest, I've never actually done that before -- save incomplete form data for later user. But, my first instinct would be to save it in LocalStorage. If you've read that LocalStorage is not the best idea, it is likely due to the longevity of it (which is a potential security risk). If that's the case, you might want to consider using SessionStorage instead:

https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage

It works like LocalStorage, but will expire the data when the page-session ends (ie, when the user closes the browser). This should alleviate more of the security concerns.

As far as how to listen for the data -- I also do not have much experience with Reactive Forms, but from what I recall, they are based on a Form state object. There's like some event on the State object that you can listen to and then push that data into the SessionStorage. You would then have to read from SessionStorage when the component first renders to see if you need to pull data from a previous form attempt.

Sorry I don't have more concrete advice - it's not something I've done before. I'll see if I can carve out some time to experiment.

3 Comments

@Ben,

Hi Ben,

Thanks for the answer. I tested different ways to achieve this. Creating a service may be the best fix here. My only concern is since it's a form I won't need to share the data with other components and once the form is complete I wont need to save it, the data will be in my DB.

I'm looking into creating a "state".

Maybe we can collaborate on a fix.

Many thanks.

1 Comments

private httpClient: HttpClient;

// I initialize the upload service.
constructor( httpClient: HttpClient ) {

    this.httpClient = httpClient;

}

could be written as

constructor(private httpClient: HttpClient) {
}
15,640 Comments

@Michael,

I know it's been a hot-minute since we last communicated; but, I finally got around to playing with storing temporary form-data so that it isn't lost upon refresh or back-button:

www.bennadel.com/blog/3834-saving-temporary-form-data-to-the-sessionstorage-api-in-angular-9-1-9.htm

In that, I'm using SessionStorage; but, it could have just as easily been LocalStorage for more persistent data (though, in that case, you would have to worry about cross-tab consistency).

15,640 Comments

@Richard,

Completely! It's just a matter of personal preference. I tend to be more wordy in all of my choices. Something always feels good (to me) about writing things out.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel