27

I have always wondered whether public, protected, and private has security implications post compilation.

Hypothetically:

class Foo 
{
public:
    int m_Foo; // Completely vulnerable and dangerous
    
protected:
    int m_Bar; // Possible attack vector from subclasses
    
private:
    int m_FooBar; // Totally secure
};

public members [by terminology alone] suggest that they are more vulnerable than private members, but I can not imagine how this could be taken advantage of post compilation in something like a proprietary program.

Questions:

  1. Pre-Compilation, are members unneccessarily left in public, a security concern?

  2. Why or why not?

  3. Post-Compilation, are members unneccessarily left in public, a security concern?

  4. Why or why not?

  5. Are there any historical or hypothetical examples of attacks which used public designated members as an attack vector?

16
  • 2
    I note that your question is reasonable in the context of redistributable (and licensed) COM objects (and their IDL), as a COM object could retain sensitive license information in a private field that should not be exposed via a public property - for example.
    – Dai
    Commented Nov 14, 2021 at 7:25
  • 1
    IDL is a language for describing object interfaces (like a C++ class declaration in a header file, but it's language-agnostic), and COM is... hard to explain, but I guess I'd explain COM as a way to "export" classes and other types from a Windows DLL so they can be reused by other applications without needing to statically-link anything. It's how OLE-DB, ActiveX, OLE, WinRT, VB6, VBA, Office Automation and more... just work. Because COM basically lets you export a C++ class so that a VBScript or Word document can use it matters how you design it, hence my link to COM licenses.
    – Dai
    Commented Nov 14, 2021 at 11:39
  • 2
    @Dai while licensing may feel "important" to a licensed library developer, I would not say that licenses are "security" topics. If there was a password stored in a private field in a COM object, I am pretty sure that somebody could use C++'s weak memory safety to "find" it. That may or may not apply to DCOM, I'm not sure. Most of the time licenses from my experience are not meant to be "securely" impossible to find, just something keep people honest and encourage people to pay as they should. Licenses are always merely obfuscation, because the program needs to access the license.
    – jrh
    Commented Nov 14, 2021 at 16:04
  • 2
    And to extend that, if hypothetically somebody trusted a "private" anything to be "secure" (e.g., by having some kind of "authentication" that assumed that private was "trusted" and public was "untrusted") that would be a severe security breach, because I could probably with enough effort bypass the public API and poke or call the private fields/methods directly, skipping your "authentication". Private is pretty much just for library devs to say "do not use this because it might change in future versions or isn't documented".
    – jrh
    Commented Nov 14, 2021 at 16:09
  • 1
    @Chuu reinterpret_cast does not work with DCOM. In fact, DCOM will be secure and trusted provided the remoted object's interface doesn't expose secrets via public properties.
    – Dai
    Commented Nov 15, 2021 at 21:58

9 Answers 9

98

Access modifiers like public/private/protected are not intended as a security boundary. And since C++ is not a memory-safe language, this cannot be a security boundary.

The laziest “attack” to access private members would be to reinterpret-cast the value to a struct with equivalent layout:

struct PublicFoo {
    int m_Foo;
    int m_Bar;
    int m_FooBar;
};

PublicFoo* attack(Foo* supposedly_secure) {
  return reinterpret_cast<PublicFoo*>(supposedly_secure);
}

In some cases, I have manually calculated offsets in order to access fields for objects that were created by a different library, e.g. to pick out the m_Bar field:

int attack_bar(Foo const* supposedly_secure) {
  const auto start_of_the_object = reinterpret_cast<char*>(supposedly_secure);
  const auto offset = sizeof(int);  // skip over m_Foo field
  const auto location = start_of_the_object + offset;
  return *reinterpret_cast<int*>(location);
}

So what are access modifiers for? They just help you to manage the data flows in your code. While you can circumvent access modifiers, you typically don't try to sabotage yourself. So if the field m_Foo must guarantee certain invariants, you want all modifications to that value to go through a method of your class. If you declare it private, then attempts to directly access this field will generate a helpful compiler error. This encapsulation helps you build more robust systems, which is especially helpful for larger projects or libraries.

In other languages like Java, access modifiers can sometimes serve as a security boundary. But that only works because Java is a memory-safe language with an explicit security model, so tricks like reinterpret-casting do not work (and reflection can be prevented). But in general, you should not trust language constructs to guarantee security. You would want real sandboxing technology for security boundaries, e.g. Linux Containers.

12
  • 35
    Yep. In short, access modifiers are for safety, not for security.
    – Doc Brown
    Commented Nov 13, 2021 at 12:24
  • 18
    @gnasher729 Yes, such memory safety violations are all UB. But C++ compilers don't calculate class layout via rand(). The layout of a particular compiler is quite predictable in practice. Here we have three int members so that there are no alignment/padding issues, but even if we had those that could be handled.
    – amon
    Commented Nov 13, 2021 at 14:14
  • 6
    Java is a memory-safe language JNI. That's my address space the JVM is running in. Commented Nov 13, 2021 at 19:06
  • 8
    @Anon In Rust, the same things are possible as in C++, I just need an unsafe{…} block to call std::mem::transmute() or to do pointer arithmetic. There is no “private” at run time. In Java, the equivalent “attack” would typically use reflection though there are security limits, especially since Java 9 modules. The JVM was designed to work as a sandbox that can run untrusted code, but it was never particularly reliable in this role compared e.g. with VMs for JavaScript. We'd need to agree on a threat model to analyze this in detail, e.g. does the attacker control the runtime environment.
    – amon
    Commented Nov 13, 2021 at 21:56
  • 7
    Worth noting that Java's security model has failed. That's why applets aren't allowed any more. Commented Nov 15, 2021 at 10:10
14

Using public and private correctly (and following good practices in general) helps you write better code with fewer bugs, and code with fewer bugs is typically harder for an attacker to exploit. However, private is not a security boundary, as other answers have already explained.

The same thing is true with many other aspects of a programming language, like excessive use of global variables. While global variables do not create security risks on their own, you're more likely to introduce buggy (and potentially exploitable) code when you use them excessively.

1
  • That's what I'm thinking, too. It's a way to enforce what your code should be changing and when. So, let's say I'm stupid enough to attempt something like: int someVariable; cin << someVariable; if (object.thingThatShouldNotBeChanged = someVariable && someVariable == 8008) doSomethingThatEndUserShouldNeverBeAbleToDo(); I've just thrown the door wide open for a malicious actor to come in and mess things up.
    – moonman239
    Commented Nov 16, 2021 at 19:58
8

Private and protected do not provide any security at all. Why? Because they are in your source code. They only affect code written by someone with access to your source code.

If I have access to your source code that includes “private” and “protected” then I just can change them both to “public”. Any possible protection is gone.

They protect you from your own or someone else’s stupidity. They don’t protect you from sabotage or attacks.

(They also encourage a better programming style, and serve as documentation- “private” means you shouldn’t touch this without a very good reason, or perhaps discussing with the original developer).

PS. You can override “final” functions easily - just edit the source code and remove the “final”. Same principle.

13
  • 7
    Ah yes, the classic #define private public before including a header file :)
    – amon
    Commented Nov 13, 2021 at 14:15
  • 3
    "They protect you from your own or someone else’s stupidity." - which very much has security implications. Most security vulnerabilities stem from stupidity (or negligence), not from sabotage. So while an unnecessary public or missing private is not a vulnerability in its own, it does increase the likelihood of issues.
    – Bergi
    Commented Nov 13, 2021 at 22:01
  • 1
    I've never seen a working protection against stupidity. IMO access modifiers prevent accidents, but you still need to be smart enough not to try and circumvent them, but instead stop and ask yourself - "what is the right and intended way to do this?"
    – Vilx-
    Commented Nov 13, 2021 at 22:02
  • 3
    @Amon I do believe you have earned the association with your namesake from Starcraft 2, which sought to unravel all of reality for all time.
    – Cort Ammon
    Commented Nov 13, 2021 at 23:26
  • 1
    Eric: if something is private in class A and class B thinks it should be public and modifiable, and the developer CAN change A and make things public, THAT is when you should think very hard about it.
    – gnasher729
    Commented Nov 15, 2021 at 9:01
7

I have always wondered whether public, protected, and private has security implications post compilation.

Since you're asking about C++, I'd say the answer is "not per se" (unless this leads you to shoot yourself in the foot down the line somehow). The reason (and this is something I'd really like to emphasize compared to the other explanations) is that in C++, almost all type information exists only at compile time. The compiler may include a little bit of type information in the binary to facilitate stuff like dynamic_cast, but otherwise it's done away with by and large even by the time the compiler outputs assembly language, let alone machine code. This is true even for C; a typical CPU has no idea if the address you're telling it to jump to truly holds the entry point of a function that takes and returns a double or if it actually holds a char in a list of rutabaga varieties—it just jumps and attempts to treat the next series of bits it encounters as an instruction, even if this leads to disaster. Likewise, no model of CPU I've ever heard of understands what a C++ class is, let alone varying degrees of class member access.

When people talk about security vulnerabilities in a programing context, they generally mean ways to get the program to perform other-than-intended behavior from the outside at runtime. A classic example is failing to check that user input can fit into a fixed-size buffer before storing it there: too-large input can cause your program to write the input past the end of the buffer in memory. With sufficient knowledge, an attacker might be able to craft input to that part of the program that will be written to a location the processor will try to proceed from later, allowing them to take control of the process.

Since member access specifiers like private and protected aren't represented directly in the binary, they wouldn't be said to have "security implications" in that sense. Rather, private and protected are to shield users of a public interface you're designing from depending on parts of the code that you imagine they'd rather not depend on, generally because you don't want to make the same guarantees about them that you do for the public API. They're very much for the benefit of human programmers and don't do anything on their own to guarantee correct program behavior at runtime. On that note, if you have some insecure code, it doesn't help to make it non-public; it's still getting run at some point, presumably, so the vulnerability may still be exploitable through some outside interface that leads to that code path. Instead, you should fix it and alert anyone who might be affected.

In general, if you want to know what sorts of things in your C++ code do exist in some direct way at runtime and how, a great thing to do is to have your compiler stop after compilation proper and output assembly. g++ takes the -S flag for this purpose, for instance, and you can pick between att and intel syntax with -masm=[DIALECT]. You can also walk through your program instruction-by-instruction in a debugger like gdb (see the stepi and nexti commands there, and maybe also the documentation for TUI mode which is very helpful when doing this).

If you're not familiar with the instructions and features supported by the processor you're targeting, you can learn about it by reading the programmer's manuals for the processor, which are often freely available; for x86 those are Intel's. You can also pick up a more tutorial-style book about working close-to-the-metal (I like Low-Level Programming by Igor Zhirkov).

C++-centric resources do often make it clear when something has runtime implications if it's required by the standard. However, the standard gives a lot of latitude to compiler authors at the hardware level as C++ is designed to be portable. As such, in order to develop an intuition for how your code might look post-compilation, I'd say it's easier to look at things from the hardware side for starters. You know that whatever's happening it must be terms the processor can understand, and there's only so many things you can say to a given processor. A bit past that, it can help to study how C compilers work in order to get a foothold—C is much simpler and closer-to-the-metal than C++ on the whole, so it's easier to understand from that angle, and much of the most "dangerous" parts of C++ are the parts that are shared with C or closeby to them. Studying how the operating system you're targeting works in detail is also helpful, especially its process model and memory management features (provided that you're not writing code to run on bare metal or the like).

Things are different if you're running code on an interpreter. C++ generally isn't run this way of course, but with many "higher-level" languages it's possible to exploit the behavior of the interpreter in a way that's far above the level of the processor, such as the classic case of trying to get a web server to send malicious SQL commands to a database. Of course, if you write an interpreter in C++, you can open up that whole can of worms, but that's not a C++-specific topic exactly.

As a postscript, there actually is a sense in which member access specifiers can have a direct effect on the behavior of your program at runtime. If you put two separate sections in a class with the same access specifier, the compiler is free to reorder them. For instance, with

class C {
public:
    int n;
public:
    int m;
};

the compiler is free to put m before n in memory. If your program's behavior depends on the data members of instances of C being laid out a certain way, this may cause bugs, depending on the compiler. I imagine that's not quite what you had in mind, though.

3

No

Reason: Once compiled, private, protected and public are gone.


In C++, you can think about these keywords as nothing more or less than directives to tell the compiler that certain member accesses are invalid code. I.e, they instruct the compiler where it should fail with an error. That's in the same vein as including a static_assert() in your code: It makes the code invalid if a certain assumption does not hold.

Since error messages are only generated while compiling, the output of a successful compiler run contains no remains of these three keywords. The compiler has checked the rules, your code complies and is translated to machine code. Both the compiler and the access modifiers have done their work at this point and exit the stage. What happens when the compiled code is executed is none of their business anymore.

This is especially true since C++ allows programmers to shoot themselves by circumventing all the safeguards that the compiler enforces. For instance, consider this simple example:


class Foo {
    public:
        Foo() = default;
        const int& getFoo() const {
            return foo;
        }
    private:
        int foo = 0;
};

Foo myFoo;
const_cast<int&>(myFoo->getFoo()) = 666;    //insert evil value into myFoo
cout << "My number is " << myFoo->getFoo() << "!\n";    // a truly evil message...

"All" that the evil programmer has done, is to cast away constness from a reference (as so much existing code does all over the place), and it has completely circumvented the private "protection" of foo. The compiler doesn't care that the reference points to a private member, it only considers that the reference is const qualified, and that the programmer has explicitly stated to ignore that const qualifier. And since the C++ compiler treats the programmer as an omniscient god who surely knows better, it simply follows its orders.

2

For public/protected/private to be making a difference, you would have to be allowing someone to compile and link their C++ code against your headers & binary.

If someone already has that much access to your system, then you have already lost from a security standpoint. As other answers have mentioned, C++ code that you allow to run in the same address space as your code can potentially access everything in that address space. private is just an instruction to the compiler not to compile a direct access to the field, but you can use one of many techniques to indirectly access the memory where the field is stored anyway. So none of the following will be safe:

  • your code
  • anything loaded into memory by your code
  • anything external that your code has permission to access

If there are any data or methods that you cannot allow an attacker to access, then you simply cannot allow them to compile their code against yours and have it run. i.e. you need to run your code on a machine you control and "security implications" are all about the interface through which you allow others to interact with the machine. This makes public/protected/private totally irrelevant to your security; an attacker doesn't need to care about them until they've already managed to get your machine to link their code to yours and execute it, which is far too late to stop them doing anything.

1
  • Based Ben. I was kind of waiting for this to be addressed given that when you write plugins for certain programs, you have to interface with one of its factory classes.
    – Anon
    Commented Nov 16, 2021 at 3:21
1

Security implies that you wish to protect something. Anyone with the right tools can disassemble the binary, look at what is being done, and, with enough time and experience, can reasonably infer the original code. Or at least that is the way you should treat your code in a security related environment.

If there is something actually secret, you should not distribute it, or use some tested and proofed encryption scheme to guard it. It should still be hard to break, even if an attacker knows every last detail of how the secret is created, except the decryption key.

But, to come back to your question, there is one instance I can think of, where handling of private and public differs after compilation: dynamic libraries. The compiler has to keep all public members and functions visible, but I could envision that there is no such guarantee for private members, especially if they are const or constexpr.

0

I would explain it by comparing public, protected, and private to the locks on bathroom stalls. You should honor those locks because honoring those locks will save you from unpleasantness.

But at the same time, you should not store your valuables in the stall and count on the stall lock to protect them.

-1

I didn't see that the OP specified the question was specific to statically compiled languages. In dynamically loaded languages (ie; java) leaving unintended public entry points is definitely an invitation to hacking.

Not the answer you're looking for? Browse other questions tagged or ask your own question.