Excessive Data Exposure on Digitalocean
How Excessive Data Exposure Manifests in Digitalocean
Excessive Data Exposure in Digitalocean APIs often stems from returning entire database records when clients only need specific fields. This manifests in several Digitalocean-specific patterns:
Database Query Over-fetching
// Vulnerable: Returns all droplet fields including sensitive metadata
app.get('/api/droplets/:id', async (req, res) => {
const droplet = await db.collection('droplets').findOne({ _id: req.params.id });
res.json(droplet); // Exposes internal IDs, timestamps, system flags
});
// Secure: Select only required fields
app.get('/api/droplets/:id', async (req, res) => {
const droplet = await db.collection('droplets').findOne(
{ _id: req.params.id },
{ projection: { name: 1, status: 1, ip_address: 1, region: 1 } }
);
res.json(droplet);
});
Digitalocean API Wrapper Vulnerabilities
// Vulnerable: Using Digitalocean SDK without field filtering
const { DropletsController } = require('@digitalocean/node-do-wrapper');
async function getDropletDetails(dropletId) {
const droplets = new DropletsController(oauthClient);
const droplet = await droplets.getById(dropletId);
return droplet; // Returns full object including API keys, internal IDs
}
// Secure: Extract only necessary fields
async function getDropletDetails(dropletId) {
const droplets = new DropletsController(oauthClient);
const droplet = await droplets.getById(dropletId);
return {
name: droplet.name,
status: droplet.status,
ip_address: droplet.networks.v4[0]?.ip_address,
region: droplet.region.slug
};
}
Firewalls and Networking APIs
// Vulnerable: Exposing firewall rules with internal IPs
app.get('/api/firewalls/:id', async (req, res) => {
const firewall = await db.collection('firewalls').findOne({ _id: req.params.id });
res.json(firewall); // Includes internal network ranges, admin notes
});
// Secure: Filter sensitive firewall data
app.get('/api/firewalls/:id', async (req, res) => {
const firewall = await db.collection('firewalls').findOne(
{ _id: req.params.id },
{ projection: { name: 1, inbound_rules: 1, outbound_rules: 1, tags: 1 } }
);
res.json(firewall);
});
Spaces (S3-compatible) Metadata Exposure
// Vulnerable: Returning full Spaces object
app.get('/api/spaces/:id', async (req, res) => {
const space = await spacesClient.getSpace(req.params.id);
res.json(space); // Exposes endpoint URLs, CORS configs, ACLs
});
// Secure: Return minimal metadata
app.get('/api/spaces/:id', async (req, res) => {
const space = await spacesClient.getSpace(req.params.id);
res.json({
name: space.name,
region: space.region,
size: space.size,
created_at: space.created_at
});
});
Digitalocean-Specific Detection
Detecting Excessive Data Exposure in Digitalocean environments requires understanding both the API surface and common data exposure patterns:
middleBrick API Scanning
# Scan Digitalocean API endpoints directly
middlebrick scan https://api.yourdomain.com/digitalocean
# Scan with specific Digitalocean context
middlebrick scan https://api.yourdomain.com/digitalocean \
--category=data-exposure \
--threshold=high
Manual Detection Checklist
| Detection Area | What to Look For | Digitalocean Context |
|---|---|---|
| Response Size | Unexpectedly large JSON payloads | Check for full droplet objects vs. needed fields |
| Metadata Fields | Internal IDs, timestamps, system flags | Digitalocean internal IDs, creation timestamps |
| Configuration Data | API keys, endpoints, CORS configs | Spaces endpoints, firewall rules, API credentials |
| Network Information | Internal IP ranges, VPC details | Private networking configs, VPC IDs |
OpenAPI Spec Analysis
# In your OpenAPI spec, look for:
paths:
/api/droplets/{id}:
get:
responses:
'200':
description: Droplet details
content:
application/json:
schema:
$ref: '#/components/schemas/Droplet'
# Check if Droplet schema includes unnecessary fields:
components:
schemas:
Droplet:
type: object
properties:
id: # Required
type: string
name: # Required
type: string
status: # Required
type: string
created_at: # Optional for client
type: string
format: date-time
updated_at: # Often unnecessary
type: string
format: date-time
region: # Optional
type: object # Could be just region slug
vcpus: # Often unnecessary
type: integer
memory: # Often unnecessary
type: integer
disk: # Often unnecessary
type: integer
GitHub Action Integration
# Add to your CI/CD pipeline
name: API Security Scan
on: [pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scan Digitalocean API
run: |
npx middlebrick scan https://staging.yourdomain.com/api/digitalocean \
--fail-below=B \
--output=json > security-report.json
- name: Fail if score too low
run: |
SCORE=$(jq '.overall_score' security-report.json)
if [ $SCORE -lt 80 ]; then
echo "Security score too low: $SCORE"
exit 1
fi
Digitalocean-Specific Remediation
Remediating Excessive Data Exposure in Digitalocean APIs requires systematic field filtering and proper data modeling:
Field Selection with Digitalocean SDK
// Instead of returning full Digitalocean objects:
const { DropletsController } = require('@digitalocean/node-do-wrapper');
async function getDropletSummary(dropletId) {
const droplets = new DropletsController(oauthClient);
const droplet = await droplets.getById(dropletId);
// Return only what the client needs:
return {
id: droplet.id,
name: droplet.name,
status: droplet.status,
ip_address: droplet.networks.v4[0]?.ip_address,
region: droplet.region.slug,
size_slug: droplet.size_slug,
created_at: droplet.created_at
};
}
// For list endpoints, use pagination and field filtering:
async function listDroplets(page = 1, perPage = 20) {
const droplets = new DropletsController(oauthClient);
const { droplets: rawDroplets, meta } = await droplets.getAll({
per_page: perPage,
page: page
});
return {
data: rawDroplets.map(d => ({
id: d.id,
name: d.name,
status: d.status,
ip_address: d.networks.v4[0]?.ip_address,
region: d.region.slug
})),
pagination: {
total: meta.total,
page: meta.current_page,
last_page: meta.last_page
}
};
}
Database Query Optimization
// Mongoose with field projection
const Droplet = require('../models/Droplet');
// Vulnerable: Returns all fields
app.get('/api/droplets/:id', async (req, res) => {
const droplet = await Droplet.findById(req.params.id);
res.json(droplet);
});
// Secure: Select only required fields
app.get('/api/droplets/:id', async (req, res) => {
const droplet = await Droplet.findById(req.params.id)
.select('name status ip_address region size_slug created_at');
res.json(droplet);
});
// For list endpoints with population
app.get('/api/droplets', async (req, res) => {
const { page = 1, limit = 20 } = req.query;
const droplets = await Droplet.find()
.select('name status ip_address region size_slug created_at')
.limit(limit * 1)
.skip((page - 1) * limit)
.sort({ created_at: -1 });
const count = await Droplet.countDocuments();
res.json({
data: droplets,
pagination: {
current: page,
pageSize: limit,
total: count,
totalPages: Math.ceil(count / limit)
}
});
});
Spaces API Data Minimization
// Instead of returning full Spaces metadata:
async function getSpaceInfo(spaceName) {
const space = await spacesClient.getSpace(spaceName);
// Return minimal required information:
return {
name: space.name,
region: space.region,
size: space.size,
created_at: space.created_at,
object_count: space.object_count,
// Exclude: endpoint URLs, CORS configs, ACLs, internal metadata
};
}
// For file listings, filter metadata:
async function listSpaceFiles(spaceName, prefix = '', page = 1, perPage = 50) {
const { objects, next } = await spacesClient.getObjects(spaceName, {
prefix: prefix,
delimiter: '/',
page: page,
perPage: perPage
});
return objects.map(obj => ({
key: obj.key,
size: obj.size,
last_modified: obj.last_modified,
// Exclude: internal storage class, eTag, version ID
}));
}
Firewall API Data Exposure Fix
// Instead of returning full firewall configuration:
async function getFirewallDetails(firewallId) {
const firewall = await db.collection('firewalls').findOne({ _id: firewallId });
// Return only what's necessary for the client:
return {
id: firewall._id,
name: firewall.name,
inbound_rules: firewall.inbound_rules.map(rule => ({
protocol: rule.protocol,
ports: rule.ports,
sources: rule.sources.map(src => ({
type: src.type,
// Exclude: internal IP ranges, admin notes
}))
})),
outbound_rules: firewall.outbound_rules.map(rule => ({
protocol: rule.protocol,
ports: rule.ports,
destinations: rule.destinations.map(dest => ({
type: dest.type
}))
})),
tags: firewall.tags,
created_at: firewall.created_at
// Exclude: admin notes, internal system flags, audit logs
};
}
Related CWEs: propertyAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-915 | Mass Assignment | HIGH |