Description
Command.getJsonOutput() post-processes JSON.stringify output with a regex intended to escape newlines (comment references #2807):
getJsonOutput(logStatement: any): string {
return JSON
.stringify(logStatement, null, 2)
// replace unescaped newlines with escaped newlines #2807
.replace(/([^\\])\\n/g, '$1\\\\\\n');
}
JSON.stringify already emits valid JSON (all control characters in string values are escaped), so this regex never has a legitimate match to fix — but its single-character lookbehind cannot see escape context, so it corrupts data: any \n escape inside a string value that is preceded by a non-backslash character gets rewritten, which injects a literal backslash into the parsed value. Since every newline's \n escape is preceded by a normal character (\r\n → the n-escape follows r; bare \n follows the previous data character), this fires on every newline inside every string value of every command's JSON output.
Minimal repro (pure JS, current main logic)
const mangled = JSON.stringify({ b: "line1\r\nline2" })
.replace(/([^\\])\\n/g, '$1\\\\\\n');
// mangled === '{"b":"line1\\r\\\\\\nline2"}'
JSON.parse(mangled).b;
// → "line1\r\\\nline2" — a literal backslash injected between CR and LF
Bare-LF values are corrupted the same way ("a\nb" → "a\<LF>b" after parse).
Real-world impact
Microsoft Graph message bodies (Exchange HTML) are full of CRLF newlines. Reading a message through the CLI returns corrupted content:
m365 request --method post --url "https://graph.microsoft.com/v1.0/me/messages/{id}/createReplyAll"
# → body.content contains a literal "\" before every newline
The same draft fetched with curl and a bearer token comes back clean — on a real reply draft we measured 77 injected backslashes via the CLI vs 0 via curl for byte-identical Graph content. --output json and the default output are equally affected (both route through getJsonOutput).
This is not cosmetic: in a compose flow (createReplyAll → read draft body → PATCH body with quoted history → send), the injected backslashes are sent to recipients — at least one survives Outlook's HTML sanitization as visible \ text in the delivered email.
Steps to reproduce
m365 request --url "https://graph.microsoft.com/v1.0/me/messages/{any-id}?$select=body" for any message whose HTML contains CRLF (virtually all Exchange-generated HTML).
- Compare
body.content with the same GET via curl/Graph Explorer.
Or run the pure-JS snippet above.
Expected results
JSON output is JSON.stringify's output: parseable, and parsing returns the original data unchanged.
Actual results
Parsing the CLI's JSON output returns string values with a literal \ injected before every newline.
Suggested fix
Remove the .replace(...). The #2807 problem (invalid JSON from _ObjectIdentity_ containing raw control characters) cannot be reproduced through JSON.stringify, which escapes control characters by construction — if some code path emits raw control characters today, the fix belongs there, not in a post-stringify rewrite that corrupts valid output.
Diagnostics
- CLI for Microsoft 365 version: 11.8.0 (latest at time of filing; logic present in current
main)
- Node.js: v22 (Homebrew), macOS (Darwin 25.5)
- Shell: zsh
Description
Command.getJsonOutput()post-processesJSON.stringifyoutput with a regex intended to escape newlines (comment references #2807):JSON.stringifyalready emits valid JSON (all control characters in string values are escaped), so this regex never has a legitimate match to fix — but its single-character lookbehind cannot see escape context, so it corrupts data: any\nescape inside a string value that is preceded by a non-backslash character gets rewritten, which injects a literal backslash into the parsed value. Since every newline's\nescape is preceded by a normal character (\r\n→ then-escape followsr; bare\nfollows the previous data character), this fires on every newline inside every string value of every command's JSON output.Minimal repro (pure JS, current
mainlogic)Bare-LF values are corrupted the same way (
"a\nb"→"a\<LF>b"after parse).Real-world impact
Microsoft Graph message bodies (Exchange HTML) are full of CRLF newlines. Reading a message through the CLI returns corrupted content:
The same draft fetched with
curland a bearer token comes back clean — on a real reply draft we measured 77 injected backslashes via the CLI vs 0 via curl for byte-identical Graph content.--output jsonand the default output are equally affected (both route throughgetJsonOutput).This is not cosmetic: in a compose flow (
createReplyAll→ read draft body →PATCHbody with quoted history →send), the injected backslashes are sent to recipients — at least one survives Outlook's HTML sanitization as visible\text in the delivered email.Steps to reproduce
m365 request --url "https://graph.microsoft.com/v1.0/me/messages/{any-id}?$select=body"for any message whose HTML contains CRLF (virtually all Exchange-generated HTML).body.contentwith the same GET via curl/Graph Explorer.Or run the pure-JS snippet above.
Expected results
JSON output is
JSON.stringify's output: parseable, and parsing returns the original data unchanged.Actual results
Parsing the CLI's JSON output returns string values with a literal
\injected before every newline.Suggested fix
Remove the
.replace(...). The #2807 problem (invalid JSON from_ObjectIdentity_containing raw control characters) cannot be reproduced throughJSON.stringify, which escapes control characters by construction — if some code path emits raw control characters today, the fix belongs there, not in a post-stringify rewrite that corrupts valid output.Diagnostics
main)