What's the real reason for preventing protected member access through a base/sibling class?
UPDATE: This question was the subject of my blog in January 2010. Thanks for the great question! See:
https://blogs.msdn.microsoft.com/ericlippert/2010/01/14/why-cant-i-access-a-protected-member-from-a-derived-class-part-six/
Does anyone have an example of a problem that would be caused by letting MyDerived access Base's protected members through a variable of type YourDerived or Base, but does not exist already when accessing them through a variable of type MyDerived or MySuperDerived?
I am rather confused by your question but I am willing to give it a shot.
If I understand it correctly, your question is in two parts. First, what attack mitigation justifies the restriction on calling protected methods through a less-derived type? Second, why does the same justification not motivate preventing calls to protected methods on equally-derived or more-derived types?
The first part is straightforward:
// Good.dll:
public abstract class BankAccount
{
abstract protected void DoTransfer(BankAccount destinationAccount, User authorizedUser, decimal amount);
}
public abstract class SecureBankAccount : BankAccount
{
protected readonly int accountNumber;
public SecureBankAccount(int accountNumber)
{
this.accountNumber = accountNumber;
}
public void Transfer(BankAccount destinationAccount, User user, decimal amount)
{
if (!Authorized(user, accountNumber)) throw something;
this.DoTransfer(destinationAccount, user, amount);
}
}
public sealed class SwissBankAccount : SecureBankAccount
{
public SwissBankAccount(int accountNumber) : base(accountNumber) {}
override protected void DoTransfer(BankAccount destinationAccount, User authorizedUser, decimal amount)
{
// Code to transfer money from a Swiss bank account here.
// This code can assume that authorizedUser is authorized.
// We are guaranteed this because SwissBankAccount is sealed, and
// all callers must go through public version of Transfer from base
// class SecureBankAccount.
}
}
// Evil.exe:
class HostileBankAccount : BankAccount
{
override protected void Transfer(BankAccount destinationAccount, User authorizedUser, decimal amount) { }
public static void Main()
{
User drEvil = new User("Dr. Evil");
BankAccount yours = new SwissBankAccount(1234567);
BankAccount mine = new SwissBankAccount(66666666);
yours.DoTransfer(mine, drEvil, 1000000.00m); // compilation error
// You don't have the right to access the protected member of
// SwissBankAccount just because you are in a
// type derived from BankAccount.
}
}
Dr. Evil's attempt to steal ONE... MILLION... DOLLARS... from your swiss bank account has been foiled by the C# compiler.
Obviously this is a silly example, and obviously, fully-trusted code could do anything it wants to your types -- fully-trusted code can start up a debugger and change the code as its running. Full trust means full trust. Don't actually design a real security system this way!
But my point is simply that the "attack" that is foiled here is someone attempting to do an end-run around the invariants set up by SecureBankAccount, to access the code in SwissBankAccount directly.
That answers your first question, I hope. If that's not clear, let me know.
Your second question is "Why doesn't SecureBankAccount also have this restriction?" In my example, SecureBankAccount says:
this.DoTransfer(destinationAccount, user, amount);
Clearly "this" is of type SecureBankAccount or something more derived. It could be any value of a more derived type, including a new SwissBankAccount. Couldn't SecureBankAccount be doing an end-run around SwissBankAccount's invariants?
Yes, absolutely! And because of that, the authors of SwissBankAccount are required to understand everything that their base class does! You can't just go deriving from some class willy-nilly and hope for the best! The implementation of your base class is allowed to call the set of protected methods exposed by the base class. If you want to derive from it then you are required to read the documentation for that class, or the code, and understand under what circumstances your protected methods will be called, and write your code accordingly. Derivation is a way of sharing implementation details; if you don't understand the implementation details of the thing you are deriving from then don't derive from it.
And besides, the base class is always written before the derived class. The base class isn't up and changing on you, and presumably you trust the author of the class to not attempt to break you sneakily with a future version. (Of course, a change to a base class can always cause problems; this is yet another version of the brittle base class problem.)
The difference between the two cases is that when you derive from a base class, you have the behaviour of one class of your choice to understand and trust. That is a tractable amount of work. The authors of SwissBankAccount are required to precisely understand what SecureBankAccount guarantees to be invariant before the protected method is called. But they should not have to understand and trust every possible behaviour of every possible cousin class that just happens to be derived from the same base class. Those guys could be implemented by anyone and do anything. You would have no ability whatsoever to understand any of their pre-call invariants, and therefore you would have no ability to successfully write a working protected method. Therefore, we save you that bother and disallow that scenario.
And besides, we have to allow you to call protected methods on receievers of potentially more-derived classes. Suppose we didn't allow that and deduce something absurd. Under what circumstances could a protected method ever be called, if we disallowed calling protected methods on receivers of potentially-more-derived classes? The only time you could ever call a protected method in that world is if you were calling your own protected method from a sealed class! Effectively, protected methods could almost never be called, and the implementation that was called would always be the most derived one. What's the point of "protected" in that case? Your "protected" means the same thing as "private, and can only be called from a sealed class". That would make them rather less useful.
So, the short answer to both your questions is "because if we didn't do that, it would be impossible to use protected methods at all." We restrict calls through less-derivedtypes because if we don't, it's impossible to safely implement any protected method that depends on an invariant. We allow calls through potential subtypes because if we do not allow this, then we don't allow hardly any calls at all.
Does that answer your questions?
Eric Lippert has explained it well in one of his blog posts.