Over-Posting, Under-Posting, Over-Sharing, Under-Sharing
Context
When building a REST API, clients send JSON payloads to your endpoints — for example, when creating or updating an entity.
Let’s say you have a model like this:
public class User
{
public int Id { get; set; }
public string Username { get; set; } = default!;
public string Email { get; set; } = default!;
public string Role { get; set; } = default!;
public bool IsAdmin { get; set; }
}
Now consider what happens when a client does a GET, POST or PUT.
Over-Posting
Over-posting happens when a client sends more data than intended, and your API blindly binds that data to your entity model — potentially overwriting fields that the client should not control.
For example, the client sends:
{
"username": "bob",
"email": "bob@example.com",
"isAdmin": true
}
If your API action looks like this:
[HttpPost]
public async Task<IActionResult> CreateUser(User user)
{
_dbContext.Users.Add(user);
await _dbContext.SaveChangesAsync();
return Ok(user);
}
Then the client just granted themselves admin rights because your API bound the JSON directly to the EF model.
This is an over-posting vulnerability.
How to Avoid Over-Posting
Use DTOs or View Models
Instead of binding directly to your EF entity, define a limited version of it:
public class CreateUserDto
{
public string Username { get; set; } = default!;
public string Email { get; set; } = default!;
}
Then map it manually:
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserDto dto)
{
var user = new User
{
Username = dto.Username,
Email = dto.Email,
IsAdmin = false // enforced server-side
};
_dbContext.Users.Add(user);
await _dbContext.SaveChangesAsync();
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
This approach prevents malicious clients from setting properties they shouldn’t.
Use [Bind] Attribute (less preferred)
In MVC or minimal APIs, you can limit what properties get bound:
public IActionResult Create([Bind("Username,Email")] User user)
But this is more brittle than using DTOs.
Use a Mapper or manual mapping
Automate mapping between DTOs and entities:
var user = _mapper.Map<User>(dto);
Under-Posting
Under-posting is the opposite problem — when a client sends less data than expected, causing partial updates or missing fields to overwrite or null out existing data.
For example the client sends:
{
"email": "bob.new@example.com"
}
Your controller does:
[HttpPut("{id}")]
public async Task<IActionResult> UpdateUser(int id, User user)
{
if (id != user.Id)
return BadRequest();
_dbContext.Entry(user).State = EntityState.Modified;
await _dbContext.SaveChangesAsync();
return NoContent();
}
Because the user object only has the Email populated, EF treats all other fields as null and overwrites them in the database — even though the client didn’t intend to change them. 😬
That’s under-posting.
How to Avoid Under-Posting
Use a separate DTO for updates
public class UpdateUserDto
{
public string? Email { get; set; }
public string? Role { get; set; }
}
Then in your controller:
var user = await _dbContext.Users.FindAsync(id);
if (user == null)
return NotFound();
if (dto.Email is not null)
user.Email = dto.Email;
if (dto.Role is not null)
user.Role = dto.Role;
await _dbContext.SaveChangesAsync();
You explicitly control what fields can be updated and how.
Support PATCH for partial updates
Instead of PUT, use HTTP PATCH for partial updates — e.g. with JsonPatchDocument<T>:
[HttpPatch("{id}")]
public async Task<IActionResult> PatchUser(int id, JsonPatchDocument<User> patchDoc)
{
var user = await _dbContext.Users.FindAsync(id);
if (user == null)
return NotFound();
patchDoc.ApplyTo(user);
await _dbContext.SaveChangesAsync();
return NoContent();
}
This makes intent explicit:
PATCH= partialPUT= full replacement.
Validate DTOs
Use [Required], [StringLength], etc., to ensure critical fields are present when needed.
Posting Summary
| Issue | Description | Risk | Fix |
|---|---|---|---|
| Over-Posting | Client sends more data than intended | Security flaw (e.g., privilege escalation) | Use DTOs / ViewModels / manual mapping |
| Under-Posting | Client sends less data than required | Data loss or incomplete updates | Use PATCH or explicit field updates |
Rule of Thumb
Never bind your EF entities directly to API input models. Always use DTOs and control what properties are read from or written to.
When we talk about over-posting and under-posting, we’re usually referring to incoming data (i.e. client → API). But there are analogous concepts for outgoing data (i.e. API → client) — often referred to as over-sharing and under-sharing.
Over-Sharing (the response-side version of over-posting)
Over-sharing happens when your API response exposes more data than the client should see.
For example, you return an EF entity directly:
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUser(int id)
{
var user = await _dbContext.Users.FindAsync(id);
return user!;
}
The response might look like:
{
"id": 1,
"username": "bob",
"email": "bob@example.com",
"passwordHash": "AQAAAAEAACcQAAAAEG34j2a...",
"isAdmin": true
}
That’s an over-share — the client now knows internal fields that should never leave the server, such as passwordHash, internal flags (isAdmin, isDeleted, isLocked, etc.), audit fields (createdBy, modifiedAt, etc.)
How to Avoid Over-Sharing
Use Output DTOs (Response Models)
Define a separate DTO for what you actually want to return (either a class or record):
public class UserDto
{
public int Id { get; set; }
public string Username { get; set; } = default!;
public string Email { get; set; } = default!;
}
Then map it:
[HttpGet("{id}")]
public async Task<ActionResult<UserDto>> GetUser(int id)
{
var user = await _dbContext.Users.FindAsync(id);
if (user == null)
return NotFound();
var dto = new UserDto
{
Id = user.Id,
Username = user.Username,
Email = user.Email
};
return dto;
}
This ensures only intended fields leave your service.
Use a Mapper (or similar)
Automate the projection:
var dto = _mapper.Map<UserDto>(user);
Use LINQ projection with EF
You can avoid even loading unneeded fields:
var user = await _dbContext.Users
.Where(u => u.Id == id)
.Select(u => new UserDto { Id = u.Id, Username = u.Username, Email = u.Email })
.FirstOrDefaultAsync();
This ensures only those columns are fetched from the database.
Avoid exposing internal entities
Never return EF entities directly. They may include navigation properties, lazy-loading proxies, etc.
Under-Sharing (response-side version of under-posting)
Under-sharing happens when your API doesn’t return enough data for clients to function properly — often unintentionally.
For example, you return only an Id after creating a resource:
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserDto dto)
{
var user = new User { ... };
_dbContext.Users.Add(user);
await _dbContext.SaveChangesAsync();
return Ok(user.Id);
}
This works, but the client may now have to make an extra GET call just to retrieve the full resource. If your design expects the client to show the new user’s data immediately, this is an under-share.
How to Fix Under-Sharing
Return the full DTO that the client actually needs:
return CreatedAtAction(nameof(GetUser), new { id = user.Id },
new UserDto { Id = user.Id, Username = user.Username, Email = user.Email });
This way, the response is:
{
"id": 5,
"username": "bob",
"email": "bob@example.com"
}
which containins exactly the right amount of information.
Summary: API Input vs Output
| Concept | Direction | Problem | Example | Solution |
|---|---|---|---|---|
| Over-posting | Client → API | Client can set fields they shouldn’t | isAdmin: true in POST |
Use Input DTOs |
| Under-posting | Client → API | Client omits needed fields | Missing email causes nulls |
Use PATCH or explicit updates |
| Over-sharing | API → Client | API exposes sensitive/internal data | Sends passwordHash |
Use Output DTOs |
| Under-sharing | API → Client | API doesn’t return enough info | Returns only Id |
Return full DTO or resource URI |
Best Practice Pattern
Always use separate DTOs for input and output.
Even if they have similar fields, keeping them distinct:
- Clarifies intent (what clients can send vs what they can see)
- Helps to prevent security leaks
- Decouples your persistence model from your API contract
For example:
public record CreateUserRequest(string Username, string Email);
public record UserResponse(int Id, string Username, string Email);