fix(security): annotate AppUser.password with @JsonIgnore to prevent accidental hash leakage #110
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
UserControllercurrently guards against password hash exposure by callinguser.setPassword(null)before returning the entity. This is fragile: any new endpoint, refactor, or projection that returns anAppUserwithout that manual null-out will silently leak the BCrypt hash.Risk
BCrypt hashes are not reversible in practice, but exposing them:
Fix
Add
@JsonIgnoreto thepasswordfield on theAppUserentity:Then remove the
user.setPassword(null)call(s) from controllers — they become dead code.Acceptance Criteria
GET /api/usersandGET /api/users/meresponses contain nopasswordfieldGET /api/users/{id}response contains nopasswordfield👨💻 Felix Brandt — Senior Fullstack Developer
Questions & Observations
user.setPassword(null)call in the controllers becomes dead code and must be deleted. Dead code in a security-sensitive path is actively misleading — it implies protection where there no longer is any.AppUser, I'd want to confirm: isAppUserever used as@RequestBody? If so,@JsonIgnoreblocks deserialization of the password field from incoming JSON too, which could silently break any endpoint that sets the password via the entity directly. Looking at the CLAUDE.md, there's aCreateUserRequestDTO — so this should be safe, but worth verifying explicitly.AppUserpassed to any JacksonObjectMapper.writeValueAsString()calls outside of the normal Spring MVC serialization path (e.g., in logging, audit trails, or tests)? Those would also be affected.Suggestions
setPassword(null). A@WebMvcTestthat GETs/api/users/meand asserts the response body contains nopasswordkey. Without this, the acceptance criteria are prose, not enforced code.setPassword(null)and remove every hit. Don't leave them as "harmless" dead code — they're noise that will confuse the next reader.should not expose password hash in user response— it reads as a security property, not just a serialization detail.🔒 Nora "NullX" Steiner — Application Security Engineer
Questions & Observations
setPassword(null)in every controller is a classic "defense by convention" anti-pattern — it works until someone adds a new endpoint and forgets the convention.@JsonIgnoreat the model level is the right fix: the protection travels with the object.@JsonIgnorevs@JsonProperty(access = WRITE_ONLY)— know the difference.@JsonIgnoresuppresses the field in both serialization (output) and deserialization (input). IfAppUseris ever deserialized from a JSON request body — even in a test — the password field will be silently ignored. For a field that is only ever set via DTOs or Spring Security'sUserDetailsService,@JsonIgnoreis fine. But if there's any path where the entity is bound from JSON input, use@JsonProperty(access = JsonProperty.Access.READ_ONLY)instead to block output while preserving input.@JsonIgnorein place, verify there are no@RequestBody AppUserbindings anywhere in the codebase. If there are, that's a mass assignment vulnerability independent of this issue — but worth catching now.AppUserlikely has agroupsrelationship. Consider whether the full group permission set should also be suppressed in certain response contexts (e.g., a user fetching their own profile should not see a full enumeration of all permission strings if that information is useful for privilege escalation reconnaissance).Suggestions
@Entityclass that has a field namedpasswordwithout@JsonIgnoreor@JsonProperty(access = WRITE_ONLY).curl -s GET /api/users/meresponse does not contain the string"password"— not justnull, but absent entirely.🧪 Sara Holt — QA Engineer & Test Strategist
Questions & Observations
UserControllerTestor@WebMvcTestslice? If yes, the regression test slots in there. If not, this issue is a good trigger to add the slice.Test Strategy
Integration layer —
@WebMvcTest(UserController.class)with@WithMockUser:shouldNotExposePasswordHashInUserResponse$.passworddoes not exist — notisNull(). A null field still signals the hash is being computed; absent is the correct assertion.GET /api/users/{id}andGET /api/users(array element).Edge Cases to Cover
@RepositoryRestResourceis configured? Those can expose entity fields independently of@JsonIgnorein some configurations — worth a quick check.@Dataregeneration —@JsonIgnoreon the field survives Lombok, but it's worth a note in the PR.Suggestions
shouldNotExposePasswordHashInUserResponse, nottestPasswordFieldAbsent.🏗️ Markus Keller — Application Architect
Questions & Observations
@JsonIgnoreon the entity moves the invariant to where it belongs — the model — so it holds regardless of how many controllers serialise the type in future.ObjectMapper.writeValueAsString(user)calls in services or tests; (2) any@RepositoryRestResourceonAppUserRepository— Spring Data REST bypasses controller-level protection entirely; (3) any SSE or WebSocket handlers that push user objects.AppUseris one of those entities where the gap between "what the DB stores" and "what the API should expose" is wide. Thepasswordfield is the urgent case;groupswith full permission strings may be a future one. Worth a comment in the PR.Suggestions
AppUserever needs to accept a password via JSON (e.g., a password-change endpoint), use@JsonProperty(access = JsonProperty.Access.READ_ONLY)instead — it blocks serialization (output) while allowing deserialization (input).@JsonIgnoreblocks both. Given the current DTO-first pattern for writes,@JsonIgnoreis fine — just document the choice.setPassword(null)is non-negotiable. Don't leave it as "technically harmless" — it creates false confidence in the code path and will confuse the next person tracing serialization behavior.🎨 Leonie Voss — UI/UX Designer & Accessibility Strategist
No UI concerns from my angle — this fix touches only backend serialization with no user-facing rendering changes. I checked there are no frontend components that display or depend on a
passwordfield from the user API response, so no template or binding changes are required on the SvelteKit side.🚀 Tobias Wendt — DevOps & Platform Engineer
Questions & Observations
AppUserobjects viatoString()? Lombok's@Datagenerates atoString()that includes all fields by default, includingpassword.@JsonIgnorehas no effect ontoString(). If user objects are ever logged — even at DEBUG level — the BCrypt hash ends up in your log store.Suggestions
@ToString.Excludeto thepasswordfield alongside@JsonIgnore:$2a$(BCrypt prefix) to confirm no prior leakage has been stored. Low effort, high value for a compliance audit.