"Access modifiers are not a security mechanism" is a claim you will hear in most code reviews. Decide where it is literally true: in which language runtimes can untrusted code in the same process reach a private member, and where is the boundary genuinely enforced?
answer
- compile-time only: C++, Rust - erased in the binary
- Python __x = _Class__x, reversible
- Java: setAccessible vs module strong encapsulation (JDK 16 default)
- JS # fields: no reflective path, brand TypeError
- secrets need a process, not a modifier
basics
~20 sTrue wherever privacy is compile-time only: C++ and Rust erase it entirely, and Python's double underscore only mangles the name. Java's reflection can open members, but module strong encapsulation blocks that across boundaries. JavaScript's # fields have no reflective back door.
solid answer
~60 sThe claim is a generalisation from languages where privacy does not exist at run time; the exceptions matter. - **Erased at compile time - C++, Rust.** After compilation there is no privacy to check: a cast or a computed offset in C++, an `unsafe` transmute in Rust, reads any field and nothing can detect it. The modifier is a coupling tool only. - **Reversible convention - Python.** `__x` is mangled to `_Class__x` and readable by anyone who spells it out; `_x` is pure etiquette. - **Checked but openable - Java, .NET.** Java's `setAccessible(true)` opens members, but since JDK 16 strong encapsulation is the default, so it throws across module boundaries unless the owner declares `opens` or the launcher passes `--add-opens`. The real boundary moved from the class to the module. .NET reflection reads private members freely; Code Access Security was removed in .NET Core, so there is no sandbox behind it. - **Genuinely closed - JavaScript `#` fields.** Not enumerable, not reflectable, absent from JSON.stringify - unlike `Symbol` keys, which `Object.getOwnPropertySymbols` reveals.
code
python · 7 linesclass Vault:
def __init__(self):
self.__key = "s3cr3t"
v = Vault()
print(v._Vault__key) # 's3cr3t'
print(vars(v)) # {'_Vault__key': 's3cr3t'}go deeper
Know that visibility keeps honest code honest and that anything shipped to a client can be read, so secrets never live in the source.
Explain that C++, Rust and Python privacy vanishes at run time, and that Python's double underscore is name mangling.
Distinguish enforcement against accidents, against non-cooperating code, and against an adversary, and name the runtimes that actually deliver the middle case.
Set policy: visibility governs coupling and integrity, module or realm boundaries are the strongest in-process enforcement available, and any confidentiality requirement is answered with a process, a network boundary, or a server-held secret.
## What the slogan is actually claiming "Private is not a security boundary" bundles two different claims: that visibility is enforced only against *cooperating* code, and that a determined caller in the same process can always get through. The first is true everywhere. The second is true in most languages and false in a few, and knowing which is which decides whether you may ever put a secret behind a modifier. ## Erased at compile time In **C++**, access control is a rule the compiler applies to names; it produces no runtime artefact. A `reinterpret_cast` over the object's storage, a computed member offset, or a `memcpy` of the object reads any private field, and nothing at run time exists to notice. The standard even leaves a legal loophole: explicit template instantiation ignores access checks, which is how the well-known "private member access" idiom works without undefined behaviour. **Rust** is the same at heart: module privacy is a compile-time property of paths, and after monomorphisation the layout is just bytes. Safe code cannot reach a private field of another module, which is a real guarantee against *accidents* and against sound-by-construction libraries; `unsafe` transmutes or raw pointer arithmetic go straight through, and `#[repr(Rust)]` layout instability is the only thing that makes it awkward. In both languages the honest description is: the modifier controls *coupling*. It tells you who could have depended on this member, which is what makes refactoring safe. It tells you nothing about an adversary. ## Reversible convention **Python** has no access control at all. A leading underscore is a message to humans. A double leading underscore is name mangling - `self.__key` inside `class Vault` becomes the attribute `_Vault__key` - whose purpose is avoiding accidental collisions between a base class and a subclass, not preventing access. Anyone can read `v._Vault__key`, and `vars(v)` lists it. Ruby is similar in spirit: `send` bypasses private, and that is a documented, widely used feature rather than an exploit. ## Checked at run time but openable **Java** is the interesting case because the answer changed. Classic reflection lets you call `setAccessible(true)` on any member and read it. JDK 9's module system made packages non-open by default, and since JDK 16 *strong encapsulation* is on by default, so that call throws an InaccessibleObjectException across module boundaries unless the owning module declares `opens` for the package or the launcher is given `--add-opens`. So the enforceable boundary in modern Java is the **module**, not the class - and it is enforced against non-cooperating code, which package-private never was. Note also that the SecurityManager, the old sandbox story, is deprecated for removal, so the strategy of running untrusted code in-process with permission checks is gone. **.NET** allows reflective access to private members in full trust; Code Access Security existed on .NET Framework and was removed in .NET Core, so there is no supported in-process sandbox at all. The boundary there is the process. ## Genuinely closed **JavaScript's `#` private fields** are the strongest per-class privacy in wide use. They are not properties: they do not appear in `Object.getOwnPropertyNames` or `getOwnPropertySymbols`, are not serialised by `JSON.stringify`, are not reachable through a Proxy trap, and reading one on an object that does not carry the brand throws a TypeError. Contrast `Symbol`-keyed properties, which look private but are fully enumerable via `Object.getOwnPropertySymbols`, and closure-captured variables, which are genuinely unreachable but also untestable and re-allocated per instance. That said, "unreachable" means unreachable by *script in that realm*: it is not a defence against someone reading the bundle, a debugger, or the memory of the process. ## The decision rule 1. **Never put a secret behind a modifier.** Not in a browser bundle, not in a mobile binary, not in a JAR you ship. If the code runs on someone else's machine, they have the bytes. 2. **Use visibility for integrity and coupling.** It documents and enforces who may depend on a member, which is what makes an internal change safe. That value is real in every language on this list. 3. **When you need enforcement against non-cooperating code in-process, use the boundary the runtime actually enforces** - a JPMS module with no `opens`, JavaScript `#` fields with brand checks, a separate WebAssembly instance or a worker with a message interface. 4. **When you need enforcement against an adversary, use a process, a container, or a server.** The only trust boundaries that hold are the ones the operating system or the network draws. ## The one sentence to say The slogan is right about C++, Rust, Python and pre-module Java, wrong as a blanket claim about JavaScript's `#` fields and modern Java modules, and irrelevant to what visibility is really for - controlling coupling so you can change your own code.
- Given that Java reflection can open private members, in what sense did the module system change the answer?It moved the enforceable boundary from the class to the module. Inside a module, reflection is as permissive as it ever was; across a module boundary, setAccessible on a member of a package that is not opened throws InaccessibleObjectException, and that has been the default since JDK 16. So a library packaged as a named module with no opens directives can genuinely deny reflective access to non-cooperating code in the same JVM, which package-private and private never could. Callers who need in are expected to ask the owner for opens or to be launched with --add-opens, which makes the dependency explicit.
- A teammate proposes storing an API key in a private field of a frontend class because # fields are unreachable. What do you say?Unreachable by script in that realm is not the same as secret. The key is in the shipped bundle, visible in the network tab, in the debugger, and in process memory, so anyone running the page owns it. # fields are excellent for integrity - preventing accidental or hostile tampering with an object's internals by other code in the page - but the only place a secret survives is a server the client cannot read. The correct design is a backend that holds the key and exposes a scoped, revocable token.
saying these in an interview costs you the question
- Claiming Python's double underscore enforces access rather than mangling the attribute name.
- Asserting that Java reflection can always open any member, ignoring module strong encapsulation since JDK 16.
- Calling JavaScript's # fields a naming convention, or equating them with Symbol keys, which are enumerable via getOwnPropertySymbols.
- Proposing a SecurityManager or .NET Code Access Security as an in-process sandbox - both are gone.
- Storing a secret behind any visibility modifier in code that ships to a client.