A Java application can run on a current JDK and still expose private data if it accepts an untrusted filename, logs a session token, or has more permissions than it needs. Start by tracing what enters the program, what it can access, and what it sends back. Java’s type system and memory management prevent some classes of bugs; they do not make application behavior safe by default.
Keep the runtime and dependencies current
Use a supported JDK and apply security updates. Record the version used to build the application and the version running in each environment. Updating a developer laptop does not update a deployed server. Remove obsolete runtimes, especially if build tools might select them by mistake.
Libraries are part of the application’s attack surface, including those pulled in indirectly. Use a maintained dependency manifest with Maven or Gradle, and lock or otherwise record resolved versions if your workflow supports it. Review update notices, investigate reported vulnerabilities in the versions you use, and test upgrades before deployment. A vulnerability report calls for an exposure check; it does not mean every application using that library is exploitable.
- Add dependencies for a clear purpose, not for trivial tasks.
- Remove unused libraries so they no longer need security and maintenance attention.
- Keep development-only tools out of the production runtime when the build permits it.
- Rebuild and redeploy after an update. Changing a version number does not replace running code.
Keep dependency sources and build configuration under team control. Review an unexpected repository change or a new build plugin as carefully as an application code change: build steps can execute code.

Treat input as untrusted at every boundary
Input can arrive as an HTTP parameter, JSON body, uploaded file, message-queue event, configuration value, or response from another service. Validate it when it enters the application. Define what you accept rather than trying to list every malicious string you might encounter.
Validate meaning, size, and format
A page number should be an integer within a sensible range. A username needs a length limit and a defined set of supported characters. For an uploaded document, cap its size and check that its content matches an allowed format; the filename extension and client-supplied content type are not enough. Reject malformed requests with a clear error that does not echo sensitive input.
Validation is only one boundary. Encode user-provided text for the HTML context when displaying it on a page, and use the appropriate encoding when placing it in a URL or another output format. Do not build commands, SQL statements, or template expressions by concatenating untrusted values; use the relevant structured API. One general-purpose “sanitize” function cannot safely handle all these contexts.
Keep file access inside its intended directory
A download endpoint should not turn a submitted filename directly into an arbitrary filesystem path. If users select stored files, an opaque identifier mapped to an application-controlled location is often simpler to secure than a free-form path. If paths are necessary, resolve each candidate against a fixed base directory, normalize it, and check that it remains inside that directory. Symbolic links and filesystem changes between checking and opening a file can complicate that check. For sensitive files, arrange storage so the application cannot reach unrelated locations at all.
Make permissions explicit
Authentication establishes who a user is; authorization determines whether that user can perform a specific action on a specific resource. A successful login must not grant access to every account record or project. Check permission before returning a record, then check the permission for the particular change before modifying it. A hidden button or hard-to-guess identifier is no substitute.
Put authorization checks in a consistent server-side layer so a new endpoint is less likely to miss them. Deny access when a role or resource relationship is unknown. Separate administrative operations from ordinary user actions, and test both allowed and denied requests. In a local test, create two accounts and verify that one cannot read or edit the other’s private record by changing an identifier in an otherwise valid request.
Limit the Java process as well. Run the service under a dedicated operating-system identity that can access only the files and directories it needs. Give its database account only the permissions required for its operations. Keep development credentials out of production, and do not run routine application processes as an administrator. These limits reduce the damage if an application-level check fails.

Handle secrets and sensitive data deliberately
Keep passwords, API keys, and signing keys out of source files, committed configuration, and container images. Supply them through an approved runtime mechanism with access controls, and limit who can read or change them. Environment variables can be useful during local development, but they are not automatically secret: deployment settings, process inspection, or diagnostic output may reveal them. Avoid printing configuration objects or exception details that could contain credentials.
Store user passwords with a reputable password-hashing implementation, a unique salt, and an appropriate work factor. Ordinary fast hashes are not suitable for password storage. Data that must later be recovered needs encryption, which has different requirements. Use maintained libraries and platform APIs rather than inventing a cryptographic format or managing keys in application code without a plan.
Collect and retain only the data the application needs. Log security-relevant events, including failed authorization decisions and unexpected validation failures, but leave out passwords, access tokens, full payment details, and sensitive request bodies. A request identifier, action, and outcome can help operators investigate without making logs a second store of private information. Restrict log access and set a retention period.
Protect network connections and responses
Use HTTPS for traffic carrying credentials or private data. Java HTTP clients should validate server certificates and hostnames. Disabling those checks to get a development connection working is a shortcut that may reach production; fix the certificate or trust configuration instead. Use TLS for database and service connections when traffic crosses a network boundary that needs protection.
Return controlled errors to clients. A response can say that a request is invalid or unavailable without exposing a stack trace, filesystem path, database query, or configuration value containing a secret. Keep detailed exception information in restricted logs, subject to the same sensitive-data rules. For browser-facing applications, review cookie attributes, cross-origin settings, and response headers for the way the application is used. Permissive settings can undermine otherwise sound Java code.
Build security checks into normal development
Security tests work best when they exercise real application behavior. Alongside a successful request, test oversized input, an invalid format, a missing login, and an authenticated user requesting a resource they do not own. Add a regression test when fixing a security defect so the same path cannot reopen unnoticed. Run dependency checks and automated tests in the build pipeline, then review the findings rather than treating a green badge as a guarantee.
For a small practice project, create a private document endpoint with two test users and a dedicated document directory. Write tests showing that the owner can download a document, the second user receives an access-denied response, and a nonexistent identifier reveals neither a server path nor a stack trace. Rerun them whenever you change the endpoint’s routing, storage, or permission logic.
