-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Warn when [Authorize] is overridden by [AllowAnonymous] from "farther" away #56244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good. As soon as I saw this, I was ready to point out that you need to look for IAllowAnonymous
and not just AllowAnonymous
, but you already are.
@@ -315,4 +315,10 @@ | |||
<data name="Analyzer_UseAddAuthorizationBuilder_Title" xml:space="preserve"> | |||
<value>Use AddAuthorizationBuilder</value> | |||
</data> | |||
<data name="Analyzer_AuthorizeAttributeOverridden_Message" xml:space="preserve"> | |||
<value>This [Authorize] attribute is overridden by an [AllowAnonymous] attribute from farther away on '{0}'</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the title below is good, but for the message I think people who don't know what is going on will want some more detail.
More information:
- When authorization is run, AllowAnonymous takes precedence over Authorize. If both are present, then authorization doesn't run.
- MVC actions inherit (is that the right word? You should look at routing docs to see what it says) metadata from the controller. And controllers inherit metadata from base controls.
- Investigate your controllers and actions and decide whether to remove the [Authorize] attribute or move where the [AllowAnonymous] attribute is placed.
That detail could be in the message. Or there could be an aka.ms link here. Or it could be in the doc for the analyzer warning on learn.microsoft.com
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When authorization is run, AllowAnonymous takes precedence over Authorize. If both are present, then authorization doesn't run.
Does this need a bit of refinement? It sounds we are saying the filter doesn't run, but that's not truly the case. The filter still runs partially, it just doesn't call policy.AuthorizeAsync
. So maybe we can clarify a bit saying that in this case the filter will run the authentication phase but not the authorization phase. This is relevant because the authentication phase can change the principal.
See https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Authorization/AuthorizeFilter.cs#L161-L183 for details.
At least AuthenticateAsync
happens, which sets the principal on to the context. See https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authorization/Policy/src/PolicyEvaluator.cs#L56
[Fact] | ||
public async Task AuthorizeAfterAllowAnonymousOnAction_NoAttributeOnController_HasDiagnostics() | ||
{ | ||
var source = $$""" | ||
{{CommonPrefix}} | ||
public class MyController | ||
{ | ||
[AllowAnonymous] | ||
[{|#0:Authorize|}] | ||
public object Get() => new(); | ||
} | ||
"""; | ||
|
||
await VerifyCS.VerifyAnalyzerAsync(source, | ||
new DiagnosticResult(DiagnosticDescriptors.AuthorizeAttributeOverridden).WithArguments("MyController.Get").WithLocation(0) | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we really warn in this case? I understand that when AllowAnonymous
is on a controller its problematic, but here it should be obvious that the action is not secure
in the sense that it doesn't enforce authorization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An example of this (and I might be biased since I have experience in this field and I can probably agree it's not the common case) is for example when you are implementing multi-factor auth or social logins, where you have an action that receives the callback and has a custom Authorize(Scheme = "Social")
to make sure the cookie is read and the principal set on the request, and that checks within the action if the principal has the right information on it or performs a custom action if not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is why I currently don't warn when [AllowAnonymous]
is after [Authorize]
on the same symbol. That's the way to signal to the analyzer that you know [AllowAnonymous]
will win, so don't warn. We could never warn when the attributes are on the same symbol. That would probably be the least risky in terms of false positives. I discuss some other options in my earlier comment.
public async Task AuthorizeOnAction_AllowAnonymousOnBaseMethod_HasDiagnostics() | ||
{ | ||
var source = $$""" | ||
{{CommonPrefix}} | ||
public class MyControllerBase | ||
{ | ||
[AllowAnonymous] | ||
public virtual object Get() => new(); | ||
} | ||
|
||
public class MyController : MyControllerBase | ||
{ | ||
[{|#0:Authorize|}] | ||
public override object Get() => new(); | ||
} | ||
"""; | ||
|
||
await VerifyCS.VerifyAnalyzerAsync(source, | ||
new DiagnosticResult(DiagnosticDescriptors.AuthorizeAttributeOverridden).WithArguments("MyControllerBase.Get").WithLocation(0) | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to also cover situations where the signature is hidden?
[Route("[controller]")]
[AllowAnonymous]
public class MyBaseController : Controller
{
public IActionResult Index()
{
return View();
}
}
public class HomeController : MyBaseController
{
[Authorize]
public new IActionResult Index()
{
return View();
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the new
case, the closer attributes always win, right? And that's what we think people expect. So, there should be no relevant "farther away" [AllowAnonymous] attributes to warn about.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, my example got botched at some point. I've updated the comment to reflect that. Maybe I put the comment in the wrong place and was misinterpreted as related to overriding, but I was suggesting that we also covered the case with new, even if we "know" it works like other cases to ensure that if we find this case we don't produce the wrong result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looked at the tests so far. Overall, changes look good, with very thorough testing. Left some comments, and I'll take another pass at the implementation later.
How does the order impact the behavior in this case? My understanding was that it didn't matter, that the moment |
It doesn't. However, it could be somewhat sensible to have something like The idea was if you put I can think of two ways to improve this.
Or we can leave it as is with the assumption that accidently putting |
This is fine, thanks for the clarification. I think we are on the same page here, I just didn't see it :). |
I think what we have is fine. If you put the two things on the same symbol we shouldn't warn. That's the behavior, isn't it? If we do warn, then we should explain what's going on in the warning and suggest the alternative ordering to avoid the warning. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good.
My only remaining comment would be that we need to provide an extended error message that explains the situation in more detail.
If we can provide concrete examples based on what happens (for example, AllowAnonymous
on controller but Authorize
on action) and (Authorize
after AllowAnonymous
on the same action) that would be better I think.
As a rough example, the error message could be something like:
[AllowAnonymous]
was applied to the class <<Controller>>
. Unauthorized users will be able to invoke this action without the proper permissions and potentially create a security issue. If your action is explicitly designed to allow anonymous users, you can <> or disable this warning.
But essentially comes down to is explaining what's happening, what are the consequences, and what action they can take to avoid the warning.
On An analyzer like this is an opportunity for us to shift-left knowledge about how they interact together from a runtime bug to a build-time warning (@DamianEdwards am I using shift-left it right?). Having the warning is great, but a few sentences about why it is a warning is my ask, e.g. AllowAnonymous takes priority over Authorize, so using them together is pointless. And attributes can be inherited. |
- Improve comments and variable names
Thanks for the reviews. We can work on improving the warning title and message in the Analyzer proposal at #56310. I'll submit a follow up to address the feedback from the review. |
This PR adds analyzer that warns when
[Authorize]
is overridden by[AllowAnymous]
from farther away. A lot of people don't realize that the relative order of[Authorize]
and[AllowAnonymous]
does not matter, and incorrectly assume that if they put[Authorize]
closer to an MVC action than[AllowAnonymous]
, that it will still force authorization.At first, I wanted to make this work and change the runtime to ignore
[AllowAnonymous]
if we observe an[Authorize]
attribute that’s closer or at least log a warning. I tried this, but after seeing some of our test cases that started failing, I question if even logging a warning when there’sIAuthorizeData
metadata afterIAllowAnonymous
metadata because it’s just so common to add[Authorize]
using something like theapp.MapControllers().RequireAuthorization()
oroptions.Conventions.AuthorizeFolder()
. Both those examples would result false positives if there any individual endpoints with[AllowAnonymous]
because the[Authorize]
attribute gets added last programmatically. I think this kind of thing is very common in real-world applications where certain endpoints like a login page never need authorization.I now think we should stick solely with this analyzer that looks at the attributes on MVC controllers actions and base types in .NET 9. While this might not catch everything a runtime change would, I think the vast majority of the cases the analyzer would miss are false positives because they involve adding an authorization requirement programmatically which is an advanced scenario. Plus, the analyzer can catch some things that cannot be reliably caught at runtime, like
[Authorize]
coming after[AllowAnonymous]
in the same attribute list since the order of attributes using reflection is undefined.I’ve considered suggesting a code fix, but I’m leaning against it. The two main possibilities would be to recommend removing either the
[AllowAnonymous]
attribute or the[Authorize]
attribute, but I don’t think we should assume intent. And even if the intent was for the endpoint to allow anonymous access, removing something like[Authorize(AuthenticationSchemes = “Cookies”)]
could easily be breaking since the authorization middleware would no longer know which authentication scheme to try to authenticate with. This can matter even if authentication isn’t required to access the endpoint, because the behavior of the endpoint could change given an authenticated user.API Proposal: #56310
Fixes #43550