How to secure file download via controller action and ajax? - c#

I have an application where I want users to be able to upload and download their own files. I implemented the upload and download however I am concerned with XSS vulnerability of the download action. I was only able to implement the file actually downloading using GET method, but I want to secure it (usually I use POST + antiforgery token). How can I do this?
This is my controller action:
public ActionResult DownloadFile(int clientFileId)
{
var clientId = GetClientId(clientFileId);
var client = _unitOfWork.Clients.GetById(clientId);
if (client == null)
return HttpNotFound();
var file = _unitOfWork.ClientFiles.GetById(clientFileId);
if (file == null)
return HttpNotFound();
var practiceId = _unitOfWork.Users.GetPracticeIdForUser(User.Identity.GetUserId());
if (!AuthorizationHelper.CheckBelongsToPractice(_unitOfWork.Clients, typeof(Client),
practiceId, client.Id, nameof(Client.Id), nameof(Client.PracticeId)))
{
return new HttpUnauthorizedResult();
}
var fileInfo = new FileInfo(file.FilePath);
var fileName = fileInfo.Name;
if (!fileInfo.Exists)
return HttpNotFound();
var path = Path.Combine(Server.MapPath("~/ClientFiles/" + clientId + "/"), fileName);
var contentType = MimeMapping.GetMimeMapping(path);
try
{
var contentDisposition = new System.Net.Mime.ContentDisposition
{
FileName = fileName,
Inline = false,
};
Response.AppendHeader("Content-Disposition", contentDisposition.ToString());
return File(path, contentType, fileName);
}
catch (Exception ex)
{
new ExceptionlessLogger(ex).Log();
return new HttpStatusCodeResult(500);
}
}
And my ajax call
$('#client-files-table').on('click', '.js-download', function () {
var link = $(this);
$.ajax({
url: '/clients/clientfiles/downloadfile?clientFileId=' + link.attr('data-clientfile-id'),
method: 'GET',
//data: {
// __RequestVerificationToken: getToken()
//},
success: function () {
window.location = '/clients/clientfiles/downloadfile?clientFileId=' + link.attr('data-clientfile-id'),
loadPartials();
},
error: function () {
toastr.error('Unable to download.');
}
});
});

I found the answer here: https://codepen.io/chrisdpratt/pen/RKxJNo
$('#client-files-table').on('click', '.js-download', function () {
var link = $(this);
$.ajax({
url: '/clients/clientfiles/downloadfile?clientFileId=' + link.attr('data-clientfile-id'),
method: 'POST',
data: {
__RequestVerificationToken: getToken()
},
xhrFields: {
responseType: 'blob'
},
success: function (data, status, xhr) {
var a = document.createElement('a');
var url = window.URL.createObjectURL(data);
a.href = url;
var header = xhr.getResponseHeader('Content-Disposition');
var filename = getFileNameByContentDisposition(header);
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
loadPartials();
},
error: function () {
toastr.error('Unable to download.');
}
});
});

Related

C# Return File not working to download an Excel file

I'm trying to download a file like this with an MVC project:
Controller:
[HttpPost]
[ActionName("ExportList")]
public async Task<IActionResult> ExportList(List<object> objectList)
{
var fileName = "fileName" ".xlsx";
try
{
var fileContent = await _serviceLayer.ExportListToExcel(objectList);
return File(fileContent, "application/vnd.ms-excel", fileName);
// return new JsonResult("ok");
}
catch(Exception ex)
{
return BadRequest(ex.Message);
}
}
then my service layer:
public Task<byte[]> ExportListToExcel(List<object> objectList)
{
try
{
using(var workbook = new XLWorkbook())
{
var worksheet = workbook.Worksheets.Add("Report");
var currentRow = 1;
#region Header
worksheet.Cell(currentRow, 1).Value = "val1";
worksheet.Cell(currentRow, 2).Value = "val2";
worksheet.Cell(currentRow, 3).Value = "val3";
#endregion
#region Body
foreach(var obj in objectList)
{
currentRow++;
worksheet.Cell(currentRow, 1).Value = obj.val1;
worksheet.Cell(currentRow, 2).Value = obj.val2;
worksheet.Cell(currentRow, 3).Value = obj.val3;
}
#endregion
using(var stream = new MemoryStream())
{
workbook.SaveAs(stream);
var content = stream.ToArray();
return Task.FromResult(content);
}
}
}
catch (Exception ex)
{
throw new Exception("Error");
}
}
I call the controller method with ajax:
function exportList() {
$.ajax({
type: "POST",
url: "#Url.Action("ExportList")",
data: { objectList: objectList},
dataType: "json",
success: function (data) {
//show success message
}, error: function (req, status, error) {
//show error message
}
});
}
I read a lot of posts and this is bassically what they suggest to download the file, but instead I'm returning to the view with the following error Message and the file is not being downloaded but the controller is not throwing any kind of exception message:
Any ideas to download the file?
Try Blob responseType,
$.ajax({
type: "POST",
url: "#Url.Action("ExportList")",
data: { objectList: objectList},
xhrFields: {
responseType: 'blob' // FileContentResult will return as blob
},
success: function (data) {
const url = window.URL.createObjectURL(data); // create url from blob
const link = document.createElement('a'); // create anchor at DOM
link.href = url;
link.setAttribute('download', 'file.xlsx'); // download attribute
document.body.appendChild(link);
link.click(); // trigger click for download
},
error: function (req, status, error) {
//show error message
}
});
Hi your problem is your javascript function exportList()
it's better create a redirect on where the file is and then download it.
In your export method
That will be part of your ExportList controller method:
// Generate a new unique identifier against which the file can be stored
string handle = Guid.NewGuid().ToString();
using (MemoryStream memoryStream = new MemoryStream())
{
//save your file to memory stream
workbook.SaveAs(memoryStream);
memoryStream.Position = 0;
TempData[handle] = memoryStream.ToArray();
}
// Note we are returning a filename as well as the handle
return new JsonResult()
{
Data = new { FileGuid = handle, FileName = "exporfile.xlsx" , FileType="xlsx"}
};
your download file should look like this:
public virtual ActionResult Download(string fileGuid, string fileName, string fileType)
{
if (TempData[fileGuid] != null)
{
byte[] data = TempData[fileGuid] as byte[];
if (fileType == "xlsx" || fileType == "xls")
{
return File(data, "application/vnd.ms-excel", fileName);
}
else if (fileType == "pdf")
{
return File(data, "application/pdf", fileName);
}
else
{
//sin especificar...
return File(data, "application/octet-stream", fileName);
}
}
else
{
return new EmptyResult();
}
}
then change your exportlist method:
function exportList() {
$.ajax({
type: "POST",
url: "#Url.Action("ExportList")",
data: { objectList: objectList},
dataType: "json",
success: function (data) {
var response = JSON.parse(data);
window.location = '/YourController/Download?fileGuid=' + response.FileGuid + '&filename=' + response.FileName +'&fileType=' + response.FileType;
}, error: function (req, status, error) {
//show error message
}
});
}

Ajax call to async method returning file successfully but success/complete part of an Ajax request is not getting executed

I am trying to export selected records in to a file and reload the page to update the records in a current view. I am calling web api asynchronously to get all the records. An AJAX call is executing an action in a controller successfully and returning expected data without any error but none of the 'success', 'complete' or 'error' part of ajax function is executing. There are no errors in a developer tool of the browser, no exception, nothing unusual so its getting trickier for me to investigate this issue further. Can I request your a suggestions on this please? Thanks
View :
#Html.ActionLink("Export records", "Index", null, new { Id = "myExportLinkId")
Script :
$("a#myExportLinkId").click(function (e) {
var selected = "";
$('input#myCheckBoxList').each(function () {
if (this.checked == true) {
selected += $(this).val() + ',';
}
});
if (selected != "") {
$.ajax({
url: '/MyController/MyAction',
type: 'GET',
contentType: "application/json; charset=utf-8",
dataType: "json",
data: {
'MyString': 'stringValue'
},
success: function (data) {
alert("success");
},
error: function () {
alert("error");
}
});
})
And the action/method looks like this :
[HttpGet]
public async Task<ActionResult> ExportNewOrders(string OrderIdString)
{
//code to create and store file
//actually want to send the file details as json/jsonResult but for testing only returning
//string here
return Json( "Success", "application/json", JsonRequestBehavior.AllowGet);
}
Finally I have resolved this with Promisify functionality of an AJAX call. Obviously the json response I was returning had an issue so I have replaced
return Json( "Success", "application/json", JsonRequestBehavior.AllowGet);
to
return new JsonResult(){
Data = new { success = true, guid = handle, fileName = exportFileName },
ContentType = "application/json",
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
which has fixed the bug and the success function of ajax call got executed.
But other than this there were issues to wait until the file download (which involved encryption decryption, server validations etc) completes and then refresh the page. This I have resolved by implementing an ajax call with Promisify fuctionality. You can find codepen example here and the original post here.
Here is the complete code.
View/HTML
#Html.ActionLink("Export", "yourActionName", null, new { Id = "exportRequest", #onclick = "letMeKnowMyFileIsDownloaded();" })
Script/Ajax
function letMeKnowMyFileIsDownloaded() {
return new Promise(function (resolve, reject) {
$("a#exportRequest").on("click", function () {
$.ajax({
url: this.href + "?param=whatever params you want to pass",
dataType: "json",
data: {
'param1': 'value'
},
success: function (data) {
var a = document.createElement("a");
var url = '/yourControllerName/Download?fileGuid=' + data.guid + '&filename=' + data.fileName;//window.URL.createObjectURL(data);
a.href = url;
a.download = data.fileName;
document.body.append(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
resolve(true);
},
error: function (error) {
reject(error);
}
});
});
});
}
letMeKnowMyFileIsDownloaded()
.then(function (bool) {
if (bool) {
//alert("File downloaded 👇");
window.location.reload(1);
}
})
.catch(function (error) {
alert("error");
});
I have used nuget package ClosedXML to handle excel file functionality. Using the stream to create and download the data in excel file without storing the file physically on the server.
And in the controller
//can be async or sync action
public async Task<ActionResult> Index(YourModel model)
{
//do stuff you want
var exportOrders = your_object;
//using DataTable as datasource
var dataSource = new DataTable();
//write your own function to convert your_object to your_dataSource_type
dataSource = FormatTypeToDataTable(exportOrders);
if (dataSource != null && dataSource.Rows.Count > 0)
{
//install ClosedXML.Excel from nuget
using (XLWorkbook wb = new XLWorkbook())
{
try
{
var handle = Guid.NewGuid().ToString();
wb.Worksheets.Add(dataSource, "anyNameForSheet");
string exportFileName = "yourFileName" + ".xlsx";
MemoryStream stream = GetStream(wb);
TempData[handle] = stream; exportFileName);
return new JsonResult()
{
Data = new { success = true, guid = handle, fileName = exportFileName },
ContentType = "application/json",
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
catch (Exception ex)
{
//ModelState.AddModelError("", ex.Message);
}
}
}
}
public virtual ActionResult Download(string fileGuid, string fileName)
{
if (TempData[fileGuid] != null)
{
var stream = TempData[fileGuid] as MemoryStream;
var data = stream.ToArray();
return File(data, "application/vnd.ms-excel", fileName);
}
else
{
return new EmptyResult();
}
}

How to get Base64 from ajax Post?

I was trying to get the base64 post in my codebehind webmethod,
but it seems like everytime I include the base64 I get an error : the server responded with a status of 500 (Internal Server Error) - it keeps on hitting the error function.
The Post works with the other strings when the base64 is not inlcuded int the data that im passing.
function event_create() {
alert("alert test : function works => onclick");
function getBase64(file) {
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function () {
console.log(reader.result);
};
reader.onerror = function (error) {
console.log('Error: ', error);
};
}
var eventTitle = $("#eventTitle").val();
var eventDesc = $("#eventDesc").val();
var eventTimeStart = $("#eventTimeStart").val();
var eventTimeEnd = $("#eventTimeEnd").val();
var eventDateStart = $("#eventDateStart").val();
var eventDateEnd = $("#eventDateEnd").val();
var eventType = $("#eventType").val();
var eventPlace = $("#eventPlace").val();
var eventAttendee = document.getElementById("lblSelected").innerText;
var userID = sessionStorage.getItem("userID");
var imageBase64 = getBase64(document.getElementById('test').files[0]);
var data = { 'eventTitle': eventTitle, 'eventDesc': eventDesc, 'eventPlace': eventPlace, 'eventType': eventType, 'eventAttendee': eventAttendee, 'userID': userID, 'imageBase64': imageBase64};
$.ajax({
type: "POST",
async: true,
contentType: "application/json; charset=utf-8",
url: ".../../../../Operation/insert.aspx/createEvent",
data: JSON.stringify(data),
datatype: "json",
success: function (result) {
if (result.d <= 0) {
//false alert something
alert("FALSE");
}
else if (result.d > 0) {
//true
alert(result.d);
}
else {
alert("sent but no call-back");
}
console.log(result);
},
error: function (xmlhttprequest, textstatus, errorthrown) {
alert(" connection to the server failed ");
console.log("error: " + errorthrown);
}
});
}
Here's the Webmethod that will get the post
[WebMethod(EnableSession = true)]
public static string createEvent(string eventTitle, string eventDesc, string eventPlace, string eventType, string eventAttendee, string userID, string imageBase64)
{
String orgID = (String)HttpContext.Current.Session["orgID"];
string response = orgID;
string path = HttpContext.Current.Server.MapPath("~/Users/Organizer/organizerData/"); // de path
//Check if directory exist
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path); //Create directory if it doesn't exist
}
string imageName = "event1" + ".jpg";// for instance
//set the image path
string imgPath = Path.Combine(path, imageName);
byte[] imageBytes = Convert.FromBase64String(imageBase64);
File.WriteAllBytes(imgPath, imageBytes); //write the file in the directory
return imageBase64;
}

Ajax Response Doesn't work from server side (iis)

I try to save a file (image) on C# MVC and JS with the following function js
function Add() {
var data = new FormData();
var files = $("#txtUploadFile").get(0).files;
var cod = document.getElementById('cod').value;
var mat = document.getElementById('mat').value;
var status = document.getElementById('status').value;
var plant = document.getElementById('plant').value;
if (files.length > 0) {
if (window.FormData !== undefined) {
var data = new FormData();
for (var x = 0; x < files.length; x++) {
data.append("file" + x, files[x]);
data.append("mat", mat);
data.append("status", status);
data.append("plant", plant);
data.append("code", cod);
}
$.ajax({
type: 'POST',
url: '/Pred/Admin/AddPred',
contentType: false,
processData: false,
data: data,
success: function (response) {
if(response.msg == 1)
{
refreshTable(response.data);
}
alert('Predio agregado.');
},
error: function (xhr, status, p3, p4) {
var err = "Error " + " " + status + " " + p3 + " " + p4;
if (xhr.responseText && xhr.responseText[0] == "{")
err = JSON.parse(xhr.responseText).Message;
}
});
}
}
}
and on the codebehind I used it
public ActionResult AddPred()
{
int isInsert=0;
string route = ConfigurationManager.AppSettings["MAPS_ROUTE"];
string[] status, plants, mats, codes;
int stat;
try
{
var requeststatus = Request.Params;
status = requeststatus.GetValues("status");
plants = requeststatus.GetValues("plant");
codes = requeststatus.GetValues("cod");
mats = requeststatus.GetValues("mat");
if (status[0] == "On")
stat= 1;
else
stat= 0;
string plant = plants[0];
string mat = mats[0];
string code = codes[0];
foreach (string file in Request.Files)
{
var fileContent = Request.Files[file];
if (fileContent != null && fileContent.ContentLength > 0)
{
var fileName = fileContent.FileName;
var path = Path.Combine(Server.MapPath(route), fileName);
path = Server.MapPath(route) + fileName;
var sqlpath = "." + route + "/" + fileName;
fileContent.SaveAs(path);
isInsert = ma.InsertPred(code, mat, stat, plant, sqlpath);
}
}
merge.preds = ma.GetPreds();
return Json(new { success = true, data = merge.preds, msg = isInsert }, JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
Response.StatusCode = (int)HttpStatusCode.BadRequest;
return Json("add failed");
}
}
But the server response ever is
POST myserver/Preds/Admin/AddPred 500 (Internal Server Error)
and I used console.log but I can't to get the error information, When used this code on local side, it's runs Okey, save the image and return model for refresh the front, but when put the aplication on the server, only return error, others funtions works (modal show, return model with json) but doesn't work save a image, I set permissions (write, load, modify) on the server folder,
someone give a idea for solves this problem, I don't know whats wrong
Try like this :
function getCities(id) {
$.ajax({
type: "POST",
url: "#Url.Action("Index", "Default")",
data: '{id: id }',
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (response) {
//alert(response.responseData);
window.location = '/Default/YourView';//
},
error: function (response) {
alert("error!"); //
}
});
}
//In your controller
public JsonResult Index(string id)
{
merge.country= mp.Country();
merge.city= mp.City(id);
return Json("you can return an object or a string, what you want");
}

How to pass parameters from angular $upload to web api

I'm having trouble passing parameters from my UI to my upload logic
I'm setting up the upload request like this
$upload.upload({
url: "./api/import/ImportRecords",
method: "POST",
data: { fileUploadObj: $scope.fileUploadObj },
fields: { 'clientId': $scope.NewImport.clientId },
file: $scope.$file
}).progress(function (evt) {
}).success(function (data, status, headers, config) {
}).error(function (data, status, headers, config) {
});
My API is setup as follows:
[HttpPost]
public IHttpActionResult ImportRecords()
{
var file = HttpContext.Current.Request.Files[0];
// Need to read parameter here
}
What is the clean/correct way to accomplish this?
Must you use $upload? Uploading files using $http is pretty simple without the need of a separate plugin.
Factory
app.factory('apiService', ['$http', function($http){
return {
uploadFile: function(url, payload) {
return $http({
url: url,
method: 'POST',
data: payload,
headers: { 'Content-Type': undefined },
transformRequest: angular.identity
});
}
};
}]);
Controller
//get the fileinput object
var fileInput = document.getElementById("fileInput");
fileInput.click();
//do nothing if there's no files
if (fileInput.files.length === 0) return;
//there is a file present
var file = fileInput.files[0];
var payload = new FormData();
payload.append("clientId", $scope.NewImport.clientId);
payload.append("file", file);
apiService.uploadFile('path/to/ImportRecords', payload).then(function(response){
//file upload success
}).catch(function(response){
//there's been an error
});
C# Webmethod
[HttpPost]
public JsonResult ImportRecords(int clientId, HttpPostedFileBase file)
{
string fileName = file.FileName;
string extension = Path.GetExtension(fileName);
//etcc....
return Json("horray");
}
Assuming that you are using ng-file-upload. This should work
[Route("ImportRecords")]
[HttpPost]
public async Task<HttpResponseMessage> ImportRecords()
{
if (!Request.Content.IsMimeMultipartContent())
{
this.Request.CreateResponse(HttpStatusCode.UnsupportedMediaType);
}
string tempFilesPath = "some temp path for the stream"
var streamProvider = new MultipartFormDataStreamProvider(tempFilesPath);
var content = new StreamContent(HttpContext.Current.Request.GetBufferlessInputStream(true));
foreach (var header in Request.Content.Headers)
{
content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
var data = await content.ReadAsMultipartAsync(streamProvider);
//this is where you get your parameters
string clientId = data.FormData["clientId"];
...
}
And this is how you should be calling $upload.upload
$upload.upload({
url: "./api/import/ImportRecords",
method: "POST",
data: { fileUploadObj: $scope.fileUploadObj,
clientId: $scope.NewImport.clientId,
file: $scope.$file
}
}).progress(function (evt) {
}).success(function (data, status, headers, config) {
}).error(function (data, status, headers, config) {
});
Hope it helps!

Categories

Resources