Before Deployment: 30 Security Checks I Run on Every PHP Application
A practical pre-deployment security checklist for PHP applications. Learn the 30 checks I use before putting an application into production, covering dependencies, authentication, sessions, authorization, SQL injection, XSS, CSRF, file uploads, SSRF, secrets, logging, HTTPS, server exposure, backups, and more.

Deploying an application to production should mean more than running git pull, installing dependencies, and restarting PHP-FPM.
Before exposing an application to the internet, I like to perform one final security review.
This is not meant to replace a penetration test, a full code audit, or a comprehensive security assessment based on the OWASP ASVS. The goal is different: to have a practical pre-deployment checklist that can catch common problems before they reach production.
My checklist covers application code, dependencies, authentication, data handling, files, PHP configuration, and infrastructure.
1. Am I using a supported PHP version?
The first check is simple:
php -vI do not like putting a new application into production using a PHP version that is already close to the end of its support lifecycle.
The PHP project maintains supported versions for several years, with security fixes continuing beyond the active support period.
At the time this article was written, for example, PHP 8.2 is still receiving security fixes until December 31, 2026, while PHP 8.3 is supported until December 31, 2027, PHP 8.4 until December 31, 2028, and PHP 8.5 until December 31, 2029.
It is not enough to ask:
“Is it working?”
The better question is:
“Will this version continue receiving security fixes throughout the expected lifetime of the application?”
2. Did I run composer audit?
Before deployment:
composer auditComposer provides a built-in audit mechanism that can identify known security issues in dependencies.
I can also use:
composer audit --lockedto audit the dependencies defined by the lock file.
This is the kind of check I want to see in CI/CD rather than relying on someone remembering to run it manually.
A vulnerability in a third-party library can be just as dangerous as a vulnerability in my own code.
3. Is composer.lock committed?
For production applications, I verify that composer.lock is present and up to date.
For example:
git status
git diff composer.lockI also run:
composer validate --strictThe goal is to avoid having development, CI, and production install different dependency versions.
Reproducible deployments are an important part of security.
4. Have I removed development dependencies from production?
Production does not need every tool that was used during development and testing.
For example:
composer install --no-dev --optimize-autoloaderThis reduces the installed footprint and the number of components exposed by the production environment.
Debugging tools, profilers, testing frameworks, and development utilities generally do not belong on a production server.
5. Are there any secrets in Git?
I search for things such as:
password=
secret=
api_key=
token=
AWS_SECRET_ACCESS_KEY=
DATABASE_URL=
PRIVATE_KEYI also check:
git log --all -- .envand:
git ls-files | grep -E '(^|/)\.env'Taking secrets out of the repository is only the first step.
I also want to make sure that secrets are not:
- exposed in logs;
- included in exceptions;
- embedded in frontend code;
- baked into Docker images;
- stored with unnecessary permissions;
- impossible to rotate.
Secrets should have controlled access, proper storage, auditing, and a rotation strategy.
6. Is the production .env actually protected?
Removing .env from Git is not enough if the file is still publicly accessible from the web.
I check:
ls -laand, depending on the environment:
chmod 600 .envI also verify that the web server cannot expose configuration files, backups, temporary files, or other internal artifacts.
The application should expose only what is intentionally public.
7. Is APP_DEBUG disabled?
For frameworks such as Symfony and Laravel, I explicitly review the production configuration.
For example:
APP_ENV=prod
APP_DEBUG=0I do not want production exceptions exposing:
stack traces
absolute file paths
SQL statements
environment variables
internal service names
credentialsDetailed errors are extremely useful during development and extremely useful to an attacker in production.
8. Is PHP exposing unnecessary information?
I review PHP settings such as:
display_errors = Off
display_startup_errors = Off
log_errors = OnI also check whether the runtime is revealing unnecessary implementation details.
For example, the expose_php setting can cause PHP to advertise itself through the X-Powered-By response header.
Hiding a version number is not a primary security control, but reducing unnecessary information exposure is still part of a clean production configuration.
9. Are database queries parameterized?
I search for code like:
$sql = "SELECT * FROM users WHERE email = '$email'";That is a red flag.
I prefer:
$stmt = $pdo->prepare(
'SELECT * FROM users WHERE email = :email'
);
$stmt->execute([
'email' => $email,
]);Or the equivalent mechanism provided by the ORM or framework.
I check this not only for SELECT, but also for:
INSERT
UPDATE
DELETE
ORDER BY
LIKE
LIMIT
filters
reports
administrative queriesThe basic rule is simple:
Never concatenate untrusted input directly into SQL.
10. Is all input validated on the server?
I do not rely only on:
required
maxlength
type="email"
pattern="..."Frontend validation improves user experience, but it is not a security boundary.
The backend must validate the data again.
For example:
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address.');
}I prefer explicit validation rules and, whenever possible, allowlists.
11. Is the application protected against XSS?
I pay close attention to places where user-controlled data is rendered directly into HTML.
For example:
echo $name;Depending on the context, the output may need to be escaped:
echo htmlspecialchars(
$name,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);Frameworks and template engines usually provide automatic escaping, but I still review places where escaping was disabled intentionally.
I also check:
- stored HTML;
- rich text;
- URL parameters;
- error messages;
- values injected into JavaScript;
- HTML attributes.
Output encoding needs to be appropriate for the context where the data is used.
12. Are state-changing forms protected against CSRF?
This is especially important for applications using cookie-based authentication.
I review operations such as:
create
update
delete
password changes
email changes
administrative actions
paymentsWhen the framework provides CSRF protection, I prefer using the built-in mechanism.
I also avoid state-changing operations through GET.
Bad:
GET /users/123/deleteBetter:
DELETE /users/123or a protected POST, depending on the application’s architecture.
13. Are sessions configured securely?
Sessions are easy to overlook.
In PHP, I review settings such as:
session.use_cookies = On
session.use_only_cookies = On
session.use_strict_mode = On
session.use_trans_sid = OffI also check that session cookies use:
Secure
HttpOnly
SameSiteThe goal is to reduce the chances of session theft and session abuse.
14. Is the session ID regenerated after authentication?
After login, I do not want to keep the same session identifier.
For example:
session_regenerate_id(true);I also review situations such as:
login
logout
password changes
privilege changes
account switchingSession management should be designed so that attackers cannot take advantage of predictable or attacker-controlled session identifiers.
15. Are passwords stored using an appropriate password hashing algorithm?
If I find:
md5($password)or:
sha1($password)being used to store passwords, that needs to be fixed.
Passwords should never be stored as plaintext or with fast general-purpose hashes.
For example:
$passwordHash = password_hash(
$password,
PASSWORD_ARGON2ID
);and:
password_verify($password, $passwordHash);Password hashing should use an algorithm specifically designed to make brute-force attacks expensive.
16. Is there protection against brute-force attacks?
I especially review:
/login
/reset-password
/forgot-password
/verify-code
/2faI look for:
rate limiting
temporary lockouts
IP-based controls
account-based controls
captcha when appropriate
logging
alertsA login endpoint can still be vulnerable even when password storage is implemented correctly.
Security does not end at password_verify().
17. Does authorization happen on the backend?
One of my favorite questions is:
“If I remove the button from the UI, is the operation still blocked?”
If the answer is no, there is a problem.
For example:
GET /admin/users/10should not become accessible simply because someone discovered the URL.
The backend should verify:
authenticated user?
required permission?
owns the resource?
allowed to perform this operation?This is especially important for APIs and IDOR/BOLA vulnerabilities.
18. Are predictable resource IDs protected by authorization?
Changing:
/customer/100to:
/customer/101must not be enough to access another user’s resource.
Using UUIDs can make enumeration harder, but UUIDs do not replace authorization.
The rule is:
identification != authorizationEven with:
/customer/550e8400-e29b-41d4-a716-446655440000the application still has to determine whether the current user is allowed to access that resource.
19. Are file uploads properly protected?
File uploads deserve a dedicated security review.
I check:
allowed extension
MIME type
actual file content
maximum size
filename
storage location
permissions
authorizationI never trust only:
$_FILES['file']['type']The value sent by the client can be manipulated.
I prefer approaches such as:
- allowlisted file types;
- content validation;
- size limits;
- generated filenames;
- storage outside the web root whenever possible.
I also check whether a file such as:
.php
.phtml
.phar
.htaccessor another executable format can be uploaded into a directory served by the web server.
20. Is the application protected against path traversal?
I look for code like:
$file = $_GET['file'];
readfile($file);or:
include $_GET['page'];or:
$path = $basePath . '/' . $_GET['filename'];Inputs that influence filesystem paths need careful handling.
I pay particular attention to parameters such as:
file
path
filename
template
document
download
attachment
image21. Can users influence system commands?
I carefully search for:
exec($command);
shell_exec($command);
system($command);
passthru($command);And especially cases like:
shell_exec("convert " . $filename);Even when an external command is genuinely required, user input should be strictly controlled and command construction should be done safely.
I treat any user-controlled value that gets close to:
shell
CLI
exec
bash
ImageMagick
FFmpeg
git
zip
unzipas something that deserves manual review.
22. Is there an SSRF risk?
If the application accepts a URL from the user and then does something like:
$response = file_get_contents($url);I stop and investigate.
Typical examples include:
webhooks
image imports
integrations
URL fetchers
PDF generators
proxies
link previews
callbacksSSRF can allow an application to access internal resources that were never intended to be exposed to users.
Where possible, I prefer strict allowlists of trusted destinations and carefully validate redirects and final destinations.
23. Is CORS restrictive?
I look for configurations such as:
Access-Control-Allow-Origin: *and ask:
“Does this API really need to be accessible from every origin?”
If not, I explicitly define the allowed domains.
For example:
https://app.example.com
https://admin.example.cominstead of allowing everything.
CORS should reflect the actual trust model of the application.
24. Is the application always behind HTTPS?
In production:
HTTP → HTTPSI verify:
curl -I http://example.com
curl -I https://example.comI want to confirm:
- a valid certificate;
- correct redirects;
- Secure cookies;
- no sensitive traffic over HTTP;
- correct reverse-proxy behavior.
I also consider HSTS when the domain and deployment model are ready for it.
25. Are security headers configured?
I inspect the response with:
curl -I https://example.comDepending on the application, I review headers such as:
Content-Security-Policy
Strict-Transport-Security
X-Content-Type-Options
Content-Security-Policy: frame-ancestors ...
Referrer-Policy
Permissions-PolicyThese headers can add another layer of defense against XSS, clickjacking, MIME sniffing, and unwanted information exposure.
I do not blindly copy a generic header configuration, though.
Security headers need to match the application’s actual behavior.
26. Are logs leaking sensitive information?
I search logs and logging calls for things such as:
password
token
authorization
cookie
secret
credit_card
cpf
cnpj
api_keyEspecially in code like:
$logger->debug($request);or:
Log::info($request->all());Logs should help with incident investigation without becoming a database of credentials and sensitive personal information.
I also review who can access the logs and how long they are retained.
27. Are errors logged internally but sanitized for users?
A good production pattern is:
user → generic error message
server → detailed internal logFor example:
Unable to process the request.instead of:
PDOException:
SQLSTATE[HY000]
/var/www/app/src/Repository/UserRepository.php:143Production error messages should not expose implementation details that make attacks easier.
28. Are backups protected?
Security does not end with the application.
I review:
database backups
uploaded files
configuration backups
permissions
encryption
retention
access
restore proceduresOne important question is:
“Does my backup contain the same credentials and sensitive data as production?”
And another:
“Have I actually tested restoring it?”
A backup that has never been restored is mostly an assumption.
29. Is the server exposing services that should remain internal?
Before deployment, I inspect listening ports:
ss -lntupor:
sudo ss -lntupI want to know whether services such as:
MySQL
PostgreSQL
Redis
RabbitMQ
MongoDB
PHP-FPM
Adminer
Portainerare exposed to the internet without a good reason.
The architecture should preferably keep internal services internal.
For example:
Internet
|
v
Nginx/Apache
|
v
PHP-FPM
|
+----> PostgreSQL
|
+----> Redisrather than:
Internet
|
+----> PostgreSQL
+----> Redis
+----> RabbitMQ30. Did I perform at least one manual test as an attacker?
This is my final check.
After going through the checklist, I try to break my own application.
I do not necessarily start with complex tools.
I try simple things such as:
access an admin URL without logging in
change a resource ID
send unexpected fields
send empty values
send oversized strings
change HTTP methods
repeat requests quickly
inject HTML into fields
upload unexpected files
alter headers
remove CSRF tokens
access endpoints directly
reuse an old session after logoutI also use tools such as:
curl
Postman
Burp Suite
OWASP ZAPThe goal is to move beyond:
“The normal flow works.”
and ask:
“What happens when someone does something I did not expect?”
My Pre-Deployment Checklist
In day-to-day work, I like reducing everything to a list that can be checked off:
[ ] 01. PHP version is supported
[ ] 02. composer audit was executed
[ ] 03. composer.lock is committed and up to date
[ ] 04. Development dependencies are excluded from production
[ ] 05. Secrets are not stored in Git
[ ] 06. .env and configuration files are protected
[ ] 07. Debug mode is disabled
[ ] 08. Unnecessary PHP information is hidden
[ ] 09. SQL queries are parameterized
[ ] 10. Backend input validation is enabled
[ ] 11. XSS protection is in place
[ ] 12. CSRF protection is configured
[ ] 13. Sessions are securely configured
[ ] 14. Session IDs are regenerated after authentication
[ ] 15. Passwords use appropriate password hashing
[ ] 16. Brute-force/rate limiting controls are reviewed
[ ] 17. Authorization is enforced by the backend
[ ] 18. IDOR/BOLA risks are reviewed
[ ] 19. File uploads are protected
[ ] 20. Path traversal risks are reviewed
[ ] 21. Command execution is reviewed
[ ] 22. SSRF risks are reviewed
[ ] 23. CORS is restrictive
[ ] 24. HTTPS is enforced
[ ] 25. Security headers are reviewed
[ ] 26. Logs do not expose secrets
[ ] 27. Errors do not expose sensitive information
[ ] 28. Backups are protected and tested
[ ] 29. Internal services are not publicly exposed
[ ] 30. A manual security test was performedSecurity Is Not a Step After Development
One thing I have learned from working with PHP applications is that security is rarely solved by adding a single library to a project.
It appears in small decisions:
how SQL is built;
how files are stored;
how sessions are created;
how authorization is checked;
how errors are logged;
how secrets are stored;
how the server is exposed.An application can use modern PHP, an up-to-date framework, and recent dependencies and still contain a serious vulnerability caused by a few lines of code.
That is why, before every deployment, I like asking:
“What can a malicious user make this application do?”
That question often finds problems that functional testing does not.
And this checklist is not a replacement for a full security assessment. For more critical applications, I would still consider code review, automated security testing, SAST/DAST, infrastructure assessments, and a more complete verification process based on standards such as the OWASP ASVS.
The goal is not to make the application “impossible to attack”.
That does not exist.
The goal is to prevent a basic security issue from reaching production simply because nobody stopped to ask the right question before deployment.
References
- OWASP Application Security Verification Standard (ASVS)
- OWASP Cheat Sheet Series — Secure Code Review
- OWASP Cheat Sheet Series — Secrets Management
- OWASP Cheat Sheet Series — Session Management
- OWASP Cheat Sheet Series — SQL Injection Prevention
- OWASP Cheat Sheet Series — File Upload
- OWASP Cheat Sheet Series — HTTP Security Headers
- PHP Manual — Session Security
- PHP — Supported Versions
- Composer —
audit