Platform
Web Application Security and Cloud Operations Guardrails
A practical review of web application attack surfaces, container boundaries, cloud storage controls, and operational security guardrails.
Web security doesn't last long just by memorizing the names of vulnerabilities. Items such as SQL injection, file upload, missing JWT signature verification, and directory traversal may seem like independent attacks, but when viewed in an operating environment, they mostly come back to the same question. To what extent can the input sent by the user be trusted? What information does the application display to the outside world in a failure situation? How far do containers and cloud infrastructure allow attackers to expand the small gaps they gain?
This connection becomes clearer if you launch a lab environment such as DVWA, PortSwigger Web Security Academy, or bWAPP with Docker. Running a vulnerable application in a container is not a simple hacking exercise, but is more like observing the path that a web request leads to the application runtime, file system, network port, authentication token, and database permissions.
Why we first created an isolated lab environment with Docker
Security practices intentionally run vulnerable services. So, if you inadvertently expose it to a local PC or public network, the practice itself becomes dangerous. Using containers, vulnerable applications can be run within an independent network and file system, and only necessary ports can be opened.
docker network create security-lab
docker run -d \
--name dvwa \
--network security-lab \
-p 127.0.0.1:8888:80 \
vulnerables/web-dvwa
docker run -d \
--name bwapp \
--network security-lab \
-p 127.0.0.1:8080:80 \
mupersei/bwappThe important part here is to specify the binding address, such as 127.0.0.1:8888:80. Running with -p 8888:80 can make it accessible from all interfaces on the host. The difference may seem small on a personal laptop, but if you run the same command on a cloud VM, vulnerable services on the Internet may be opened depending on NSG, security group, and firewall settings.
The same principle applies when creating a training VM in the cloud. SSH is restricted to my IP, and HTTP ports are opened only when absolutely necessary. It is better not to reuse images for security practice and to delete containers and VMs after the practice.
docker ps
docker logs dvwa --tail 50
docker inspect dvwa --format '{{json .NetworkSettings.Networks}}'
docker stop dvwa bwapp
docker rm dvwa bwapp
docker network rm security-labContainers are a starting point for isolation, not a complete security perimeter. Web application vulnerabilities can lead to host compromise if the Docker daemon socket is exposed, the container is running with root privileges, and the host directory is overmounted. Therefore, even in practice, it is a good idea to apply settings such as non-root execution, removal of unnecessary capabilities, and read-only file system as a habit.
services:
app:
image: my-web-lab:0.1.0
ports:
- "127.0.0.1:8080:8080"
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmpInformation exposure is not a small mistake but the starting point of an attack
Information disclosure vulnerabilities often seem harmless on the surface. The exception message may show the framework version, a sensitive path in /robots.txt, or a backup file remaining in the web root. However, the attacker combines this small amount of information to decide the next action.
In the PortSwigger exercise, if an unexpected string is entered in the product ID parameter, a server error occurs and the framework version is exposed in the error message. Additionally, if a debug page such as /cgi-bin/phpinfo.php remains, you can check environment variables, extension modules, server paths, and sometimes even secret values. If a .bak file remains in the /backup directory and directory indexing is turned on, source code and DB connection information may be leaked.
Defense of this problem does not end with “hiding the error.” The application, web server, deployment pipeline, and secret management must work together.
server_tokens off;
location ~ /\.(git|svn|hg) {
deny all;
return 404;
}
location ~* \.(bak|old|backup|swp|sql)$ {
deny all;
return 404;
}The application only shows generalized errors to the user and leaves detailed information in a structured log. Logs should include request IDs for traceability, but mask values such as tokens and passwords.
app.use((err, req, res, next) => {
req.log.error(
{
requestId: req.id,
path: req.path,
method: req.method,
errorName: err.name,
},
"request failed",
);
res.status(500).json({
message: "Internal server error",
requestId: req.id,
});
});In the cloud, secrets are not stored in images or storage, but are separated into managed services such as Secret Manager, Key Vault, and Parameter Store. Once a secret is entered into the Git history, it does not become safe even if it is deleted from the file. Tokens must be discarded and new ones issued, and if possible, the inflow itself should be reduced through secret scanning and pre-commit hooks.
Authentication is a broader issue than the login screen
By practicing brute force attacks, you can see how easily a system with no limit to the number of login failures can be broken. At DVWA's lower security level, the password list can be iterated through an automated script or Burp Suite Intruder. There is a waiting period after failure in the intermediate stage, but dictionary attacks are still possible. At higher levels, CSRF tokens and random waiting time are added, increasing the cost of the attack.
From an operational perspective, this line of defense must be divided into several layers. Applications must look at both the number of failures by account and the request rate by IP. API Gateway or WAF first reduces abnormal request patterns, and the authentication service manages account lockout, MFA, and session disposal policies.
const key = `login:${ip}:${email}`;
const attempts = await redis.incr(key);
await redis.expire(key, 60);
if (attempts > 10) {
throw new TooManyRequestsException("Too many login attempts");
}JWT is similar. JWTs are great for reducing server session storage and passing authentication information between services, but without signature verification, tokens are subject to simple string manipulation. In the practice, attempt to elevate privileges by changing the sub value in the payload to the administrator account or manipulating the alg in the header to none. In the operating code, the algorithms to be allowed must be specified, and the issuer, audience, and expiration must all be verified.
import jwt from "jsonwebtoken";
const payload = jwt.verify(token, publicKey, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
audience: "api.example.com",
});If you use cookie-based sessions, the HTTPOnly, Secure, and SameSite settings should be default. Session IDs should be rotated on events such as re-login, permission changes, and MFA completion, and server-side sessions should also be discarded upon logout. Deleting a single token stored on the client is insufficient for multiple devices and takeover scenarios.
Set-Cookie: session=opaque-session-id; HTTPOnly; Secure; SameSite=Lax; Path=/Input values are verified, queries are not assembled
SQL Injection is an older vulnerability, but it is still important. The principle is simple. If user input is directly attached to the SQL string, the input value becomes a query structure rather than data.
SELECT *
FROM users
WHERE user_id = '$id';If a value such as ' OR '1'='1 is entered here, the condition can always be true. Using UNION, you can also attempt to query columns of another table. The core of defense is Prepared Statement. Instead of concatenating input values into strings, separate the query structure and values.
const user = await db.query(
"SELECT id, email, name FROM users WHERE email = $1",
[email],
);Input validation is not just a device for SQL Injection. Even values that appear to be “simple strings” such as page numbers, sorting criteria, file names, redirect URLs, and callback URLs can point to other resources within the system. It is safer to map the sorting criteria to an allow list and manage the file name as an identifier separate from the actual storage path.
const sortColumns = {
createdAt: "created_at",
name: "name",
} as const;
const sortColumn = sortColumns[query.sort as keyof typeof sortColumns] ?? "created_at";Input problems do not end just by being careful about SQL. OS commands such as ping, nslookup, convert, ffmpeg, and tar may be executed from operating tools or internal management functions. At this time, if the user input is attached to the shell command string, it becomes Command Injection.
// avoid
exec(`ping -c 1 ${host}`);The attacker would replace host with example.com; You can try to execute a command different from the original intention by entering a value such as cat /etc/passwd. The basic defense is to separate arguments into an array without going through the shell.
import { spawn } from "node:child_process";
function pingHost(host: string) {
if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
throw new Error("invalid host");
}
return spawn("ping", ["-c", "1", host], {
shell: false,
timeout: 3000,
});
}A better approach is to reduce the execution of OS commands on the application server itself. Tasks such as image resizing, file conversion, and backup compression are performed in separate workers or restricted sandboxes, and the execution account's permissions are minimized. If it is a container, look at read-only filesystem, dropped capabilities, CPU/memory limit, and seccomp profile.
services:
worker:
image: image-worker:0.1.0
read_only: true
cap_drop:
- ALL
pids_limit: 128
mem_limit: 512m
security_opt:
- no-new-privileges:trueCommand Injection appears to be a web input validation issue, but the actual extent of damage is determined by runtime permissions and infrastructure permissions. If an application process has access to credentials with cloud administrator privileges, a small input vulnerability can grow into an account compromise. So, secret access permissions, instance role, container role, and file system permissions must be reduced.
File paths and uploads are considered separate trust boundaries
Directory traversal occurs when user input is mixed into a file path. The method is to read the server file by inserting a value such as ../../../etc/passwd into the parameter that receives the image file name. Some filters can be bypassed by nested strings or double URL encoding by simply removing ../. Therefore, what is more important than the blacklist filter is the structure that prevents the user from directly determining the path.
import path from "node:path";
const baseDir = path.resolve("/srv/app/uploads");
const requested = path.resolve(baseDir, filename);
if (!requested.startsWith(baseDir + path.sep)) {
throw new BadRequestException("invalid file path");
}Be more careful when uploading files. If you only check the extension, you may miss names like shell.php.jpg, and the MIME type can be manipulated by the client. File signature inspection, extension whitelisting, storage separation, removal of execution permissions, and separation of public URLs of uploaded files must also be considered.
location /uploads/ {
types {
image/jpeg jpg jpeg;
image/png png;
image/webp webp;
}
default_type application/octet-stream;
add_header X-Content-Type-Options nosniff;
}If possible, store uploaded files in object storage rather than in the web root of the application server. When using S3 or Azure Blob Storage, it is safer to have the application verify and then issue a signed URL for a limited time rather than opening it as a public bucket/container.
The cloud shared responsibility model is a boundary line, not a checklist
The first thing to check when organizing cloud security is the shared responsibility model. In IaaS, users must take care of OS patches, network security, application runtime, and account permissions themselves. In PaaS, parts of the operating system are transferred to the provider's responsibility, but authentication, data, application settings, and network exposure are still the user's responsibility. With SaaS, the provider is responsible for more areas, but account security and data access policies remain.
If you do not know this boundary, you will have wrong expectations when using managed services. For example, with Azure App Service, you do not need to patch the VM yourself, but secret management in environment variables, authentication settings, outbound connections, private endpoint configuration, and log access rights are still design subjects. With AKS/EKS, part of the control plane is managed, but node images, RBAC, NetworkPolicy, workload identity, and ingress TLS must be handled separately.
A security group or NSG is not a “configuration that opens a port,” but a device that represents a network trust boundary. The web server must only be accessible from the Internet, and the application server must be accessible only from a load balancer or private subnet. The database is accessed only from the application subnet, and management access goes through Bastion or VPN.
Secrets and permissions leak first in the deployment pipeline
As you practice information disclosure vulnerabilities, you'll see how easily sensitive values can be revealed in error messages, backup files, .git directories, and debug pages such as phpinfo(). In an operating environment, this problem is not simply a matter of “let’s delete the file.” You must follow the path to determine where the secret can remain among code, images, logs, CI/CD variables, environment variables, object storage, and backup files.
The most important structure to avoid is putting credentials in application code or images. Once a secret enters the Git history or container image layer, it may remain even if you simply delete it from the current file. It is better to place the secret in the secret manager rather than in the storage, and have the application read only the values needed as a runtime identity.
Do not
- commit .env files
- bake DB passwords into Docker images
- print tokens in startup logs
- store long-lived cloud keys in CI scripts
Prefer
- GitHub secret scanning
- masked CI/CD variables
- Azure Key Vault or AWS Secrets Manager
- workload identity / managed identity
- short-lived credentials and rotationAuthority should be viewed in the same way. Just because an application accesses the DB, it does not require DDL permissions. Just because the file upload service uses Blob storage, full storage account administrator privileges are not required. Separating read, write, delete, list search, and key management permissions can reduce the scope of failures and infringements.
grant select, insert, update on payment_requests to app_user;
grant select, insert on payment_status_history to app_user;
revoke drop any table from app_user;It's the same in the cloud. The deployment pipeline can have permission to change the infrastructure, but the runtime application should only have the permissions necessary for its data path. When connecting GitHub Actions, Vercel, Azure, and AWS, it is better to consider OIDC-based federation or managed identity first rather than storing long-term access keys.
Without the possibility of observation, it is difficult to verify the defense line.
Security settings are not something you just set at the time of deployment. You should observe whether login failures increase rapidly, abnormal 4xx numbers are concentrated in a specific API, upload failures repeat, or JWT validation errors suddenly increase. Without these signals, it is impossible to know whether the defenses are actually working.
At a minimum, application logs should contain structured request ID, user identifier, client IP, path, status code, processing time, and failure type. However, passwords, authorization headers, cookies, and personal identification information are not left in the log.
{
"level": "warn",
"event": "login_failed",
"requestId": "req_01HX...",
"ip": "203.0.113.10",
"userId": null,
"reason": "invalid_password",
"path": "/auth/login"
}If you use Prometheus and Grafana, you also see security events along with general operational metrics. Authentication failure rate, number of 429 responses, number of WAF blocks, 5xx error rate, and DB connection error are indicators that can explain both failure and security. By attaching OpenTelemetry, it becomes easier to trace where a specific request failed as it passed through the edge, application, and database.
Operational notifications are quickly ignored if they are set too broadly. So, rather than treating all “security events” with the same severity, we should divide them into those that can lead to user error and those that need immediate attention.
Notice
- single validation failure
- user typo on login
- expected 404 for missing resource
Warning
- repeated login failures for one account
- JWT validation failures from same IP range
- upload blocked by file signature mismatch
Critical
- secret-looking value printed in logs
- public access enabled on private storage
- admin endpoint hit from unknown network
- sudden spike in 403 / 429 / 5xx after deploymentWith this standard, failure response and security response are not separated. For example, if 403s rapidly increase immediately after deployment, it may be due to a change in authentication policy or RBAC, and if 429s are repeated from a specific IP, brute force or scraping attempts may be suspected. Logs, metrics, traces, and audit events must lead to the same request ID or user ID to narrow down the cause.
Cleanup
The purpose of web vulnerability practice is to learn attack techniques, but more importantly, it is to develop an eye for seeing system boundaries. Information exposure leads to problems with secret management and distribution pipeline, and authentication vulnerabilities lead to rate limits, MFA, session policies, and token verification. SQL injection and file upload vulnerabilities lead to input value validation, storage isolation, removal of execution privileges, and least privileged DB accounts. The Docker lab environment extends to operational habits such as container isolation, network binding, non-root execution, and image scanning.
In the cloud, this all sits on a shared responsibility model. Even if the provider takes care of some of the physical infrastructure and managed services, the application's input values, authentication flow, data access rights, network exposure, secret delivery method, logs and notifications still need to be designed. Security is not about adding a specific tool, but rather about constantly narrowing and verifying the path from when a request comes in to reaching data and leaving a log.
Unauthorized copying, redistribution, and automated scraping are not permitted. Please cite the original URL when referencing this article.