Skip to content
Fernando Valler

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.

Before Deployment: 30 Security Checks I Run on Every PHP Application

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:

bash
php -v

I 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:

bash
composer audit

Composer provides a built-in audit mechanism that can identify known security issues in dependencies.

I can also use:

bash
composer audit --locked

to 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:

bash
git status
git diff composer.lock

I also run:

bash
composer validate --strict

The 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:

bash
composer install --no-dev --optimize-autoloader

This 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:

text
password=
secret=
api_key=
token=
AWS_SECRET_ACCESS_KEY=
DATABASE_URL=
PRIVATE_KEY

I also check:

bash
git log --all -- .env

and:

bash
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:

bash
ls -la

and, depending on the environment:

bash
chmod 600 .env

I 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=0

I do not want production exceptions exposing:

text
stack traces
absolute file paths
SQL statements
environment variables
internal service names
credentials

Detailed 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:

ini
display_errors = Off
display_startup_errors = Off
log_errors = On

I 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:

php
$sql = "SELECT * FROM users WHERE email = '$email'";

That is a red flag.

I prefer:

php
$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:

text
INSERT
UPDATE
DELETE
ORDER BY
LIKE
LIMIT
filters
reports
administrative queries

The 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:

html
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:

php
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:

php
echo $name;

Depending on the context, the output may need to be escaped:

php
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:

text
create
update
delete
password changes
email changes
administrative actions
payments

When the framework provides CSRF protection, I prefer using the built-in mechanism.

I also avoid state-changing operations through GET.

Bad:

text
GET /users/123/delete

Better:

text
DELETE /users/123

or 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:

ini
session.use_cookies = On
session.use_only_cookies = On
session.use_strict_mode = On
session.use_trans_sid = Off

I also check that session cookies use:

text
Secure
HttpOnly
SameSite

The 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:

php
session_regenerate_id(true);

I also review situations such as:

text
login
logout
password changes
privilege changes
account switching

Session 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:

php
md5($password)

or:

php
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:

php
$passwordHash = password_hash(
    $password,
    PASSWORD_ARGON2ID
);

and:

php
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:

text
/login
/reset-password
/forgot-password
/verify-code
/2fa

I look for:

text
rate limiting
temporary lockouts
IP-based controls
account-based controls
captcha when appropriate
logging
alerts

A 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:

text
GET /admin/users/10

should not become accessible simply because someone discovered the URL.

The backend should verify:

text
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:

text
/customer/100

to:

text
/customer/101

must not be enough to access another user’s resource.

Using UUIDs can make enumeration harder, but UUIDs do not replace authorization.

The rule is:

text
identification != authorization

Even with:

text
/customer/550e8400-e29b-41d4-a716-446655440000

the 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:

text
allowed extension
MIME type
actual file content
maximum size
filename
storage location
permissions
authorization

I never trust only:

php
$_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:

text
.php
.phtml
.phar
.htaccess

or 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:

php
$file = $_GET['file'];

readfile($file);

or:

php
include $_GET['page'];

or:

php
$path = $basePath . '/' . $_GET['filename'];

Inputs that influence filesystem paths need careful handling.

I pay particular attention to parameters such as:

text
file
path
filename
template
document
download
attachment
image

21. Can users influence system commands?

I carefully search for:

php
exec($command);
shell_exec($command);
system($command);
passthru($command);

And especially cases like:

php
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:

text
shell
CLI
exec
bash
ImageMagick
FFmpeg
git
zip
unzip

as 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:

php
$response = file_get_contents($url);

I stop and investigate.

Typical examples include:

text
webhooks
image imports
integrations
URL fetchers
PDF generators
proxies
link previews
callbacks

SSRF 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:

http
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:

text
https://app.example.com
https://admin.example.com

instead of allowing everything.

CORS should reflect the actual trust model of the application.


24. Is the application always behind HTTPS?

In production:

text
HTTP → HTTPS

I verify:

bash
curl -I http://example.com
curl -I https://example.com

I 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:

bash
curl -I https://example.com

Depending on the application, I review headers such as:

text
Content-Security-Policy
Strict-Transport-Security
X-Content-Type-Options
Content-Security-Policy: frame-ancestors ...
Referrer-Policy
Permissions-Policy

These 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:

text
password
token
authorization
cookie
secret
credit_card
cpf
cnpj
api_key

Especially in code like:

php
$logger->debug($request);

or:

php
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:

text
user → generic error message
server → detailed internal log

For example:

text
Unable to process the request.

instead of:

text
PDOException:
SQLSTATE[HY000]
/var/www/app/src/Repository/UserRepository.php:143

Production error messages should not expose implementation details that make attacks easier.


28. Are backups protected?

Security does not end with the application.

I review:

text
database backups
uploaded files
configuration backups
permissions
encryption
retention
access
restore procedures

One 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:

bash
ss -lntup

or:

bash
sudo ss -lntup

I want to know whether services such as:

text
MySQL
PostgreSQL
Redis
RabbitMQ
MongoDB
PHP-FPM
Adminer
Portainer

are exposed to the internet without a good reason.

The architecture should preferably keep internal services internal.

For example:

text
Internet
   |
   v
Nginx/Apache
   |
   v
PHP-FPM
   |
   +----> PostgreSQL
   |
   +----> Redis

rather than:

text
Internet
   |
   +----> PostgreSQL
   +----> Redis
   +----> RabbitMQ

30. 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:

text
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 logout

I also use tools such as:

text
curl
Postman
Burp Suite
OWASP ZAP

The 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:

text
[ ] 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 performed

Security 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:

text
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