Overview
Sensitive Data Exposure occurs when an application unintentionally reveals sensitive information to an unauthorized party. This can include PII, financial data, internal system details, or credentials. This weakness is often the result of misconfiguration (like running in “debug” mode) or failing to filter sensitive data from API responses.Business Impact
This vulnerability can lead to massive data breaches, loss of user trust, and severe regulatory fines (e.g., under GDPR, CCPA). Leaked system information and stack traces also provide attackers with a detailed map of your application, making other attacks (like Injection or Access Control bypass) much easier.Reference Details
CWE ID: CWE-200
OWASP Top 10 (2021): A01:2021 - Broken Access Control
Severity: Medium
Framework-Specific Analysis and Remediation
All frameworks have a “debug” or “development” mode that is intentionally verbose. The most common vulnerability is a simple failure to disable this mode in production. The second most common is serializing entire data models (like aUser object) directly to an API response, which accidentally includes password hashes, tokens, or PII.
- Python
- Java
- .NET(C#)
- PHP
- Node.js
- Ruby
Framework Context
Django’sDEBUG = True setting is the primary culprit. For Django Rest Framework (DRF), ModelSerializer can over-share data if not explicitly configured.Vulnerable Scenario 1: Debug Mode in Production
LeavingDEBUG = True in settings.py on a production server will show detailed stack traces to the public.Copy
# settings.py
# DANGEROUS: This will leak all settings, stack traces, and more.
DEBUG = True
Vulnerable Scenario 2: Over-sharing in API
AModelSerializer for the User model that doesn’t restrict fields.Copy
# users/serializers.py
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
# DANGEROUS: This will serialize ALL fields, including 'password' (hash),
# 'is_staff', 'is_superuser', and 'date_joined'.
fields = '__all__'
Mitigation and Best Practices
SetDEBUG = False in production. Use environment variables to control this. For DRF, explicitly list fields using the fields tuple or use exclude to blacklist sensitive ones.Secure Code Example
Use a DTO (Data Transfer Object) pattern, which DRF serializers provide, to create a “whitelist” of safe fields.Copy
# users/serializers.py (Secure Version)
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
# SECURE: Only the 'id' and 'username' are exposed.
fields = ('id', 'username', 'email')
# Or, using exclude:
class UserAdminSerializer(serializers.ModelSerializer):
class Meta:
model = User
# SECURE: Excludes the password hash, even for an admin view.
exclude = ('password',)
Testing Strategy
Write a test to ensureDEBUG is False. For APIs, write a unit test for the serializer or an integration test for the endpoint and assert that sensitive keys are not in the response.Copy
# users/tests.py
from django.conf import settings
def test_debug_is_off_in_production_settings(self):
# Assuming you load a 'production.py' settings file for this test
self.assertFalse(settings.DEBUG)
def test_user_api_does_not_leak_password_hash(self):
response = self.client.get(reverse('user-detail', args=[self.user.id]))
self.assertEqual(response.status_code, 200)
self.assertNotIn('password', response.json())
Framework Context
Spring Boot’sserver.error.include-stacktrace=always is the equivalent of a debug mode. For APIs, serializing an entire JPA Entity object can leak sensitive fields or trigger lazy-loading exceptions.Vulnerable Scenario 1: Verbose Error Messages
An exception handler that reveals the internal stack trace.Copy
// controller/SomeController.java
@GetMapping("/error-example")
public String throwError() {
throw new RuntimeException("This is a detailed error message!");
}
// application.properties
# DANGEROUS: This will show a full stack trace to the user.
server.error.include-stacktrace=always
Vulnerable Scenario 2: Leaking Model Data
Returning a JPA@Entity directly, which includes fields that should be ignored.Copy
// model/User.java
@Entity
public class User {
public String username;
public String passwordHash; // DANGEROUS: This will be serialized
public String ssn; // DANGEROUS: This will be serialized
}
// controller/UserController.java
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// DANGEROUS: Returning the full entity leaks passwordHash and ssn
return userRepository.findById(id).get();
}
Mitigation and Best Practices
Setserver.error.include-stacktrace=on_param or never in production. Use DTOs (Data Transfer Objects) to separate your internal models from your external API representation. Use @JsonIgnore on sensitive fields in the model as a defense-in-depth.Secure Code Example
Create a DTO (UserDTO) that only contains the safe fields. Use @JsonIgnore on the sensitive model field.Copy
// model/User.java (Secure Version)
@Entity
public class User {
public String username;
@JsonIgnore // Defense-in-depth
public String passwordHash;
@JsonIgnore
public String ssn;
}
// dto/UserDTO.java
public class UserDTO {
public String username;
// No passwordHash or ssn field
}
// controller/UserController.java (Secure Version)
@GetMapping("/users/{id}")
public UserDTO getUser(@PathVariable Long id) {
User user = userRepository.findById(id).get();
// SECURE: Manually mapping to a safe DTO
UserDTO dto = new UserDTO();
dto.username = user.username;
return dto;
}
Testing Strategy
Write a MockMVC test for an endpoint that fails. Assert the response JSON does not contain a “trace” field. Write another test for the API and assert the response DTO does not contain sensitive fields.Copy
@Test
void userEndpoint_doesNot_leakSensitiveData() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("testuser"))
.andExpect(jsonPath("$.passwordHash").doesNotExist())
.andExpect(jsonPath("$.ssn").doesNotExist());
}
Framework Context
TheUseDeveloperExceptionPage() middleware in Startup.cs is the primary source of stack trace leaks. Serializing entire Entity Framework models is the primary source of data leaks.Vulnerable Scenario 1: Developer Exception Page
TheStartup.cs Configure method often has logic to show a detailed error page, but it’s easy to misconfigure.Copy
// Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// DANGEROUS: If env is not correctly set to "Production",
// or if this line is outside the `if` block, this will leak stack traces.
app.UseDeveloperExceptionPage();
// ...
}
Vulnerable Scenario 2: Serializing EF Model
An API controller returns the database model directly.Copy
// Models/User.cs
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string PasswordHash { get; set; } // DANGEROUS
}
// Controllers/UsersController.cs
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUser(int id)
{
var user = await _context.Users.FindAsync(id);
// DANGEROUS: Returning the 'User' object directly
return user;
}
Mitigation and Best Practices
Ensure theUseDeveloperExceptionPage() is only called within if (env.IsDevelopment()). Use DTOs (Data Transfer Objects) and AutoMapper (or manual mapping) to create API-safe objects. Use the [JsonIgnore] attribute on sensitive model properties.Secure Code Example
Use a DTO and a secureStartup.cs configuration.Copy
// Startup.cs (Secure Version)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// SECURE: Use a generic error handler for production
app.UseExceptionHandler("/Home/Error");
}
// ...
}
// Dtos/UserDto.cs
public class UserDto
{
public int Id { get; set; }
public string Username { get; set; }
// PasswordHash is intentionally omitted
}
// Controllers/UsersController.cs (Secure Version)
[HttpGet("{id}")]
public async Task<ActionResult<UserDto>> GetUser(int id)
{
var user = await _context.Users.FindAsync(id);
// SECURE: Map to a DTO (manual example)
var userDto = new UserDto { Id = user.Id, Username = user.Username };
return userDto;
}
Testing Strategy
Write an integration test that forces an exception and asserts the response is the generic error page, not the developer exception page. Write an API test and check the JSON response for the absence of sensitive fields.Copy
[Fact]
public async Task GetUser_DoesNot_ReturnPasswordHash()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/users/1");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.DoesNotContain("passwordHash", content, StringComparison.OrdinalIgnoreCase);
}
Framework Context
Laravel’sAPP_DEBUG=true in the .env file is the primary culprit, showing verbose error pages via the “Ignition” error handler. Eloquent models can leak data if the $hidden property is not used.Vulnerable Scenario 1: Debug Mode in Production
SettingAPP_DEBUG=true in the .env file on a production server.Copy
# .env
# DANGEROUS: Will show full stack traces and environment variables
APP_DEBUG=true
Vulnerable Scenario 2: Leaking Model Data
An Eloquent model that does not hide sensitive attributes.Copy
// app/Models/User.php
class User extends Authenticatable
{
// DANGEROUS: password and remember_token will be included
// when this model is converted to JSON or an array.
}
// app/Http/Controllers/UserController.php
public function show($id)
{
// DANGEROUS: This response will include the user's password hash.
return User::findOrFail($id);
}
Mitigation and Best Practices
SetAPP_DEBUG=false in your production .env file. On your Eloquent models, use the $hidden array to specify attributes that should never be included in JSON responses.Secure Code Example
Set the$hidden property in the model.Copy
// app/Models/User.php (Secure Version)
class User extends Authenticatable
{
/**
* The attributes that should be hidden for serialization.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
];
}
// app/Http/Controllers/UserController.php (Secure Version)
public function show($id)
{
// SECURE: The response will automatically filter out fields in $hidden.
return User::findOrFail($id);
}
Testing Strategy
Write a test that checks the production environment configuration. Write a feature test that calls the API endpoint and asserts that the sensitive keys are not present in the JSON response.Copy
// tests/Feature/ConfigurationTest.php
public function test_debug_is_disabled_in_production()
{
// This test assumes 'testing' env mirrors 'production' for config
$this->assertFalse(config('app.debug'));
}
// tests/Feature/UserApiTest.php
public function test_user_api_does_not_return_password()
{
$user = User::factory()->create();
$response = $this->get('/api/users/' . $user->id);
$response->assertStatus(200);
$response->assertJsonMissing(['password']);
$response->assertJsonPath('username', $user->username);
}
Framework Context
Express has no default “debug” mode, but developers often create one by sending the full error object in an error-handling middleware. Sending a full database model object to the response is also common.Vulnerable Scenario 1: Leaky Error Handler
An error-handling middleware that sends the entire error stack to the client.Copy
// app.js
app.get('/error', (req, res) => {
throw new Error("Database connection failed!");
});
// DANGEROUS: This middleware sends the full error, including the stack trace.
app.use((err, req, res, next) => {
res.status(500).send({
message: err.message,
stack: err.stack // The leak is here
});
});
Vulnerable Scenario 2: Serializing Full Model
Sending a full Mongoose or Sequelize model object directly.Copy
// routes/users.js
router.get('/:id', async (req, res) => {
const user = await User.findById(req.params.id);
// DANGEROUS: If the 'user' object from the ORM has passwordHash,
// it will be sent to the client.
res.send(user);
});
Mitigation and Best Practices
Use an environment variable to control error handling. In production, send a generic error message. When sending user data, either manually map it to a “safe” object or usetoJSON() overrides in your model.Secure Code Example
Copy
// app.js (Secure Error Handler)
app.use((err, req, res, next) => {
res.status(500);
if (process.env.NODE_ENV === 'production') {
res.send({ message: 'An internal server error occurred.' });
} else {
// Only send details in development
res.send({ message: err.message, stack: err.stack });
}
});
// models/User.js (Secure Mongoose Model)
userSchema.methods.toJSON = function() {
var obj = this.toObject();
delete obj.passwordHash; // Remove sensitive field
delete obj.salt;
return obj;
}
// routes/users.js (Secure Route)
router.get('/:id', async (req, res) => {
const user = await User.findById(req.params.id);
// SECURE: The toJSON method is called automatically by res.send()
res.send(user);
});
Testing Strategy
Use Jest/Supertest to hit an endpoint that throws an error. SetNODE_ENV=production and assert the response body does not contain “stack”. Test the user API and assert the response does not contain the “passwordHash” key.Copy
// tests/error.test.js
process.env.NODE_ENV = 'production';
it('should not return stack trace in production', async () => {
const response = await request(app).get('/error');
expect(response.status).toBe(500);
expect(response.body.stack).toBeUndefined();
expect(response.body.message).toBe('An internal server error occurred.');
});
// tests/user.test.js
it('should not return password hash', async () => {
const response = await request(app).get('/api/users/123');
expect(response.body.passwordHash).toBeUndefined();
});
Framework Context
Rails’config.consider_all_requests_local = true in production will show detailed error pages. Rendering a full ActiveRecord model as JSON can also leak data.Vulnerable Scenario 1: Debug Pages in Production
Settingconfig.consider_all_requests_local = true in config/environments/production.rb.Copy
# config/environments/production.rb
# DANGEROUS: This will show public error pages with stack traces.
config.consider_all_requests_local = true
Vulnerable Scenario 2: Serializing Full Model
A controller action that renders the full model as JSON.Copy
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
# DANGEROUS: This will serialize all attributes, including
# password_digest, tokens, etc.
render json: @user
end
end
Mitigation and Best Practices
Ensureconfig.consider_all_requests_local = false in production. Use a “view model” pattern with tools like jbuilder or ActiveModel::Serializer to create a whitelist of attributes to be rendered.Secure Code Example
Usingjbuilder (which is included with Rails).Copy
# config/environments/production.rb (Secure)
# SECURE: This serves static error files (e.g., public/500.html)
config.consider_all_requests_local = false
Copy
# app/views/users/show.json.jbuilder
# SECURE: We are explicitly stating which attributes to include.
json.extract! @user, :id, :username, :email
Copy
# app/controllers/users_controller.rb (Secure)
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
# This will now use the show.json.jbuilder template
render :show
end
end
Testing Strategy
Write a test to ensure production config is secure. Write a request spec to check the JSON response for the absence of sensitive fields.Copy
# spec/config/production_spec.rb
it 'disables local request debugging in production' do
expect(Rails.application.config.consider_all_requests_local).to be(false)
end
# spec/requests/users_api_spec.rb
it 'does not return sensitive user data' do
user = create(:user)
get user_path(user, format: :json)
expect(response).to have_http_status(:ok)
json_response = JSON.parse(response.body)
expect(json_response.keys).to include('id', 'username')
expect(json_response.keys).not_to include('password_digest')
end

