In today’s digital age, web applications have become central to the operations of businesses across the globe. At the same time, these applications offer unprecedented convenience and functionality but pose significant security risks.
This blog post delves into some of the most common web applications security vulnerabilities, their potential effects, and strategies for mitigation to help protect sensitive data and maintain user trust.
# Identifying Common Web Application Security Vulnerabilities
1 — SQL Injection
SQL Injection occurs when an attacker exploits a vulnerability to execute malicious SQL commands in a web application database. This can lead to unauthorized access to sensitive information, data loss, and destruction.
Mitigation Strategies
Use prepared statements and parameterized queries
Employ web application firewalls (WAFs)
Regularly update and patch database management systems
SQL Injection remains one of the most severe threats to web applications, primarily because it directly targets the data that powers businesses. The key to defense lies in meticulous validation and preparedness. — Dr. Alex Rivera, Cybersecurity Research
Example: A vulnerable PHP code snippet without parameterized queries:
// Vulnerable PHP code
$userInput = $_GET['user_id'];
$sql = "SELECT * FROM users WHERE user_id = '$userInput'";
Mitigation with Prepared Statement:
// Secure PHP code using prepared statements
$stmt = $conn->prepare("SELECT * FROM users WHERE user_id = ?");
$stmt->bind_param("s", $userInput);
$stmt->execute();
2 — Cross-Site Scripting (XSS)
XSS attacks involve inserting malicious scripts into web pages viewed by other users, which can steal the victims’ cookies, tokens, or other sensitive information.
Mitigation Strategies
Implement Content Security Policy (CSP).
Validate and sanitize all user inputs.
Before displaying it in the user interface, we should encode the data.
Cross-site scripting (XSS) exposes our inherent trust in web content. Protecting against XSS attacks is not just about filtering inputs but understanding how data moves through your application. — Jamie Chen, Lead Security Architect
Example: A vulnerable HTML form element:
<!-- Vulnerable HTML form -->
<form action="/search">
<input type="text" name="query">
<input type="submit">
</form>
Mitigation with Output Encoding:
// Secure output encoding in PHP
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
3 — Broken Authentication
Poorly implemented authentication mechanisms allow attackers to compromise passwords, keys, or session tokens and assume the identity of other users.
Mitigation Strategies
Enforce strong password policies
Use multi-factor authentication (MFA)
Use the recommended techniques for managing sessions
Example: A simplistic login mechanism:
# Vulnerable Python code
def login(username, password):
user = find_user_by_username(username)
if user.password == password:
return True
return False
Mitigation with Secure Password Handling:
# Secure Python code using hashed passwords
import bcrypt
def login(username, password):
user = find_user_by_username(username)
if bcrypt.checkpw(password.encode('utf8'), user.password.encode('utf8')):
return True
return False
4 — Sensitive Data Exposure
Inadequate protection of sensitive data can expose it to unauthorized parties, leading to compliance violations and loss of customer trust.
Mitigation Strategies
Use HTTPS for data in transit
Encrypt sensitive data at rest
Minimize data collection and retention
Vulnerable Code Example: Transmitting sensitive information without encryption in Python:
# Vulnerable Python code for sending sensitive data
import requests
def send_sensitive_data():
data = {'credit_card_number': '1234-5678-9012-3456'}
response = requests.post('http://example.com', data=data)
Mitigation with HTTPS:
# Secure Python code using HTTPS for encrypted transmission
import requests
def send_sensitive_data_securely():
data = {'credit_card_number': '1234-5678-9012-3456'}
response = requests.post('https://example.com', data=data)
5 — Security Misconfiguration
Default configurations, incomplete setups, or verbose error messages can expose web applications to attacks.
Mitigation Strategies
Regularly review and update configurations
Minimize unnecessary features and services
Implement proper error handling
Secure HTTP Headers Example:
Strict-Transport-Security: max-age=63072000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'
X-XSS-Protection: 1; mode=block
6 — Cross-Site Request Forgery (CSRF)
CSRF tricks a user’s browser into executing unintended actions on a web application where they’re authenticated, potentially leading to unauthorized changes.
Mitigation Strategies
Implement anti-CSRF tokens in web forms
Use the SameSite attribute in cookies
Validate referer headers
Vulnerable Code Example: A web form without CSRF protection:
<!-- Vulnerable HTML form without CSRF token -->
<form action="http://example.com/transfer" method="POST">
<input type="hidden" name="amount" value="1000">
<input type="hidden" name="account" value="victim">
<input type="submit" value="Transfer Money">
</form>
Mitigation with CSRF Token:
<!-- Secure HTML form with CSRF token -->
<form action="http://example.com/transfer" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="amount" value="1000">
<input type="hidden" name="account" value="victim">
<input type="submit" value="Transfer Money">
</form>
7 — Insecure Deserialization
Insecure deserialization occurs when untrusted data is used to abuse the logic of an application, leading to remote code execution, replay attacks, injection attacks, and more.
Mitigation Strategies
Avoid serialization of sensitive data
Implement integrity checks or encryption to protect serialized objects
Use serialization mediums that only allow primitive data types
Vulnerable Code Example: Deserializing data without validating its source or content:
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(data));
Object obj = in.readObject();
Mitigation with Input Validation and Custom Serialization:
// Assume 'data' is the byte array to be deserialized
// Validate 'data' before deserialization
if (isValidData(data)) {
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(data));
SafeObject obj = (SafeObject) in.readObject();
// Proceed with 'obj'
}
8 — Using Components with Known Vulnerabilities
Applications often rely on libraries and frameworks that developers may not update or secure against known vulnerabilities, which exposes the application to attacks.
Mitigation Strategies
Regularly update and patch all components
Remove unused dependencies, unnecessary features, components, and files
Use software composition analysis tools to identify and track dependencies
9 — Insufficient Logging & Monitoring
Failure to log and monitor security events adequately can prevent or delay the detection of security breaches, increasing the potential damage.
Mitigation Strategies
Implement comprehensive logging of access, changes, and transactions
Use real-time monitoring and alerting systems to detect suspicious activities
Regularly audit logs and security alerts
Here’s a basic example in Python using the logging module:
import logging
logging.basicConfig(level=logging.INFO, filename='app.log', filemode='a',
format='%(name)s - %(levelname)s - %(message)s')
def sensitive_action():
try:
# Code that performs a sensitive action
logging.info('Sensitive action performed successfully.')
except Exception as e:
logging.error('Error performing sensitive action: {}'.format(e))
10 — API Security
As web applications increasingly rely on APIs, securing them becomes crucial. Inadequate API security can lead to unauthorized access and data breaches.
Mitigation Strategies
Enforce strict authentication and authorization on all API endpoints
Validate and sanitize all input data
Encrypt traffic using SSL/TLS and consider message signing for sensitive data
Vulnerable Scenario: An API endpoint that does not implement rate limiting, allowing an attacker to send an excessive number of requests, potentially leading to service disruption or data leakage.
Mitigation with Pseudocode:
# Simple rate limiting middleware for a web application
from flask import Flask, request, g
import time
app = Flask(__name__)
RATE_LIMIT_WINDOW = 60 # seconds
MAX_REQUESTS_PER_WINDOW = 100
access_records = {}
@app.before_request
def check_rate_limit():
client_ip = request.remote_addr
current_time = time.time()
window_start = current_time - RATE_LIMIT_WINDOW
if client_ip not in access_records:
access_records[client_ip] = []
# Filter out requests outside the current window
access_records[client_ip] = [t for t in access_records[client_ip] if t > window_start]
if len(access_records[client_ip]) >= MAX_REQUESTS_PER_WINDOW:
return "Rate limit exceeded", 429
access_records[client_ip].append(current_time)
@app.route("/api")
def my_api():
return "API response"
if __name__ == "__main__":
app.run()11 — Server-Side Request Forgery (SSRF)
SSRF attacks occur when an attacker abuses a web application to perform requests to an internal system behind the firewall, which can lead to sensitive data leaks or internal system manipulation.
Mitigation Strategies
Validate and sanitize all user inputs, especially URL inputs
Restrict server requests to non-sensitive and whitelisted domains and IP addresses
Implement proper access controls and network segmentation
Vulnerable Code Example: Allowing user input to dictate external URLs accessed by the server:
import requests
def fetch_url(request):
# User-controlled input directly used in a server-side request
url = request.GET.get('url')
response = requests.get(url)
return response.content
Mitigation with URL Validation and Whitelisting:
import requests
ALLOWED_DOMAINS = ['example.com', 'api.example.com']
def fetch_url_secure(request):
url = request.GET.get('url')
domain = urlparse(url).netloc
if domain in ALLOWED_DOMAINS:
response = requests.get(url)
return response.content
else:
return 'Access Denied'
12 — Clickjacking
Clickjacking involves tricking a user into clicking something different from what the user perceives, potentially revealing confidential information, or allowing others to take control of their computer while clicking seemingly innocuous web pages.
Mitigation Strategies
Use the X-Frame-Options HTTP response header to prevent your web pages from being framed by malicious sites
Implement Content Security Policy (CSP) directives to restrict where resources can be loaded
Educate users about the risks of clickjacking and safe browsing practices
13 — Web Cache Poisoning
Web cache poisoning attacks exploit the caching mechanism to distribute malicious content to unsuspecting users. It manipulates the web cache to serve poisoned content to other users, which can lead to account takeover, financial theft, and other malicious outcomes.
Mitigation Strategies
Validate and sanitize cache keys thoroughly
Limit the cache ability of sensitive responses
Ensure that web applications correctly differentiate between multiple users and contexts
14 — File Upload Vulnerabilities
When a web application allows users to upload files without proper validation, it opens the door to uploading malicious files that can lead to server compromise or data breaches.
Mitigation Strategies
Restrict file types to only those that are necessary
Scan uploaded files for malware and threats
Implement strong access controls and store uploaded files in a separate domain or subdomain
Vulnerable Code Example: Allowing file uploads without validation in PHP:
// Vulnerable PHP code for handling file uploads
if (isset($_FILES['uploaded_file']['name'])) {
move_uploaded_file($_FILES['uploaded_file']['tmp_name'], 'uploads/' . $_FILES['uploaded_file']['name']);
}
Mitigation with File Type Validation and Sanitization:
// Secure PHP code for handling file uploads with validation
if (isset($_FILES['uploaded_file']['name'])) {
$allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
$fileInfo = finfo_open(FILEINFO_MIME_TYPE);
$detectedType = finfo_file($fileInfo, $_FILES['uploaded_file']['tmp_name']);
if (in_array($detectedType, $allowedTypes)) {
$safeName = preg_replace('/[^a-zA-Z0-9-_\.]/', '', $_FILES['uploaded_file']['name']);
move_uploaded_file($_FILES['uploaded_file']['tmp_name'], 'uploads/' . $safeName);
} else {
echo "Invalid file type.";
}
}
# Implementing Proactive Security Measures
1 — Zero Trust Architecture
Zero Trust is a security concept centered on the belief that organizations should not automatically trust anything inside or outside its perimeters and instead must verify anything and everything trying to connect to its systems before granting access.
Best Practices:
Implement strict access controls and enforce least privilege principles
Use multi-factor authentication and continuous verification for all access requests
Segment networks to limit lateral movement and contain breaches
Adopting Zero Trust Architecture is a fundamental shift towards acknowledging trust as a vulnerability. In the current digital landscape, a verification must precede trust. — Marcus Yi
2 — Secure Coding Practices
Secure coding practices are essential to prevent vulnerabilities at the source code level. They involve guidelines and best practices for writing code resistant to vulnerabilities.
Best Practices:
Follow secure coding guidelines, such as those provided by OWASP or CERT
Regularly perform code reviews and static analysis to identify and fix security flaws
Educate developers on secure coding practices through training and awareness programs
3 — Incident Response Planning
An Incident Response Plan (IRP) is a documented, strategic framework that outlines the process to be followed during a security breach or cyberattack.
Best Practices:
Develop and regularly update an incident response plan
Conduct regular security incident response drills
Establish a dedicated incident response team
4 — Secure Development Lifecycle (SDL)
A secure Development Lifecycle is a process that incorporates security considerations and practices into each phase of the software development process, from requirements analysis to design, implementation, testing, and deployment.
Best Practices:
Integrate security practices at every stage of the software development lifecycle
Conduct threat modeling to identify potential security issues early in the development process
Ensure regular security training for developers and other stakeholders involved in the development process
Integrating security from the get-go through a Secure Development Lifecycle isn’t just efficient; it’s essential. It turns security from a checklist item into a fundamental component of development. — Elena Rodriguez, VP of Engineering
# Beyond the Basics
1 — Ethical Hacking and Penetration Testing
Ethical hacking and penetration testing involve simulating cyberattacks under controlled conditions to identify vulnerabilities in web applications. This proactive security measure allows organizations to strengthen their defenses by addressing weaknesses before malicious actors can exploit them.
Best Strategies:
Engage certified ethical hackers to perform comprehensive penetration tests on your web applications regularly
Prioritize and remediate identified vulnerabilities based on their severity and potential impact
Incorporate findings into development and security practices to prevent future vulnerabilities
Ethical hacking is like regularly stress-testing your fortifications. It’s not about expecting failure but ensuring success against inevitable attempts. — Vikram Singh, Ethical Hacker and Penetration Tester
This example demonstrates a simple Python script that uses the OWASP ZAP (Zed Attack Proxy) API to automate vulnerability scanning of a web application. OWASP ZAP is a popular tool for finding vulnerabilities in web applications.
from zapv2 import ZAPv2
target = 'http://example.com' # Target web application
zap = ZAPv2()
# Start the ZAP scanner
print("Starting ZAP scan...")
zap.urlopen(target)
scanid = zap.spider.scan(target)
# Monitor the scan's progress
while (int(zap.spider.status(scanid)) < 100):
print("Spider progress %: " + zap.spider.status(scanid))
time.sleep(2)
print("Spider scan completed.")
# Perform the active scan
ascan_id = zap.ascan.scan(target)
while (int(zap.ascan.status(ascan_id)) < 100):
print("Active Scan progress %: " + zap.ascan.status(ascan_id))
time.sleep(5)
print("Active Scan completed.")
# Print vulnerabilities found by the scan
print("Vulnerabilities found:")
print(zap.core.alerts(baseurl=target))
2 — Cloud Security Posture Management (CSPM)
CSPM is a security approach that continuously monitors cloud environments for misconfigurations and compliance risks, automatically remediating issues to maintain a strong security posture. As cloud adoption increases, CSPM is essential for identifying and addressing security risks in cloud configurations.
Best Strategies:
Implement CSPM tools that offer real-time monitoring and automatic remediation capabilities
Conduct regular security assessments of your cloud environments to ensure compliance with security policies and standards
Train your team on best practices for cloud security and the importance of maintaining secure cloud configurations
This example is a conceptual Python script that checks for unsecured S3 buckets in an AWS environment. This script requires the boto3 library, which is the Amazon Web Services (AWS) SDK for Python.
import boto3
def check_s3_bucket_security():
s3 = boto3.client('s3')
buckets = s3.list_buckets()
for bucket in buckets['Buckets']:
bucket_name = bucket['Name']
bucket_acl = s3.get_bucket_acl(Bucket=bucket_name)
for grant in bucket_acl['Grants']:
if grant['Grantee']['Type'] == 'Group' and grant['Grantee']['URI'] == 'http://acs.amazonaws.com/groups/global/AllUsers':
print(f"Bucket {bucket_name} is publicly accessible!")
if __name__ == "__main__":
check_s3_bucket_security()3 — International Standards and Frameworks
3 — International Standards and Frameworks
Adherence to international standards and frameworks like ISO/IEC 27001, NIST Cybersecurity Framework, and the GDPR ensures organizations follow recognized best practices for information security management and data protection. Compliance enhances organizational reputation and trustworthiness.
Best Strategies:
Perform regular audits to assess compliance with relevant standards and frameworks and identify areas for improvement
Develop and implement security policies and procedures that align with these standards
Provide ongoing training to employees on the importance of compliance and secure practices
4 — Digital Identity and Authentication Technologies
The evolution of digital identity and authentication technologies, such as biometric verification and decentralized identity systems, transforms how we secure user access and protect against unauthorized access. These technologies offer enhanced security and user experience by leveraging unique identifiers and advanced verification methods.
Best Strategies:
Evaluate and adopt advanced authentication technologies that meet your organization’s security and usability requirements
Stay informed about emerging trends and standards in digital identity to improve your authentication systems continuously
Implement privacy-by-design principles to protect user data in line with regulatory requirements
As digital identities become more complex, the technologies securing them must evolve. Biometrics and decentralized systems aren’t just advancements; they’re the future of personal security online. — Sarah Kim, Digital Identity Specialist.
This Python Flask example demonstrates creating and validating JSON Web Tokens (JWT) for authentication. It uses the flask
and pyjwt
libraries for creating a simple web application with JWT-based authentication.
from flask import Flask, request, jsonify
import jwt
import datetime
app = Flask(__name__)
SECRET_KEY = 'your_secret_key'
@app.route('/login', methods=['POST'])
def login():
auth_data = request.json
if auth_data['username'] == 'admin' and auth_data['password'] == 'password':
token = jwt.encode({
'user': auth_data['username'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
}, SECRET_KEY)
return jsonify({'token': token})
else:
return jsonify({'message': 'Invalid credentials'}), 403
@app.route('/protected', methods=['GET'])
def protected():
token = request.headers.get('Authorization')
if not token:
return jsonify({'message': 'Token is missing'}), 403
try:
data = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return jsonify({'data': data}), 200
except:
return jsonify({'message': 'Token is invalid'}), 403
if __name__ == "__main__":
app.run(debug=True)
# The Bigger Picture
1 — Sustainability in Cybersecurity
The intersection of cybersecurity and sustainability focuses on reducing the environmental impact of digital operations. Sustainable cybersecurity practices aim to minimize energy consumption, reduce e-waste, and promote the responsible use of resources while maintaining robust security measures.
Best Approaches:
Optimize the energy efficiency of data centers and IT infrastructure
Adopt a lifecycle approach to IT hardware, focusing on reuse and recycling to minimize e-waste
Incorporate environmental considerations into purchasing decisions and cybersecurity practices
2 — Security Awareness and Training
Security awareness and training are crucial in empowering developers, administrators, and users with the knowledge to effectively identify and mitigate security threats.
Through targeted education programs, individuals learn to navigate the landscape of cyber threats, from phishing scams to software vulnerabilities, ensuring proactive defense mechanisms are ingrained in the organizational culture.
Best Approaches:
Develop comprehensive training modules covering key security topics
Regularly update training content to reflect the latest threat landscape
Conduct mock drills and simulations to reinforce learning and preparedness
For security awareness, let’s focus on a practical example of a security checklist or quiz app that can be developed to test knowledge on common security threats:
// Simple security quiz example in JavaScript
const securityQuestions = [
{
question: "What is phishing?",
options: ["A type of fish", "A security threat involving deceptive emails", "A coding language"],
answer: 1,
},
{
question: "True or False: You should use the same password for all your accounts.",
options: ["True", "False"],
answer: 1,
}
];
let score = 0;
securityQuestions.forEach((item, index) => {
console.log(`${index + 1}: ${item.question}`);
item.options.forEach((option, i) => {
console.log(` ${i}: ${option}`);
});
// In a real app, you'd capture user input. Here, we'll simulate correct answers.
let userAnswer = item.answer;
if (userAnswer === item.answer) {
console.log("Correct!");
score++;
} else {
console.log("Wrong!");
}
});
console.log(`Your score: ${score}/${securityQuestions.length}`);
3 — DevSecOps Integration
Integrating security practices into the development and deployment pipelines, DevSecOps ensures that security is integral to the application lifecycle.
This approach minimizes vulnerabilities by embedding automated security checks, code analysis, and compliance verification into every development phase, from initial design to deployment and maintenance.
Mitigation Strategies
Automate security scanning and compliance checks within CI/CD workflows
Foster collaboration between development, operations, and security teams
Implement security incident feedback loops to refine processes continuously
# .github/workflows/security.yml
name: Security Check
on: [push]
jobs:
bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install Bandit
run: pip install bandit
- name: Run Bandit security check
run: bandit -r .
Adhering to data protection and privacy laws, such as GDPR and CCPA, is essential for avoiding legal penalties and building user trust.
Compliance is committed to safeguarding personal data, requiring organizations to implement robust data management and security practices that align with regulatory standards.
Best Approaches:
Conduct periodic audits to ensure compliance with data protection laws
Implement and maintain data encryption, anonymization, and access controls
Train staff on compliance requirements and data handling procedures
For GDPR compliance, ensuring data is encrypted can be crucial. Here’s a simple example of encrypting a string in Python using the Fernet symmetric encryption method from the cryptography library:
from cryptography.fernet import Fernet
# Generate a key and instantiate a Fernet instance
key = Fernet.generate_key()
cipher_suite = Fernet(key)
# Encrypt data
data = "Sensitive data".encode()
encrypted_data = cipher_suite.encrypt(data)
print(f"Encrypted: {encrypted_data}")
# Decrypt data
decrypted_data = cipher_suite.decrypt(encrypted_data)
print(f"Decrypted: {decrypted_data.decode()}")
5 — Threat Modeling
Threat modeling is a systematic process of identifying and assessing potential security threats to a web application.
By considering realistic attack scenarios and examining the application’s architecture, teams can expect web application security vulnerabilities and implement defenses before threats materialize, making it a proactive component of a secure application design.
Best Approaches:
Regularly conduct threat modeling sessions at different stages of development
Use frameworks like STRIDE for structured threat analysis
Integrate findings into the development process to mitigate identified risks
Threat modeling often involves analysis rather than coding, but you can implement security headers in your web applications as part of mitigation strategies. Here’s an example with Express.js:
const express = require('express');
const helmet = require('helmet');
const app = express();
// Use Helmet to add secure headers
app.use(helmet());
app.get('/', (req, res) => res.send('Hello World with secure headers!'));
app.listen(3000, () => console.log('Server running on http://localhost:3000'));
6 — Community and Open Source Contributions
Engaging with security communities and contributing to open-source projects is vital for staying updated of the latest cybersecurity trends, tools, and practices.
Participation fosters a culture of learning and sharing, offering access to collective knowledge and collaborative problem-solving resources that enhance web application security and aware about web application security vulnerabilities.
Best Approaches:
Encourage team members to engage with online security forums and communities
Contribute to or leverage open-source security tools and libraries
Participate in security conferences and workshops for continuous learning
The strength of cybersecurity lies in its community. Open-source contributions don’t just improve your security posture; they elevate the global baseline of security for everyone. — Carlos Alvarez, Open Source Security Advocate.
Participating in open-source projects typically involves contributing code, documentation, or other resources to projects. Here’s an example of a simple contribution guide snippet you might find in a project’s README.md:
## Contributing to Our Project
We welcome contributions from the community! Here are a few ways you can help:
- **Reporting bugs:** Open an issue to report a bug. Please include detailed information about how to reproduce the issue.
- **Feature suggestions:** Have an idea for a new feature? Open an issue to discuss it with us.
- **Code contributions:** Want to fix a bug or implement a new feature? Submit a pull request with your changes. Please follow our coding standards and include tests for your changes.
Thank you for supporting our project!
Reading List: Mitigate web application security vulnerabilities
Conclusion
Securing web applications requires vigilance, regular updates, and a proactive approach to identifying and mitigating web application security vulnerabilities.
Developers and businesses can significantly reduce risk profiles and protect their users’ data by understanding these standard security holes and implementing the recommended strategies.
Hope this guide will help you to understand and mitigate common web application security vulnerabilities.
🔐 As you close your terminal:
👏 Enjoyed this decryption of web security? Hit that 'Like' button to show your support!
💬 Have insights on firewalling flaws or securing scripts? Add your voice in the comments!
🔄 Got a crew of coders who need to combat code vulnerabilities? Share this article with them!
Your interaction is the keystroke that helps fortify our community's code.
🛡️ Big thanks for engaging with the content! Your likes, comments, and shares don't just circulate information—they reinforce our communal cyber fortifications.
Support Our Tech Insights
Note: Some links on this page might be affiliate links. If you purchase through these links, I may earn a small commission at no extra cost to you. Thanks for your support!
Top comments (0)