Why is getattr(obj, name) unsafe when the name string comes from user input?
answer
- The name is data, the lookup is not
- Python has no private access modifier
- One string picks any attribute on the object
- Underscores and dunders are reachable too
- Hand-written mapping of action to callable
basics
~20 sPython's getattr does an ordinary attribute lookup, so a user-chosen string can reach any attribute the object has: private underscore names, inherited methods and dunder attributes alike. Route user input through a dict of permitted names instead.
solid answer
~50 s`getattr(obj, name)` is the function form of `obj.name`, and it applies exactly the same lookup rules — instance `__dict__`, then the class and its bases, then `__getattr__`. Nothing about the call filters the string. So if `name` arrives from a query parameter or a JSON body, the caller picks any attribute on that object: a private `_token`, a method that was never meant to be a public action, or a dunder attribute like `__class__` or `__dict__` that exposes internals. The convention that a leading underscore means private is a convention only; it is not enforced. The fix is not to sanitise the string but to stop using it as a lookup key: keep an explicit `dict` mapping the small set of action names you support to the callables that implement them, and treat a miss as an unknown-action error.
code
python · 13 linesclass Report:
_internal_token = "s3cr3t"
def summary(self):
return "summary ok"
field = "_internal_token" # arrived as a query parameter
print(getattr(Report(), field)) # reaches a private attribute
HANDLERS = {"summary": Report.summary} # explicit dispatch table
handler = HANDLERS.get(field)
print(handler(Report()) if handler else "unknown action")go deeper
Recall that getattr(obj, name) is exactly obj.name with the name as a value, and that Python enforces no privacy. Be ready to say the fix in one line: a dict from allowed action names to functions.
Explain the lookup path the call follows — instance dict, class and bases, then getattr — and why that makes every inherited and dunder attribute reachable. Contrast an allowlist mapping with underscore filtering.
Show the review habit: trace the name argument back to its source and reject any path from request data. Talk about how getattr dispatch silently widens the public API each time someone adds a method to the class.
Own the design rule for the codebase — routing tables are declared, not discovered — and the enforcement story, such as a lint rule or review checklist that flags getattr and setattr whose name argument is not a literal.
## What `getattr` actually does `getattr(obj, "summary")` is precisely `obj.summary`, written so the attribute name can be a runtime value. It runs the standard attribute protocol: `type(obj).__getattribute__` looks in the instance `__dict__`, then walks the class and its bases (honouring descriptors such as `property` and plain functions bound as methods), and falls back to `__getattr__` if the class defines one. With a third argument it returns that default instead of raising `AttributeError`. The important part for security is what it does **not** do. It performs no validation of the name, no visibility check, and no distinction between attributes you consider part of your API and attributes that exist only because Python puts them there. Python has no private access modifier; a single leading underscore is a naming convention that tools and humans respect and the runtime ignores entirely. Name mangling for `__two_underscore` names is a compile-time rewrite of the *source*, not a runtime guard — the mangled name is still reachable by its mangled spelling. ## Why user input is the problem A dispatcher shaped like `getattr(controller, request_action)()` looks like elegant, table-free routing, and it is a standing invitation. The caller is choosing an attribute name on an object whose full attribute surface the author never enumerated. That surface includes: * every public method, including ones that were written for internal orchestration and never meant to be a request-triggered action; * every underscore-prefixed name, which the runtime treats as ordinary; * every attribute inherited from base classes, including ones from a framework superclass the author never read; * the dunder attributes that every object carries — `__class__`, `__dict__`, `__module__`, and on functions `__globals__`, which is the module's global namespace as a live mapping. That last group is the reason "just don't expose secrets as attributes" is not a defence: a chain of ordinary attribute lookups leads from any object into interpreter-level objects and from there into module globals. The general escalation is a topic of its own; the lesson at this level is simpler and unconditional. If a string from outside your process selects an attribute name, the attacker has chosen part of the program, not part of the data. `setattr` and `delattr` have the same property in the write direction, and `hasattr` leaks the same surface as a boolean oracle. ## What to do instead Turn the lookup into a mapping you wrote by hand: ```python HANDLERS = { "summary": build_summary, "export": build_export, } handler = HANDLERS.get(action) if handler is None: raise ValueError(f"unknown action: {action!r}") return handler(request) ``` This is the whole fix, and it is worth being precise about why it beats the alternatives: * **It is an allowlist by construction.** The set of reachable callables is exactly the set you typed. Adding a new method to the class does not widen the API by accident, which is the failure mode that makes `getattr` dispatch decay over time. * **Names are decoupled from implementation.** The wire name `"export"` need not equal the function name, so renaming a function is not a breaking API change and internal helpers cannot be summoned by their internal names. * **It reads as a routing table.** Reviewers see the full public surface in one place; with `getattr`, reviewing the surface means reading the whole class and its ancestry. * **A miss is a clean error.** `dict.get` returning `None` is a normal control-flow branch, whereas `AttributeError` from `getattr` is easy to catch too broadly and turn into a 500 or, worse, a stack trace echoed to the client. If you genuinely need names discovered at runtime — a plugin registry, a table of column formatters — build the mapping once at import time from a source you control (a decorator that registers, or an explicit list), and look up in that mapping. The rule is that the *set* of reachable names is fixed by your code; only the *choice* among them comes from the request. ## Where `getattr` is still fine None of this makes `getattr` a bad function. It is the right tool with a literal or internally-computed name: optional-attribute checks with a default, duck-typed protocol probing, serialisation over a field list you defined, and framework plumbing that already knows the names. The rule is narrow and easy to apply in review: **the name argument must never be, or be derived from, untrusted input.**
- Does rejecting names that begin with an underscore make getattr dispatch safe?No. That is a blocklist, and the interesting surface is not only underscored. Every public method of the class and of every base class stays reachable, including internal orchestration methods that were never meant to be request-triggered. A blocklist also has to be re-verified every time the class or a superclass gains a method, which nobody does. Test membership against an explicit set of permitted names, or use a mapping, so the reachable surface is the one you wrote.
- Is setattr with a user-supplied name any safer than getattr?It is worse in one respect: it writes. A user-chosen attribute name lets the caller rebind any attribute on the object, including internal limits, flags and cached state, and it can also create new attributes that later code reads. Nothing raises, so the damage is silent. Apply the same discipline in the write direction: an explicit allowlist of writable field names, checked before the call.
- When is getattr the right tool?Whenever the name is a literal or comes from code you control: probing for an optional attribute with a default, duck-typed checks, serialising a field list you defined, or framework plumbing over known names. The function is not the hazard; the hazard is the name argument tracing back to untrusted input.
It is the difference between a switchboard with labelled buttons and handing a caller the wiring diagram and letting them name any wire.
saying these in an interview costs you the question
- Claims a leading underscore makes an attribute unreachable
- Thinks double-underscore mangling is an access control
- Says getattr only finds methods, not data attributes
- Sanitises the name string instead of using a mapping
- Guards with hasattr, which leaks the same surface
- Believes a try/except AttributeError makes it safe