幾年前軟體弱點檢測報告出爐,因此負責的網站關閉了幾個透過網頁上傳檔案的功能,最近其他部門的專案同意了網站檔案上傳,準備上線前,同事B通報了一個小問題:第一次進入頁面時操作上傳沒辦法上傳成功,第二次卻可以成功?
初步線索是網站使用了MasterPage + UpdatePanel + + Panel 。
在msdn找到了UpdatePanel與FileUpload不相容的關鍵證詞,
文章後面提到了解題方法: 將送出檔案上傳的控制項(上傳按鈕)設為UpdatePanel的trigger項目,意思就是要一起postback!
筆記各種情境下的FileUpload:
Upload1.aspx
<body>
<form id="form1" runat="server">
<div>
<asp:FileUpload ID="FileUpload1" runat="server" />
<asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
</div>
</form>
</body>
Upload1.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
}
protected void Button1_Click(object sender, EventArgs e)
{
if (FileUpload1.HasFile)
{
Response.Write(string.Format("對照組1:收到檔案,檔案名稱是{0}", FileUpload1.FileName));
}
}
執行結果: 第一次就正常上傳!! simple is the best
透過HTML Form Data傳送檔案,Content type需要被設定為multipart/form-data。通常ASP.NET產生網頁到前端時都會自己設定好。
從Http header發現Content type 果然是 multipart/form-data,不做任何編碼將檔案上傳。
Upload2.aspx
<body>
<form id="form1" runat="server">
<div>
<asp:ScriptManager ID="ScriptManager1" runat="server"></asp:ScriptManager>
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:FileUpload ID="FileUpload1" runat="server" />
<asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
</ContentTemplate>
</asp:UpdatePanel>
</div>
</form>
</body>
Upload2.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
}
protected void Button1_Click(object sender, EventArgs e)
{
if (FileUpload1.HasFile)
{
Response.Write(string.Format("對照組2:收到檔案,檔案名稱是{0}", FileUpload1.FileName));
}
}
按下按鈕後,卻沒有收到檔案資訊,透過偵錯FileUpload.HasFile是False
依照msdn的建議,我們透過server side程式方式將上傳按鈕button1加入trigger
protected void Page_Load(object sender, EventArgs e)
{
UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = Button1.UniqueID });
}
果然成功上船了!
如果有加上trigger,Header中的content type = multipart/form-data
如果沒加上trigger,content type= application/x-www-form-urlencoded(預設值)
情境很貼近網站的現狀。
先新增一個主版頁面
<body>
<form id="form1" runat="server">
<div>
<asp:ScriptManager ID="ScriptManager1" runat="server"></asp:ScriptManager>
<asp:Label ID="Label1" runat="server" Text="我是主版頁面(MasterPage)"></asp:Label>
<br />
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>
</ContentTemplate>
</asp:UpdatePanel>
</div>
</form>
</body>
接著新增一個使用主版頁面的Web Form表單
Upload3.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Upload/Test.Master" AutoEventWireup="true" CodeBehind="Upload3.aspx.cs" Inherits="WebApplication1.Upload.Upload3" %>
<%@ MasterType VirtualPath="~/Upload/Test.Master" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
<asp:FileUpload ID="FileUpload1" runat="server" />
<asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
</asp:Content>
Upload3.aspx.cs
也類似對照組2的作法Update Panel Trigger 加入整個頁面postback的觸發控制項button1
protected void Page_Load(object sender, EventArgs e)
{
Master.UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = Button1.UniqueID });
}
protected void Button1_Click(object sender, EventArgs e)
{
if (FileUpload1.HasFile)
{
Response.Write(string.Format("實驗組1:收到檔案,檔案名稱是{0}", FileUpload1.FileName));
}
}
第一次就成功了!
疑,怎麼沒發生同事B通報的事件,案情不單純!
header中的content type = multipart/form-data
經過比對差異發現: 實際的程式FileUpload控制項放在一個初始visible=False的Panel容器中,我們接著再加一個實驗2。
Upload4.aspx
預設PnlDetail Visible = false
<%@ Page Title="" Language="C#" MasterPageFile="~/Upload/Test.Master" AutoEventWireup="true" CodeBehind="Upload4.aspx.cs" Inherits="WebApplication1.Upload.Upload4" %>
<%@ MasterType VirtualPath="~/Upload/Test.Master" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
<asp:Button ID="btnDetail" runat="server" Text="顯示上傳區域" OnClick="btnDetail_Click" />
<asp:Panel ID="PnlDetail" runat="server" Visible="false">
<asp:FileUpload ID="FileUpload1" runat="server" />
<asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
</asp:Panel>
</asp:Content>
Upload4.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
Master.UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = Button1.UniqueID });
}
protected void Button1_Click(object sender, EventArgs e)
{
if (FileUpload1.HasFile)
{
Response.Write(string.Format("實驗組2:收到檔案,檔案名稱是{0}", FileUpload1.FileName));
}
}
protected void btnDetail_Click(object sender, EventArgs e)
{
PnlDetail.Visible = true;
}
點選顯示上傳區域按鈕,瀏覽檔案上傳
出現了同事B通報的問題,postback時沒有一起把檔案傳送上來!!
果然Http header中的content type=application/x-www-form-urlencoded
Google搜尋後發現有些解答是在Page_Load加上這一行Page.Form.Attributes.Add("enctype", "multipart/form-data")
但似乎沒有效果!!
Upload4.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
Master.UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = Button1.UniqueID });
Page.Form.Attributes.Add("enctype", "multipart/form-data");
}
查看網頁原始檔,enctype="multipart/form-data"
但form post時,content type卻還是 application/x-www-form-urlencoded(預設值)
幾經波折,從stackoverflow得到一個可能原因,如果放置fileupload控制項的容器被設定visible=false,fileupload控制項就沒辦法render成html,間接影響form無法設定編碼種類為 enctype="multipart/form-data",最後造成fileupload控制項選取的檔案無法postback到sever。
解決的方向是想辦法在執行上傳前執行一次完整的postback。
我們試著在Master Page增加一個按鈕full post back作為整頁PostBack。
<asp:Button ID="Button1" runat="server" Text="full post back" OnClick="Button1_Click" />
protected void Button1_Click(object sender, EventArgs e)
{
}
測試看看!按下full post back按鈕,再瀏覽檔案然後上傳。
果然成功上傳了!不過這個解法很阿丹!哈!換下一帖!
protected void Page_Load(object sender, EventArgs e)
{
Master.UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = Button1.UniqueID });
Master.UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = btnDetail.UniqueID });
}
雖然也可以解決,但好像也不太適合!
Upload4.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
Master.UpdatePanel1.Triggers.Add(new PostBackTrigger { ControlID = Button1.UniqueID });
Page.Form.Attributes.Add("enctype", "multipart/form-data");
ScriptManager.GetCurrent(Page).RegisterPostBackControl(Button1);
}
3種解決方法都可以成功的在第一次執行就上傳成功,最後選擇了方案3,將上傳按鈕註冊到postback control中。
小結:
- 關鍵: Update Panel Trigger + RegisterPostBackControl
- 也許這就是Web Form的宿命,好用的UpdatePanel讓我們輕易局部更新,但總有點遺憾。
- 常常為了趕案子沒時間深入了解背後解題的原因,技術債就這麼欠下來了。
版本資訊:
Visual Studio 2015
參考:
HP Fortify Often Misused: File Upload 允許使用者上傳檔案可能會使攻擊者在伺服器執行已注入的危險內容或惡意程式碼?
FileUpload and UpdatePanel: ScriptManager.RegisterPostBackControl works the second time
New Issue with FileUpload in UpdatePanel - Works After first full post back
ScriptManager.RegisterPostBackControl Method