<asp:Image ImageUrl="D:\Photos\a.jpg" /> 或 \\Server\Share\a.jpg 完全無效。瀏覽器只能請求 HTTP/HTTPS URL,無法直接訪問伺服器本地文件系統或 UNC 路徑。必須建立一個 HTTP 端點(.aspx/.ashx/.ashx),由伺服器端讀取文件並以二進制流輸出。
┌─────────────┐ HTTP GET ┌──────────────┐ FileStream ┌────────────────┐
│ 前端頁面 │ ──────────────→ │ ShowImg.ashx │ ─────────────────→ │ 磁碟/網絡路徑 │
│ <img src= │ ?path=D:\a.jpg │ (Generic Handler)│ 讀取二進制流 │ D:\ / \\UNC\ │
└─────────────┘ └──────────────┘ └────────────────┘
↑ │
│ Response.BinaryWrite(byte[]) │
└─────────────────────────────────────────────────────────────────────┘
<%@ WebHandler Language="C#" Class="ShowImage" %>
using System;
using System.IO;
using System.Web;
public class ShowImage : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string virtualPath = context.Request.QueryString["path"];
if (string.IsNullOrEmpty(virtualPath))
{
context.Response.StatusCode = 400;
context.Response.End();
return;
}
// 安全:路徑遍歷防護
string mappedPath;
try
{
// 僅允許配置白名單目錄下的文件
string[] allowedRoots = new[]
{
@"D:\Photos\",
@"\\FileServer\Share\"
};
string fullPath = Path.GetFullPath(virtualPath);
bool allowed = allowedRoots.Any(root => fullPath.StartsWith(Path.GetFullPath(root), StringComparison.OrdinalIgnoreCase));
if (!allowed)
{
context.Response.StatusCode = 403;
context.Response.End();
return;
}
mappedPath = fullPath;
}
catch
{
context.Response.StatusCode = 400;
context.Response.End();
return;
}
if (!File.Exists(mappedPath))
{
context.Response.StatusCode = 404;
context.Response.End();
return;
}
// 輸出圖片
context.Response.Clear();
context.Response.BufferOutput = false;
string ext = Path.GetExtension(mappedPath).ToLowerInvariant();
context.Response.ContentType = ext switch
{
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".bmp" => "image/bmp",
".webp" => "image/webp",
_ => "application/octet-stream"
};
context.Response.AddHeader("Content-Length", new FileInfo(mappedPath).Length.ToString());
context.Response.AddHeader("Cache-Control", "public, max-age=31536000"); // 1年緩存
// 高效流式輸出
using (FileStream fs = File.OpenRead(mappedPath))
{
fs.CopyTo(context.Response.OutputStream);
}
context.Response.Flush();
}
public bool IsReusable => true; // 無狀態,可複用
}
<asp:Image ID="Image1" runat="server"
ImageUrl='<%# "ShowImage.ashx?path=" + Server.UrlEncode(Eval("PhotoPath").ToString()) %>' />
// 或純 HTML
<img src="ShowImage.ashx?path=D%3A%5CPhotos%5Ca.jpg" />
public partial class ShowPic : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string rawPath = Request.QueryString["URL"];
if (string.IsNullOrEmpty(rawPath)) { Response.StatusCode = 400; Response.End(); return; }
// 安全驗證同 ASHX 方案
string safePath = ValidateAndMapPath(rawPath);
if (safePath == null) { Response.StatusCode = 403; Response.End(); return; }
Response.Clear();
Response.BufferOutput = false;
Response.ContentType = GetMimeType(safePath);
Response.AddHeader("Content-Length", new FileInfo(safePath).Length.ToString());
Response.TransmitFile(safePath); // 最優:內核傳輸
Response.End();
}
private string ValidateAndMapPath(string raw)
{
// 實現白名單、路徑遍歷檢查、UNC 支援...
// 返回安全的完整物理路徑或 null
}
private string GetMimeType(string path)
{
// 同 ASHX switch 表達式
}
}
| 風險 | 防護措施 |
|---|---|
| 路徑遍歷攻擊 (../../etc/passwd) | 1. `Path.GetFullPath` 規範化 2. 嚴格白名單根目錄前綴匹配 3. 拒絕相對路徑 |
| 敏感文件洩露 (web.config, .git) | 副檔名白名單(僅允許圖片/文檔)+ 內容類型驗證 |
| UNC 路徑權限問題 | 應用程式池身份設為有網絡共享讀取權限的域用戶,或使用 `WindowsIdentity.RunImpersonated` |
| 大文件佔用記憶體 | 禁止 `File.ReadAllBytes`,強制 `FileStream.CopyTo` 或 `TransmitFile` |
| 濫用導致帶寬/CPU 耗盡 | 加入速率限制(IP + Token)、Referer 檢查、簽名 URL 過期時間 |
using (WindowsIdentity.RunImpersonated(GetUserToken("DOMAIN\\User", "Password")))
{
using (FileStream fs = File.OpenRead(@"\\FileServer\Share\img.jpg"))
{
fs.CopyTo(Response.OutputStream);
}
}
// 獲取 Token 輔助方法 (需引用 System.Security.Principal, Microsoft.Win32.SafeHandles)
private SafeAccessTokenHandle GetUserToken(string userName, string password)
{
// P/Invoke LogonUser -> DuplicateTokenEx -> SafeAccessTokenHandle
// 詳細實現參考 Microsoft 文檔 "How to impersonate in ASP.NET"
}
| 方案 | 適用場景 | 優勢 | 劣勢 |
|---|---|---|---|
| IIS 虛擬目錄直連 | 公開靜態圖片、無權限控制 | 性能最強、零代碼 | 無動態權限、無法防盜鏈 |
| ASHX 代理 | 需權限、防盜鏈、動態水印 | 輕量、無頁面生命週期、易部署 | 需手寫緩存/ETag 邏輯 |
| ASMX 頁面代理 | 複用現有 Session/表單驗證 | 整合現有認證體系 | 頁面生命週期開銷大 |
**首選策略**:靜態資源走 IIS 直連,動態受保護資源走 ASHX + 白名單 + 流式輸出。