1 //------------------------------------------------------------------------------
2 // <copyright file="FileAuthorizationModule.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 //------------------------------------------------------------------------------
8 * FileAclAuthorizationModule class
10 * Copyright (c) 1999 Microsoft Corporation
13 namespace System
.Web
.Security
{
14 using System
.Runtime
.Serialization
;
17 using System
.Web
.Caching
;
18 using System
.Web
.Util
;
19 using System
.Web
.Configuration
;
20 using System
.Collections
;
21 using System
.Collections
.Specialized
;
22 using System
.Security
.Principal
;
23 using System
.Globalization
;
24 using System
.Security
.Permissions
;
25 using System
.Runtime
.InteropServices
;
26 using System
.Web
.Management
;
27 using System
.Web
.Hosting
;
28 using System
.Collections
.Generic
;
29 using System
.Diagnostics
.CodeAnalysis
;
35 /// Verifies that the remote user has NT permissions to access the
39 public sealed class FileAuthorizationModule
: IHttpModule
{
44 /// Initializes a new instance of the <see cref='System.Web.Security.FileAuthorizationModule'/>
48 [SecurityPermission(SecurityAction
.Demand
, UnmanagedCode
=true)]
49 public FileAuthorizationModule() {
52 private static bool s_EnabledDetermined
;
53 private static bool s_Enabled
;
56 [SecurityPermission(SecurityAction
.Demand
, UnmanagedCode
= true)]
57 public static bool CheckFileAccessForUser(String virtualPath
, IntPtr token
, string verb
) {
58 if (virtualPath
== null)
59 throw new ArgumentNullException("virtualPath");
60 if (token
== IntPtr
.Zero
)
61 throw new ArgumentNullException("token");
63 throw new ArgumentNullException("verb");
64 VirtualPath vPath
= VirtualPath
.Create(virtualPath
);
66 if (!vPath
.IsWithinAppRoot
)
67 throw new ArgumentException(SR
.GetString(SR
.Virtual_path_outside_application_not_supported
), "virtualPath");
69 if (!s_EnabledDetermined
) {
70 if (HttpRuntime
.UseIntegratedPipeline
) {
71 s_Enabled
= true; // always enabled in Integrated Mode
74 HttpModulesSection modulesSection
= RuntimeConfig
.GetConfig().HttpModules
;
75 int len
= modulesSection
.Modules
.Count
;
76 for (int iter
= 0; iter
< len
; iter
++) {
77 HttpModuleAction module
= modulesSection
.Modules
[iter
];
78 if (Type
.GetType(module
.Type
, false) == typeof(FileAuthorizationModule
)) {
84 s_EnabledDetermined
= true;
88 ////////////////////////////////////////////////////////////
89 // Step 3: Check the cache for the file-security-descriptor
90 // for the requested file
92 FileSecurityDescriptorWrapper oSecDesc
= GetFileSecurityDescriptorWrapper(vPath
.MapPath(), out freeDescriptor
);
94 ////////////////////////////////////////////////////////////
95 // Step 4: Check if access is allowed
97 if (verb
== "GET" || verb
== "POST" || verb
== "HEAD" || verb
== "OPTIONS")
99 bool fAllowed
= oSecDesc
.IsAccessAllowed(token
, iAccess
);
101 ////////////////////////////////////////////////////////////
102 // Step 5: Free the security descriptor if adding to cache failed
104 oSecDesc
.FreeSecurityDescriptor();
111 /// <para>[To be supplied.]</para>
113 public void Init(HttpApplication app
) {
114 app
.AuthorizeRequest
+= new EventHandler(this.OnEnter
);
119 /// <para>[To be supplied.]</para>
121 public void Dispose() {
124 void OnEnter(Object source
, EventArgs eventArgs
) {
125 if (HttpRuntime
.IsOnUNCShareInternal
)
126 return; // don't check on UNC shares -- the user token is bogus anyway
130 app
= (HttpApplication
)source
;
131 context
= app
.Context
;
133 if (!IsUserAllowedToFile(context
, null)) {
134 context
.Response
.SetStatusCode(401, subStatus
: 3);
135 WriteErrorMessage(context
);
136 app
.CompleteRequest();
140 internal static bool IsWindowsIdentity(HttpContext context
) {
141 return context
.User
!= null &&
142 context
.User
.Identity
!= null &&
143 context
.User
.Identity
is WindowsIdentity
;
146 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification
= "This method is not dangerous.")]
147 private static bool IsUserAllowedToFile(HttpContext context
, string fileName
) {
148 ////////////////////////////////////////////////////////////
149 // Step 1: Check if this is WindowsLogin
150 // It's not a windows authenticated user: allow access
151 if (!IsWindowsIdentity(context
)) {
155 if (fileName
== null) {
156 fileName
= context
.Request
.PhysicalPathInternal
;
159 bool isAnonymousUser
= (context
.User
== null || !context
.User
.Identity
.IsAuthenticated
);
160 CachedPathData pathData
= null;
162 HttpVerb verb
= context
.Request
.HttpVerb
;
164 if (verb
== HttpVerb
.GET
165 || verb
== HttpVerb
.POST
166 || verb
== HttpVerb
.HEAD
167 || context
.Request
.HttpMethod
== "OPTIONS")
171 ////////////////////////////////////////////////////////////
172 // iff it's a GET or POST or HEAD or OPTIONs verb, we can use the cached result
173 if (!CachedPathData
.DoNotCacheUrlMetadata
) {
174 pathData
= context
.GetConfigurationPathData();
175 // as a perf optimization, we cache results for annoymous access
176 // to CachedPathData.PhysicalPath, and avoid doing the full check
177 if (!StringUtil
.EqualsIgnoreCase(fileName
, pathData
.PhysicalPath
)) {
178 // set to null so we don't attempt to update it after the full check below
182 if (pathData
.AnonymousAccessAllowed
) { // fast path when everyone has access
183 Debug
.Trace("FAM", "IsUserAllowedToFile: pathData.AnonymousAccessAllowed");
186 if (pathData
.AnonymousAccessChecked
&& isAnonymousUser
) { // fast path for anonymous user
187 // another thread could be modifying CachedPathData, so return the
188 // value of AnonymousAccessAllowed instead of assuming it is false
189 Debug
.Trace("FAM", "IsUserAllowedToFile: pathData.AnonymousAccessChecked && isAnonymousUser");
190 return pathData
.AnonymousAccessAllowed
;
197 // Step 3: Check the cache for the file-security-descriptor
198 // for the requested file
200 FileSecurityDescriptorWrapper oSecDesc
= GetFileSecurityDescriptorWrapper(fileName
, out freeDescriptor
);
202 ////////////////////////////////////////////////////////////
203 // Step 4: Check if access is allowed
205 if (iAccess
== 1) { // iff it's a GET or POST or HEAD or OPTIONs verb, we can cache the result
206 if (oSecDesc
._AnonymousAccessChecked
&& isAnonymousUser
) {
207 Debug
.Trace("FAM", "IsUserAllowedToFile: oSecDesc._AnonymousAccessChecked && isAnonymousUser");
208 fAllowed
= oSecDesc
._AnonymousAccess
;
211 Debug
.Trace("FAM", "IsUserAllowedToFile: calling oSecDesc.IsAccessAllowed with iAccess == 1");
212 fAllowed
= oSecDesc
.IsAccessAllowed(context
.WorkerRequest
.GetUserToken(), iAccess
);
215 if (!oSecDesc
._AnonymousAccessChecked
&& isAnonymousUser
) {
216 oSecDesc
._AnonymousAccess
= fAllowed
;
217 oSecDesc
._AnonymousAccessChecked
= true;
220 // Cache results in CachedPathData if the file exists and annonymous access has been checked.
221 // Note that if CachedPathData.Exists is false, then it does not have a dependency on the file path,
222 // and won't be expunged if the file changes.
223 if (pathData
!= null && pathData
.Exists
&& oSecDesc
._AnonymousAccessChecked
) {
224 Debug
.Trace("FAM", "IsUserAllowedToFile: updating pathData");
225 pathData
.AnonymousAccessAllowed
= oSecDesc
._AnonymousAccess
;
226 pathData
.AnonymousAccessChecked
= true;
230 Debug
.Trace("FAM", "IsUserAllowedToFile: calling oSecDesc.IsAccessAllowed with iAccess != 1");
231 fAllowed
= oSecDesc
.IsAccessAllowed(context
.WorkerRequest
.GetUserToken(), iAccess
); // don't cache this anywhere
234 ////////////////////////////////////////////////////////////
235 // Step 5: Free the security descriptor if adding to cache failed
237 oSecDesc
.FreeSecurityDescriptor();
240 WebBaseEvent
.RaiseSystemEvent(null, WebEventCodes
.AuditFileAuthorizationSuccess
);
243 if (!isAnonymousUser
)
244 WebBaseEvent
.RaiseSystemEvent(null, WebEventCodes
.AuditFileAuthorizationFailure
);
249 private static FileSecurityDescriptorWrapper
GetFileSecurityDescriptorWrapper(string fileName
, out bool freeDescriptor
) {
250 if (CachedPathData
.DoNotCacheUrlMetadata
) {
251 freeDescriptor
= true;
252 return new FileSecurityDescriptorWrapper(fileName
);
255 freeDescriptor
= false;
256 string oCacheKey
= CacheInternal
.PrefixFileSecurity
+ fileName
;
257 FileSecurityDescriptorWrapper oSecDesc
= HttpRuntime
.Cache
.InternalCache
.Get(oCacheKey
) as FileSecurityDescriptorWrapper
;
259 // If it's not present in the cache, then create it and add to the cache
260 if (oSecDesc
== null) {
261 Debug
.Trace("FAM", "GetFileSecurityDescriptorWrapper: cache miss for " + fileName
);
262 oSecDesc
= new FileSecurityDescriptorWrapper(fileName
);
263 string cacheDependencyPath
= oSecDesc
.GetCacheDependencyPath();
264 if (cacheDependencyPath
!= null) {
265 // Add it to the cache: ignore failures, since a different thread may have added it or the file doesn't exist
267 Debug
.Trace("FAM", "GetFileSecurityDescriptorWrapper: inserting into cache with dependency on " + cacheDependencyPath
);
268 CacheDependency dependency
= new CacheDependency(0, cacheDependencyPath
);
269 TimeSpan slidingExp
= CachedPathData
.UrlMetadataSlidingExpiration
;
270 HttpRuntime
.Cache
.InternalCache
.Insert(oCacheKey
, oSecDesc
, new CacheInsertOptions() {
271 Dependencies
= dependency
,
272 SlidingExpiration
= slidingExp
,
273 OnRemovedCallback
= new CacheItemRemovedCallback(oSecDesc
.OnCacheItemRemoved
)
275 } catch (Exception e
){
276 Debug
.Trace("internal", e
.ToString());
277 freeDescriptor
= true;
285 private void WriteErrorMessage(HttpContext context
) {
286 if (!context
.IsCustomErrorEnabled
) {
287 context
.Response
.Write((new FileAccessFailedErrorFormatter(context
.Request
.PhysicalPathInternal
)).GetErrorMessage(context
, false));
289 context
.Response
.Write((new FileAccessFailedErrorFormatter(null)).GetErrorMessage(context
, true));
291 // In Integrated pipeline, ask for handler headers to be generated. This would be unnecessary
292 // if we just threw an access denied exception, and used the standard error mechanism
293 context
.Response
.GenerateResponseHeadersForHandler();
297 static internal bool RequestRequiresAuthorization(HttpContext context
) {
300 FileSecurityDescriptorWrapper oSecDesc
;
303 if (!IsWindowsIdentity(context
)) {
307 oCacheKey
= CacheInternal
.PrefixFileSecurity
+ context
.Request
.PhysicalPathInternal
;
309 sec
= HttpRuntime
.Cache
.InternalCache
.Get(oCacheKey
);
311 // If it's not present in the cache, then return true
312 if (sec
== null || !(sec
is FileSecurityDescriptorWrapper
))
315 oSecDesc
= (FileSecurityDescriptorWrapper
) sec
;
316 if (oSecDesc
._AnonymousAccessChecked
&& oSecDesc
._AnonymousAccess
)
321 internal static bool IsUserAllowedToPath(HttpContext context
, VirtualPath virtualPath
)
323 return IsUserAllowedToFile(context
, virtualPath
.MapPath());
327 /////////////////////////////////////////////////////////////////////////////
328 /////////////////////////////////////////////////////////////////////////////
329 /////////////////////////////////////////////////////////////////////////////
330 internal class FileSecurityDescriptorWrapper
: IDisposable
{
331 ~
FileSecurityDescriptorWrapper() {
332 FreeSecurityDescriptor();
335 /////////////////////////////////////////////////////////////////////////////
336 /////////////////////////////////////////////////////////////////////////////
337 internal FileSecurityDescriptorWrapper(String strFile
) {
338 _FileName
= FileUtil
.RemoveTrailingDirectoryBackSlash(strFile
);
339 _securityDescriptor
= UnsafeNativeMethods
.GetFileSecurityDescriptor(_FileName
);
342 /////////////////////////////////////////////////////////////////////////////
343 /////////////////////////////////////////////////////////////////////////////
344 internal bool IsAccessAllowed(IntPtr iToken
, int iAccess
) {
345 if (iToken
== IntPtr
.Zero
)
348 if (_SecurityDescriptorBeingFreed
)
349 return IsAccessAllowedUsingNewSecurityDescriptor(iToken
, iAccess
);
351 _Lock
.AcquireReaderLock();
354 if (!_SecurityDescriptorBeingFreed
) {
355 if (_securityDescriptor
== IntPtr
.Zero
)
357 if (_securityDescriptor
== UnsafeNativeMethods
.INVALID_HANDLE_VALUE
)
360 return (UnsafeNativeMethods
.IsAccessToFileAllowed(_securityDescriptor
, iToken
, iAccess
) != 0);
363 _Lock
.ReleaseReaderLock();
369 return IsAccessAllowedUsingNewSecurityDescriptor(iToken
, iAccess
);
372 /////////////////////////////////////////////////////////////////////////////
373 /////////////////////////////////////////////////////////////////////////////
374 private bool IsAccessAllowedUsingNewSecurityDescriptor(IntPtr iToken
, int iAccess
) {
375 if (iToken
== IntPtr
.Zero
)
378 IntPtr secDes
= UnsafeNativeMethods
.GetFileSecurityDescriptor(_FileName
);
379 if (secDes
== IntPtr
.Zero
)
381 if (secDes
== UnsafeNativeMethods
.INVALID_HANDLE_VALUE
)
386 return (UnsafeNativeMethods
.IsAccessToFileAllowed(secDes
, iToken
, iAccess
) != 0);
388 UnsafeNativeMethods
.FreeFileSecurityDescriptor(secDes
);
395 /////////////////////////////////////////////////////////////////////////////
396 /////////////////////////////////////////////////////////////////////////////
397 internal void OnCacheItemRemoved(String key
, Object
value, CacheItemRemovedReason reason
) {
398 FreeSecurityDescriptor();
401 /////////////////////////////////////////////////////////////////////////////
402 /////////////////////////////////////////////////////////////////////////////
403 internal void FreeSecurityDescriptor() {
404 if (!IsSecurityDescriptorValid())
406 _SecurityDescriptorBeingFreed
= true;
408 _Lock
.AcquireWriterLock();
411 if (!IsSecurityDescriptorValid())
413 // VSWHIDBEY 493667: double free in webengine!FreeFileSecurityDescriptor()
414 IntPtr temp
= _securityDescriptor
;
415 _securityDescriptor
= UnsafeNativeMethods
.INVALID_HANDLE_VALUE
;
416 UnsafeNativeMethods
.FreeFileSecurityDescriptor(temp
);
418 _Lock
.ReleaseWriterLock();
425 /////////////////////////////////////////////////////////////////////////////
426 /////////////////////////////////////////////////////////////////////////////
427 internal bool IsSecurityDescriptorValid() {
429 _securityDescriptor
!= UnsafeNativeMethods
.INVALID_HANDLE_VALUE
&&
430 _securityDescriptor
!= IntPtr
.Zero
;
433 /////////////////////////////////////////////////////////////////////////////
434 /////////////////////////////////////////////////////////////////////////////
435 internal string GetCacheDependencyPath() {
436 // if security descriptor is invalid, we cannot cache it
437 if (_securityDescriptor
== UnsafeNativeMethods
.INVALID_HANDLE_VALUE
) {
438 Debug
.Trace("FAM", "GetCacheDependencyPath: invalid security descriptor");
441 // if security descriptor is valid (file exists), cache it with a dependency on the file name
442 if (_securityDescriptor
!= IntPtr
.Zero
) {
443 Debug
.Trace("FAM", "GetCacheDependencyPath: valid security descriptor");
446 // file does not exist, but if it's path is beneath the app root, we will cache it and
447 // use the first existing directory as the cache depenedency path
448 string existingDir
= FileUtil
.GetFirstExistingDirectory(AppRoot
, _FileName
);
451 if (existingDir
!= null) {
452 Debug
.Trace("FAM", "GetCacheDependencyPath: beneath app root");
455 Debug
.Trace("FAM", "GetCacheDependencyPath: not beneath app root");
461 /////////////////////////////////////////////////////////////////////////////
462 /////////////////////////////////////////////////////////////////////////////
463 private static string AppRoot
{
465 string appRoot
= _AppRoot
;
466 if (appRoot
== null) {
467 InternalSecurityPermissions
.AppPathDiscovery
.Assert();
468 appRoot
= Path
.GetFullPath(HttpRuntime
.AppDomainAppPathInternal
);
469 appRoot
= FileUtil
.RemoveTrailingDirectoryBackSlash(appRoot
);
475 void IDisposable
.Dispose()
477 FreeSecurityDescriptor();
478 GC
.SuppressFinalize(this);
480 private IntPtr _securityDescriptor
;
481 internal bool _AnonymousAccessChecked
= false;
482 internal bool _AnonymousAccess
= false;
483 private bool _SecurityDescriptorBeingFreed
= false;
484 private string _FileName
= null;
485 private ReadWriteSpinLock _Lock
= new ReadWriteSpinLock();
486 private static string _AppRoot
= null;
489 /////////////////////////////////////////////////////////////////////////////
490 /////////////////////////////////////////////////////////////////////////////
491 /////////////////////////////////////////////////////////////////////////////
492 internal class FileAccessFailedErrorFormatter
: ErrorFormatter
{
493 private String _strFile
;
495 internal FileAccessFailedErrorFormatter(string strFile
) {
497 if (_strFile
== null)
498 _strFile
= String
.Empty
;
501 protected override string ErrorTitle
{
502 get { return SR.GetString(SR.Assess_Denied_Title);}
503 //get { return "Access Denied Error";}
506 protected override string Description
{
508 return SR
.GetString(SR
.Assess_Denied_Description3
);
509 //return "An error occurred while accessing the resources required to serve this request. This typically happens if you do not have permissions to view the file you are trying to access.";
513 protected override string MiscSectionTitle
{
514 get { return SR.GetString(SR.Assess_Denied_Section_Title3); }
515 //get { return "Error message 401.3";}
518 protected override string MiscSectionContent
{
521 if (_strFile
.Length
> 0)
522 miscContent
= SR
.GetString(SR
.Assess_Denied_Misc_Content3
, HttpRuntime
.GetSafePath(_strFile
));
523 //return "Access is denied due to NT ACLs on the requested file. Ask the web server's administrator to give you access to "+ _strFile + ".";
525 miscContent
= SR
.GetString(SR
.Assess_Denied_Misc_Content3_2
);
527 AdaptiveMiscContent
.Add(miscContent
);
532 protected override string ColoredSquareTitle
{
536 protected override string ColoredSquareContent
{
540 protected override bool ShowSourceFileInfo
{