Skip to content

fix: prevent path traversal in functions:create command [security]#8207

Open
NiteshCodes7 wants to merge 2 commits intonetlify:mainfrom
NiteshCodes7:fix/path-traversal-functions-create
Open

fix: prevent path traversal in functions:create command [security]#8207
NiteshCodes7 wants to merge 2 commits intonetlify:mainfrom
NiteshCodes7:fix/path-traversal-functions-create

Conversation

@NiteshCodes7
Copy link
Copy Markdown

Description

There is a path traversal vulnerability in functions:create. The interactive
prompt correctly validates function names with /^[\w.-]+$/i, but this validation
is completely bypassed when the name is supplied via the --name flag or as a
positional argument.

Affected File

src/commands/functions/functions-create.ts

Reproduction Steps

Run either of the following:

netlify functions:create --name "../../evil"
netlify functions:create "../../evil"

This causes files to be written outside the intended functions directory.

Root Cause

In getNameFromArgs, validation only runs on the interactive prompt:

const { name } = await inquirer.prompt([
{
validate: (val) => Boolean(val) && /^[\w.-]+$/i.test(val), // ✅ validated
},
])

But when --name or a positional arg is passed, the raw unvalidated value goes
directly into:

const functionPath = path.join(functionsDir, name) // ❌ no validation

Impact

An attacker or a malicious script can write files to arbitrary locations on the
filesystem outside the functions directory.

Proposed Fix

Add a containment check in ensureFunctionPathIsOk:

const ensureFunctionPathIsOk = function (functionsDir, name) {
const functionPath = path.join(functionsDir, name)

if (!functionPath.startsWith(path.resolve(functionsDir) + path.sep)) {
log(Invalid function name: "${name}" resolves outside the functions directory.)
process.exit(1)
}

if (fs.existsSync(functionPath)) {
log(Function ${functionPath} already exists, cancelling...)
process.exit(1)
}

return functionPath
}

Additional Note

A second related issue exists in downloadFromURL where filenames from the
GitHub API response are not sanitized before being passed to path.join.
Fix: wrap name with path.basename(name) before use.

@NiteshCodes7 NiteshCodes7 requested a review from a team as a code owner April 24, 2026 17:18
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

The patch updates function creation and download logic in a TypeScript command file to prevent path traversal and unsafe paths. For URL-based downloads the fallback filename is sanitized with path.basename() so directory components are not preserved. For template-based scaffolding the code now resolves the base functions directory, computes the target path, and rejects function names whose resolved path would escape the functions directory before running the existing “already exists” check.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description is comprehensive and directly related to the changeset, clearly explaining the vulnerability, reproduction steps, root cause, impact, and proposed fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main security fix: preventing path traversal vulnerabilities in the functions:create command by validating and sanitizing function names.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/functions/functions-create.ts (1)

374-400: ⚠️ Potential issue | 🔴 Critical

Path traversal still reachable via --url + --name/positional argument.

nameToUse returned by getNameFromArgs is unvalidated when it comes from a flag or positional arg (validation in getNameFromArgs runs only for the interactive prompt). Since downloadFromURL is not routed through ensureFunctionPathIsOk, the following still escapes the functions directory:

  • netlify functions:create --url <gh-url> --name "../../evil"fnFolder = path.join(functionsDir, "../../evil") on Line 379, and ${nameToUse}.js on Line 399 expands to ../../evil.js.
  • netlify functions:create --url <gh-url> "../../evil" → same.

The GitHub-filename sanitization alone does not close the original vulnerability described in the PR; the name itself is the primary attacker-controlled input. Recommend enforcing containment at the root cause (either in getNameFromArgs or at the start of downloadFromURL) so both scaffold and URL-download paths are covered.

🔒 Proposed fix — validate the name at the entry of downloadFromURL
 const downloadFromURL = async function (command, options, argumentName, functionsDir) {
   const folderContents = await readRepoURL(options.url)
   const [functionName] = options.url.split('/').slice(-1)
   const nameToUse = await getNameFromArgs(argumentName, options, functionName)

-  const fnFolder = path.join(functionsDir, nameToUse)
+  const fnFolder = ensureFunctionPathIsOk(functionsDir, nameToUse)

Alternatively, centralize validation in getNameFromArgs so any caller is safe by construction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/functions/functions-create.ts` around lines 374 - 400, The
downloadFromURL function accepts an unvalidated nameToUse from getNameFromArgs
which allows path traversal; fix by validating/sanitizing nameToUse at the start
of downloadFromURL (or update getNameFromArgs) so it cannot contain path
separators or traversal segments (e.g., strip/deny "../", "/", Windows
backslashes, and empty names) and/or resolve fnFolder and assert it is contained
inside functionsDir (using path.resolve and comparing prefixes) before creating
directories or writing files; use the validated/sanitized nameToUse for fnFolder
and final file names (references: downloadFromURL, getNameFromArgs,
functionsDir, fnFolder, nameToUse, ensureFunctionPathIsOk).
🧹 Nitpick comments (3)
src/commands/functions/functions-create.ts (3)

397-400: Remove explanatory comments (guideline) and simplify the ternary.

// SAFE: and // ← strip any directory components describe what the code does and should be removed in favor of self-explanatory code. Note also that both branches now call path.basename(name, ...), so the ternary can be simplified. Combined with the fix proposed above (validating nameToUse via ensureFunctionPathIsOk before we reach this point), this becomes:

✂️ Proposed diff
-        // SAFE:
-        const finalName = path.basename(name, '.js') === functionName
-          ? `${nameToUse}.js`
-          : path.basename(name)  // ← strip any directory components
+        const safeBaseName = path.basename(name)
+        const finalName =
+          path.basename(safeBaseName, '.js') === functionName ? `${nameToUse}.js` : safeBaseName

As per coding guidelines: "Never write comments on what the code does; make the code clean and self-explanatory instead".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/functions/functions-create.ts` around lines 397 - 400, Remove
the explanatory comments and simplify the ternary that computes finalName:
validate nameToUse earlier via ensureFunctionPathIsOk, then replace the current
ternary using redundant path.basename calls with a single expression that always
uses path.basename(name, '.js') when appropriate and falls back to
path.basename(name) only once; update the code referencing finalName,
functionName, name, nameToUse and path.basename to use the simplified expression
and delete the comments "// SAFE:" and "// ← strip any directory components".

751-751: Drop the inline comment per coding guidelines.

-    // Prevent path traversal: reject names like "../../evil"
     const resolvedFunctionsDir = path.resolve(functionsDir)

As per coding guidelines: "Never write comments on what the code does; make the code clean and self-explanatory instead". The function name ensureFunctionPathIsOk plus a clear error message already convey intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/functions/functions-create.ts` at line 751, Remove the inline
comment "// Prevent path traversal: reject names like "../../evil"" near the
ensureFunctionPathIsOk function; the function name and its error messages are
self-explanatory per guidelines, so delete that comment (and any other comments
that merely restate what ensureFunctionPathIsOk does) to comply with the coding
standard.

59-83: Root-cause opportunity: centralize name validation in getNameFromArgs.

Today, validation only fires inside the inquirer validate callback (Line 77), so values from options.name (Line 64) and argumentName (Line 68) bypass it entirely — this is the exact bypass called out in the PR description. Adding the regex check once here makes every caller (including downloadFromURL) safe by construction and removes the need to sprinkle containment checks across call sites.

🛡️ Proposed fix
 const getNameFromArgs = async function (argumentName, options, defaultName) {
+  const isValid = (val: unknown): val is string => typeof val === 'string' && /^[\w.-]+$/i.test(val)
+
   if (options.name) {
     if (argumentName) {
       throw new Error('function name specified in both flag and arg format, pick one')
     }
+    if (!isValid(options.name)) {
+      return logAndThrowError(`Invalid function name: "${options.name as string}"`)
+    }
     return options.name
   }

   if (argumentName) {
+    if (!isValid(argumentName)) {
+      return logAndThrowError(`Invalid function name: "${argumentName as string}"`)
+    }
     return argumentName
   }

   const { name } = await inquirer.prompt([
     {
       name: 'name',
       message: 'Name your function:',
       default: defaultName,
       type: 'input',
-      validate: (val) => Boolean(val) && /^[\w.-]+$/i.test(val),
+      validate: (val) => Boolean(val) && isValid(val),
     },
   ])
   return name
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/functions/functions-create.ts` around lines 59 - 83, Centralize
name validation in getNameFromArgs: after deciding the candidate name (from
options.name or argumentName) and before returning it, validate it against the
same regex used in the inquirer validate callback (/^[\w.-]+$/i) and throw a
clear Error if it fails; also ensure when options.name and argumentName are both
provided you still throw the existing duplicate-name Error, and keep the
inquirer validate as-is for interactive input. This ensures any name returned by
getNameFromArgs (including options.name and argumentName paths and callers like
downloadFromURL) is validated consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/functions/functions-create.ts`:
- Around line 750-763: Move the ensureFunctionPathIsOk declaration to column 0
to match other top-level functions; replace the startsWith containment check
with a robust containment test using path.relative(resolvedFunctionsDir,
functionPath) and reject when the relative path starts with '..' or is absolute
(or when relative === '' and name resolves to the same dir in an unexpected
way), and replace both process.exit(1) calls with calls to logAndThrowError
(preserving existing log messages via log and including
NETLIFYDEVERR/NETLIFYDEVLOG constants) so errors are thrown instead of exiting;
keep fs.existsSync to detect existing path and return functionPath on success.

---

Outside diff comments:
In `@src/commands/functions/functions-create.ts`:
- Around line 374-400: The downloadFromURL function accepts an unvalidated
nameToUse from getNameFromArgs which allows path traversal; fix by
validating/sanitizing nameToUse at the start of downloadFromURL (or update
getNameFromArgs) so it cannot contain path separators or traversal segments
(e.g., strip/deny "../", "/", Windows backslashes, and empty names) and/or
resolve fnFolder and assert it is contained inside functionsDir (using
path.resolve and comparing prefixes) before creating directories or writing
files; use the validated/sanitized nameToUse for fnFolder and final file names
(references: downloadFromURL, getNameFromArgs, functionsDir, fnFolder,
nameToUse, ensureFunctionPathIsOk).

---

Nitpick comments:
In `@src/commands/functions/functions-create.ts`:
- Around line 397-400: Remove the explanatory comments and simplify the ternary
that computes finalName: validate nameToUse earlier via ensureFunctionPathIsOk,
then replace the current ternary using redundant path.basename calls with a
single expression that always uses path.basename(name, '.js') when appropriate
and falls back to path.basename(name) only once; update the code referencing
finalName, functionName, name, nameToUse and path.basename to use the simplified
expression and delete the comments "// SAFE:" and "// ← strip any directory
components".
- Line 751: Remove the inline comment "// Prevent path traversal: reject names
like "../../evil"" near the ensureFunctionPathIsOk function; the function name
and its error messages are self-explanatory per guidelines, so delete that
comment (and any other comments that merely restate what ensureFunctionPathIsOk
does) to comply with the coding standard.
- Around line 59-83: Centralize name validation in getNameFromArgs: after
deciding the candidate name (from options.name or argumentName) and before
returning it, validate it against the same regex used in the inquirer validate
callback (/^[\w.-]+$/i) and throw a clear Error if it fails; also ensure when
options.name and argumentName are both provided you still throw the existing
duplicate-name Error, and keep the inquirer validate as-is for interactive
input. This ensures any name returned by getNameFromArgs (including options.name
and argumentName paths and callers like downloadFromURL) is validated
consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 09ee38e3-23c4-4259-8cee-bd033bb734b1

📥 Commits

Reviewing files that changed from the base of the PR and between 0415e87 and 094e830.

📒 Files selected for processing (1)
  • src/commands/functions/functions-create.ts

Comment thread src/commands/functions/functions-create.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/commands/functions/functions-create.ts (1)

749-762: ⚠️ Potential issue | 🟠 Major

Previously‑raised issues in ensureFunctionPathIsOk are still unaddressed.

Re-flagging so this doesn't ship as-is:

  1. Indentation / formatting. The declaration is indented two spaces while every other top-level function (ensureEdgeFuncDirExists, downloadFromURL, scaffoldFromTemplate, …) starts at column 0. Prettier will reject this.
  2. Fragile containment check. functionPath.startsWith(resolvedFunctionsDir + path.sep) is case-sensitive, so on case-insensitive filesystems (macOS default, Windows) a name with differing casing can bypass or false-trip the guard. path.relative is the idiomatic containment test.
  3. Inconsistent error handling. The rest of this file routes failures through logAndThrowError; using process.exit(1) here makes the command harder to test and bypasses any upstream cleanup.
  4. Explanatory comment. Line 750 describes what the code does. Per the coding guideline, the code should be self-explanatory instead.
♻️ Proposed refactor
-  const ensureFunctionPathIsOk = function (functionsDir, name) {
-    // Prevent path traversal: reject names like "../../evil"
-    const resolvedFunctionsDir = path.resolve(functionsDir)
-    const functionPath = path.join(resolvedFunctionsDir, name)
-    if (!functionPath.startsWith(resolvedFunctionsDir + path.sep)) {
-      log(`${NETLIFYDEVERR} Invalid function name: "${name}" resolves outside the functions directory.`)
-      process.exit(1)
-    }
-    if (fs.existsSync(functionPath)) {
-      log(`${NETLIFYDEVLOG} Function ${functionPath} already exists, cancelling...`)
-      process.exit(1)
-    }
-    return functionPath
-  }
+const ensureFunctionPathIsOk = function (functionsDir, name) {
+  const resolvedFunctionsDir = path.resolve(functionsDir)
+  const functionPath = path.resolve(resolvedFunctionsDir, name)
+  const rel = path.relative(resolvedFunctionsDir, functionPath)
+  if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
+    return logAndThrowError(
+      `${NETLIFYDEVERR} Invalid function name: "${name}" resolves outside the functions directory.`,
+    )
+  }
+  if (fs.existsSync(functionPath)) {
+    log(`${NETLIFYDEVLOG} Function ${functionPath} already exists, cancelling...`)
+    process.exit(1)
+  }
+  return functionPath
+}

As per coding guidelines: "Never write comments on what the code does; make the code clean and self-explanatory instead".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/functions/functions-create.ts` around lines 749 - 762, The
ensureFunctionPathIsOk function should be unindented to top-level (match other
functions like ensureEdgeFuncDirExists), remove the explanatory comment, use
path.relative to check containment (e.g. compute
path.relative(resolvedFunctionsDir, functionPath) and ensure it doesn't start
with '..' or equal '..') to avoid case/fs issues, and replace process.exit(1)
calls with the module's standard error flow by invoking logAndThrowError (or the
existing logAndThrowError helper) with a clear message when the name resolves
outside the functions dir or when the path already exists so callers can handle
errors and tests can clean up.
🧹 Nitpick comments (1)
src/commands/functions/functions-create.ts (1)

397-399: Drop the trailing explanatory comment.

The // ← strip any directory components comment describes what the code does. Rename the variable or rely on path.basename being self-evident instead.

♻️ Proposed refactor
-        const finalName = path.basename(name, '.js') === functionName
-          ? `${nameToUse}.js`
-          : path.basename(name)  // ← strip any directory components
+        const sanitizedName = path.basename(name)
+        const finalName = path.basename(sanitizedName, '.js') === functionName ? `${nameToUse}.js` : sanitizedName

As per coding guidelines: "Never write comments on what the code does; make the code clean and self-explanatory instead".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/functions/functions-create.ts` around lines 397 - 399, Remove
the trailing explanatory comment after the path.basename call and make the code
self-descriptive: drop the "// ← strip any directory components" comment and, if
helpful for clarity, consider renaming variables (e.g., finalName, nameToUse,
functionName) or adding a small, clear helper function name so the intent of
path.basename(name) is obvious without commentary.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/commands/functions/functions-create.ts`:
- Around line 749-762: The ensureFunctionPathIsOk function should be unindented
to top-level (match other functions like ensureEdgeFuncDirExists), remove the
explanatory comment, use path.relative to check containment (e.g. compute
path.relative(resolvedFunctionsDir, functionPath) and ensure it doesn't start
with '..' or equal '..') to avoid case/fs issues, and replace process.exit(1)
calls with the module's standard error flow by invoking logAndThrowError (or the
existing logAndThrowError helper) with a clear message when the name resolves
outside the functions dir or when the path already exists so callers can handle
errors and tests can clean up.

---

Nitpick comments:
In `@src/commands/functions/functions-create.ts`:
- Around line 397-399: Remove the trailing explanatory comment after the
path.basename call and make the code self-descriptive: drop the "// ← strip any
directory components" comment and, if helpful for clarity, consider renaming
variables (e.g., finalName, nameToUse, functionName) or adding a small, clear
helper function name so the intent of path.basename(name) is obvious without
commentary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3b5b06e5-e22f-4698-8505-401817bc28d7

📥 Commits

Reviewing files that changed from the base of the PR and between 094e830 and 864e2da.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • src/commands/functions/functions-create.ts

@NiteshCodes7 NiteshCodes7 changed the title fix: prevent path traversal in functions:create command fix: prevent path traversal in functions:create command [security] Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant