agentggagentgg
Back to all findings
MEDIUMconfirmedpath-traversalpath-traversalc67d42382036

Local File Read via req.body.layout in dataErasure POST (blocklist bypass)

`req.body.layout` is fed into `path.resolve()` and then into `res.render` template locals, with only a substring blocklist for guard — letting an attacker read arbitrary server files via the Pug layout include.

Fileroutes/dataErasure.ts
Lines103121
Confidence
95%
File statusvalidated
Details

In routes/dataErasure.ts:103-121:

if (req.body.layout) {
  const filePath: string = path.resolve(req.body.layout).toLowerCase()
  const isForbiddenFile: boolean = (filePath.includes('ftp') || filePath.includes('ctf.key') || filePath.includes('encryptionkeys'))
  if (!isForbiddenFile) {
    res.render('dataErasureResult', {
      ...req.body,                 // <-- layout is spread into the locals
      ...themeVars
    }, (error, html) => {
      ...
      const sendlfrResponse: string = html.slice(0, 100) + '......'
      res.send(sendlfrResponse)
      challengeUtils.solveIf(challenges.lfrChallenge, () => { return true })
    })
  }
}

The protection is a .includes() blocklist for ftp, ctf.key, and encryptionkeys. Any other absolute path is accepted. Because ...req.body is spread into the template locals, layout is exposed to the Pug template engine, which treats layout as the layout file to include — Pug then reads the attacker-supplied path from disk and the first 100 bytes are echoed back via res.send(sendlfrResponse). No startsWith(BASE + path.sep) confinement is applied; path.resolve() happily turns ../../../etc/passwd or any absolute path into an unconstrained location.

The authentication check above (security.authenticatedUsers.get(req.cookies.token)) does not restrict the file argument — any logged-in user can read arbitrary readable files (/etc/passwd, app source, JWT signing material that does not happen to contain the literal substring 'encryptionkeys', etc.).

Proof of concept

After logging in (so req.cookies.token resolves to an authenticated user), submit:

curl -X POST 'https://target/dataerasure' \
     -H 'Cookie: token=<valid token>' \
     -H 'Content-Type: application/x-www-form-urlencoded' \
     --data 'email=foo@bar&securityAnswer=x&layout=../../../etc/passwd'

The response body contains the first ~100 bytes of /etc/passwd. Any path not containing ftp, ctf.key, or encryptionkeys works.

Impact

Authenticated local file read of any file the Node.js process can read: application source, configuration, secrets, environment dumps, etc. Authentication via session cookie is required, but every regular user has this capability.

Validation
confirmed

The POST /dataerasure handler resolves req.body.layout with path.resolve() and only blocks paths whose lowercased absolute form contains the substrings ftp, ctf.key, or encryptionkeys; any other absolute or traversal path passes. The full req.body (including layout) is then spread into res.render('dataErasureResult', …), where Pug interprets the layout local as a file to include, reading arbitrary readable files from disk, and html.slice(0,100) is echoed back to the client. The only gate is security.authenticatedUsers.get(req.cookies.token), so any authenticated user can read e.g. /etc/passwd or app source. Per scope rules, the training-app intent is irrelevant — this is a real authenticated LFR.

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
Base score: 4.3 · MEDIUM

The sink is reachable via a normal HTTPS POST to /dataerasure, so AV:N and AC:L (no race or special config — just send layout=../../../etc/passwd). The security.authenticatedUsers.get(req.cookies.token) check at the top of the POST handler rejects unauthenticated callers but accepts any logged-in user, so PR:L with UI:N. The Pug render reads files only as the Node process (no privilege boundary crossed), so S:U; only confidentiality is affected — there is no write or DoS primitive — and the explicit html.slice(0, 100) cap means the attacker controls which file is read but only ever receives the first ~100 bytes of the rendered template, which is partial disclosure rather than full file dump, justifying C:L over C:H.

References