4.34 Review Obsolete Code
4.34 - Review Obsolete Code
Obsolete code increases attack surface when dead paths, test artifacts, or deprecated features remain reachable in production. Review feature flags, commented branches, unused endpoints, and legacy modules. Ask whether each block is still executed, whether it bypasses current security controls, and whether it should be removed.
What This Vulnerability Is
Dead code, test hooks, and outdated modules often linger after refactors. Some paths are still reachable through direct URLs, old API versions, or feature toggles left enabled. Obsolete authentication helpers, deprecated encryption routines, and temporary admin endpoints may lack the hardening applied to newer code. Attackers probe forgotten routes; insiders may know URLs that documentation no longer mentions.
The unsafe assumption is that unused code is harmless because "nobody calls it." Reachability is not the same as discoverability. Static links, mobile app bundles, integration partners, and scanners can still hit old endpoints. Removing obsolete code improves readability and reduces the set of behaviors that must stay secure.
Vulnerability Characteristics (Where to Identify Them)
| Signal | Where to look |
|---|---|
| Feature type | Legacy login routes, old API versions (/v0/, /v1/), debug endpoints, profiling handlers |
| Test artifacts | Hardcoded users, mock payment flows, QA bypasses without environment gating |
| Feature flags | Toggles defaulting to on, experiments never removed, alternate upload paths |
| Deprecated handlers | @Deprecated controllers, duplicate login implementations, pre-refactor validation |
| Commented blocks | Disabled auth checks left in comments, large duplicated logic with weaker controls |
| Build gaps | Debug controllers in Release builds, test packages bundled into production JARs |
| Static analysis hits | Unreferenced methods, unreachable branches flagged by coverage or linters |
Attack Payloads
Use these in authorized tests when probing for forgotten routes, debug handlers, and feature-flag bypasses. Replace TARGET with the endpoint or parameter under review.
Pattern 1: Direct legacy URL access
GET /upload/v0
GET /api/v0/login
GET /admin/legacy/export
POST /debug/reset-db
Many obsolete paths remain registered but undocumented. Scanners and wordlists often discover them before product teams do.
Pattern 2: Feature-flag enablement
ENABLE_OLD_UPLOAD=1
FEATURE_DEBUG_ROUTES=true
X-Enable-Legacy-Auth: 1
?use_legacy=true
If flags default to on in production or can be toggled via env or headers, weaker code paths activate without code changes.
Pattern 3: Debug header or token bypass
X-Debug: 1
X-Test-Mode: true
Authorization: Bearer debug-token
?debug=1
Test helpers that check a header instead of environment allow any caller who learns the header name.
Pattern 4: Old API version routing
Accept: application/vnd.company.v0+json
/api/v0/users/1
/mobile/v1/session (app still bundles v1 URL)
Mobile clients and integration partners may call deprecated versions long after web UI migration.
Pattern 5: Profiling and actuator endpoints
/debug/pprof/
/actuator/env
/metrics
/_internal/health?verbose=1
Profiling and Spring Boot actuator endpoints expose internals when left enabled in production builds.
Language-Specific Sinks and Dangerous APIs
Search for route registration, feature toggles, and debug handlers that may ship in production artifacts.
Python
if os.getenv("ENABLE_OLD_UPLOAD") == "1":
@app.route("/upload/v0")
def upload_v0(): ...
@app.route("/debug/reset-db")
def reset_db(): ...
from werkzeug.middleware.profiler import ProfilerMiddleware
app.wsgi_app = ProfilerMiddleware(app.wsgi_app) # no env gate
import flask_debugtoolbar
app.config["DEBUG_Toolbar_ENABLED"] = True
Also review: django.conf.urls legacy includes, FastAPI include_router for /debug, ENABLE_DEBUG in settings.
Java
@Profile("!prod") // misconfigured — profile not active but class still scanned
@RestController
@RequestMapping("/debug")
public class DebugController { ... }
@PostMapping("/upload/v0") // no @Deprecated removal schedule
public void uploadV0(...) { ... }
management.endpoints.web.exposure.include=* // actuator wide open
Spring: spring.profiles.active missing in prod; @ConditionalOnProperty with matchIfMissing = true.
C
#if DEBUG
[Route("debug/[controller]")]
#endif
// Same controller duplicated outside #if — ships in Release
if (Configuration["Features:LegacyUpload"] == "true")
endpoints.MapPost("/upload/v0", UploadV0);
app.UseDeveloperExceptionPage(); // not wrapped in IsDevelopment()
JavaScript (Node.js)
if (process.env.ENABLE_LEGACY === '1') {
app.post('/upload/v0', uploadV0);
}
app.use('/debug', debugRouter); // always mounted
require('./routes/test-users'); // test routes imported in server.js
Go
if os.Getenv("ENABLE_DEBUG") == "1" {
http.HandleFunc("/debug/pprof/", pprof.Index)
}
http.HandleFunc("/upload/v0", uploadV0) // never removed
http.HandleFunc("/internal/backdoor/status", statusHandler)
Shell / Docker
ENV ENABLE_OLD_UPLOAD=1
ENV FLASK_DEBUG=1
COPY tests/fixtures/mock_auth.py /app/
Sample Vulnerable Code in Python
import os
from flask import Flask, request
app = Flask(__name__)
# Legacy CSV import — still registered when LEGACY_IMPORT=1 in production
if os.getenv("LEGACY_IMPORT") == "1":
@app.route("/api/legacy/import", methods=["POST"])
def legacy_import():
# Old path without virus scan or size limits
ingest_csv(request.files["file"])
return "", 204
@app.route("/internal/reindex", methods=["POST"])
def reindex_search():
# Ops helper never removed — reachable if deployed
if request.headers.get("X-Internal-Token") == "reindex-dev":
search_index.rebuild_all()
return "ok"
Step-by-Step Review Walkthrough
- Identify unreachable code. Use static analysis, coverage reports, and route audits to find unreferenced controllers and handlers still deployed.
- Search for test artifacts. Look for hardcoded users, mock payment flows, and debug endpoints not guarded by environment checks.
- Review feature flags and toggles. Confirm disabled experiments are removed after launch or fail closed in production.
- Trace deprecated API versions. Compare auth, validation, and rate limiting on legacy endpoints against current implementations.
- Inspect bit-rot modules. Outdated dependencies and pre-refactor validation logic may lack fixes applied elsewhere.
- Check duplicate implementations. When login, upload, or admin functions exist twice, verify both received security fixes.
- Verify build exclusions. Production artifacts must not include test-only packages or debug handlers.
Risk Impact Analysis
Forgotten attack surface. Legacy endpoints may skip MFA, authorization, or input validation added to newer code paths.
Authentication bypass. Test backdoors and debug routes with weak or missing checks provide direct entry points.
Information disclosure. Profiling and debug handlers (pprof, status pages) may expose internals to unauthenticated callers.
Maintenance drift. Security fixes applied to primary code paths may never reach duplicate legacy implementations.
Compliance scope expansion. Every reachable endpoint counts toward audit scope even if product teams consider it unused.
Vulnerable Examples in Other Languages
Java
// Legacy upload — still registered when ENABLE_OLD_UPLOAD=1 in production
@PostMapping("/upload/v0")
public void uploadV0(@RequestParam MultipartFile file) throws IOException {
// Old path without virus scan or size limits
file.transferTo(Path.of("/data/uploads", file.getOriginalFilename()));
}
@PostMapping("/debug/reset-db")
public String resetDb(@RequestHeader("X-Debug") String debug) {
if ("1".equals(debug)) {
jdbcTemplate.execute("DELETE FROM users");
}
return "ok";
}
C
#if DEBUG
public IActionResult ResetAllUsers() { /* wipes database */ }
#endif
// Same endpoint duplicated outside DEBUG guard — ships in Release builds
public IActionResult ResetAllUsersRelease() { /* ... */ }
[HttpPost("upload/v0")]
public async Task<IActionResult> UploadV0(IFormFile file)
{
// Old path without virus scan or size limits
await file.CopyToAsync(File.Create(Path.Combine("/data/uploads", file.FileName)));
return Ok();
}
Go
// Unused since 2021 — still registered at startup
http.HandleFunc("/debug/pprof/", pprof.Index)
http.HandleFunc("/internal/backdoor/status", statusHandler)
if os.Getenv("ENABLE_OLD_UPLOAD") == "1" {
http.HandleFunc("/upload/v0", uploadV0) // weaker validation than /upload/v2
}
Fix: Safer Patterns and Libraries to Use
Python
Gate debug routes behind environment checks and remove obsolete paths on schedule.
from flask import Flask, abort
from werkzeug.middleware.profiler import ProfilerMiddleware
app = Flask(__name__)
def register_debug_routes(application: Flask) -> None:
if not application.config.get("DEBUG"):
return
@application.route("/debug/health-detail")
def health_detail():
return {"db": db_pool_status()}
# Production settings module — DEBUG is False; debug routes never register
if os.getenv("ENABLE_OLD_UPLOAD") == "1" and app.config["ENV"] == "development":
raise RuntimeError("ENABLE_OLD_UPLOAD is not allowed outside development")
Run vulture or coverage-guided deletion after refactors. Document API deprecation timelines and remove old routes on schedule.
Java
Delete deprecated controllers after migration windows. Return 410 Gone during sunset if partners need notice.
@GetMapping("/legacyLogin")
public ResponseEntity<Void> legacyLogin() {
return ResponseEntity.status(HttpStatus.GONE)
.header("Sunset", "2024-06-01")
.build();
}
Use ArchUnit to forbid production code depending on test packages. Manage feature flags with expiry dates in LaunchDarkly or similar.
C
Verify Release builds exclude debug-only controllers. Enable analyzer rules for unused internal classes.
#if DEBUG
[ApiController]
[Route("debug/[controller]")]
public class DiagnosticsController : ControllerBase
{
[HttpGet("ping")]
public IActionResult Ping() => Ok("debug");
}
#endif
Enable CA1812 and related rules. Use Azure App Configuration with mandatory flag retirement.
Go
Isolate debug handlers behind build tags not used in production builds.
//go:build debug
package main
import "net/http/pprof"
func registerDebug(mux *http.ServeMux) {
mux.HandleFunc("/debug/pprof/", pprof.Index)
}
//go:build !debug
package main
func registerDebug(mux *http.ServeMux) {}
Periodically diff registered routes against documentation. Run staticcheck unused-code reports before release branches.
Verify During Review
- Dead code, duplicate legacy endpoints, and test-only routes are removed or strictly environment-gated.
- Feature flags that expose alternate code paths have owners, expiry dates, and production defaults that fail secure.
- Deprecated API versions receive the same authentication, authorization, and input validation as current versions—or are shut down.
- Production builds exclude debug, profiling, and administrative utilities not required in prod.
- Coverage or static analysis confirms unreachable security-sensitive code is deleted, not commented out.
- Technical debt tickets for obsolete security paths are prioritized alongside new feature work.