Skip to content

fix: do not strip boundary quotes from unquoted values#1030

Open
spokodev wants to merge 1 commit into
motdotla:masterfrom
spokodev:fix/no-strip-boundary-quotes-on-unquoted
Open

fix: do not strip boundary quotes from unquoted values#1030
spokodev wants to merge 1 commit into
motdotla:masterfrom
spokodev:fix/no-strip-boundary-quotes-on-unquoted

Conversation

@spokodev

Copy link
Copy Markdown

The bug

An unquoted value that happens to start and end with the same quote character has those boundary quotes stripped:

JSON="a":"b"   # parsed as  a":"b   (expected "a":"b")
K="x"y"z"      # parsed as  x"y"z   (expected "x"y"z")
require('dotenv').parse('JSON="a":"b"')
// => { JSON: 'a":"b' }   wrong, lost the boundary quotes

Why this is wrong

The README documents that inner quotes are maintained (line 447, "think JSON"):

inner quotes are maintained (think JSON) (JSON={"foo": "bar"} becomes {JSON:"{\"foo\": \"bar\"}")

"a":"b" is the same shape: a value whose quote characters are content, not a wrapper. It only differs in that its quote characters land on the boundary. The first and last " are part of the value, so they should be kept.

This also matters for shell / dotenvx interop. In a POSIX shell, JSON="a":"b" assigns the literal a:b (each "..." is a separate quoted span the shell concatenates), and dotenvx-style consumers expect the raw token to round-trip. Silently deleting the outer quotes produces a value (a":"b) that matches neither the shell result nor the documented JSON-style behavior.

Root cause

The value "a":"b" cannot match the double-quoted LINE alternative "(?:\\"|[^"])*" because the unescaped inner " blocks a clean wrap, so it falls through to the unquoted alternative [^#\r\n]+ and is captured raw.

The surrounding-quote strip then runs unconditionally:

value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')

That regex fires on any value whose first and last characters are the same quote char, without verifying the value was actually captured as a wrapped quoted token. So an unquoted value gets its boundary quotes deleted.

The fix

Strip the surrounding quotes (and run the double-quote \n/\r expansion) only when the value is a fully wrapped quoted token, matched with the same single/double/backtick alternatives the LINE regex itself allows:

const QUOTED = /^'(?:\\'|[^'])*'$|^"(?:\\"|[^"])*"$|^`(?:\\`|[^`])*`$/
// ...
const quoted = QUOTED.test(value)
const maybeQuote = quoted ? value[0] : ''
if (quoted) {
  value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
}

A genuinely quoted value ("x", 'x', `x`) still matches QUOTED and is stripped exactly as before. An unquoted value whose boundary characters happen to be quotes does not match, so its quotes are preserved.

Red -> green

Added a fixture line RETAIN_BOUNDARY_QUOTES_UNQUOTED="a":"b" to tests/.env and an assertion in tests/test-parse.js:

t.equal(parsed.RETAIN_BOUNDARY_QUOTES_UNQUOTED, '"a":"b"', 'does not strip boundary quotes from an unquoted value')

Fails before the fix (parses to a":"b), passes after.

Full suite green: { total: 200, pass: 200 } (lint + dts-check + all parse / multiline / config / populate tests, including every existing single/double/backtick quote, \n expansion, and the RETAIN_INNER_QUOTES fixtures).

An unquoted value that happens to start and end with the same quote
character (e.g. JSON="a":"b") was matched by the unquoted LINE
alternative, then had its first and last quote stripped by the
surrounding-quote replace, yielding a":"b instead of "a":"b".

The strip fired whenever a value merely began and ended with a quote,
without checking it was actually a wrapped quoted token. Gate the strip
(and the double-quote newline expansion) on the value being a fully
wrapped quoted token, matched with the same single/double/backtick
alternatives the LINE regex allows. README documents that inner quotes
are maintained (think JSON); this makes the parser honor that for values
whose boundary characters are quotes.

@LuxeGold25-coder LuxeGold25-coder left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What is the payload account that is connected

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.

2 participants