{"id":"CVE-2026-126346","kind":"vulnerability","aliases":["CVE-2026-126346","GHSA-r4jh-29pc-gfqr"],"related":[],"title":"basic-ftp: Uncaught exception crashes the Node.js process when a password (or resumed-download path) contains CR/LF","description":"### Summary\n\nClient.login() calls FTPContext.send() from inside the control-socket data event handler. send() throws on CR, LF or NUL, and nothing catches the throw. A password containing \\r\\n therefore raises an uncaught exception and terminates the whole Node.js process. It does not reject the returned promise. Any application that passes untrusted input as the FTP password can be crashed remotely with one request. The same pattern exists in the REST/RETR path of downloadTo() when the remote path contains control characters.\n\n### Details\n\nThe CRLF guard (FTPContext.send, src/FtpContext.ts ~L233) correctly refuses to send commands containing \\r, \\n or \\0:\n```ts\nsend(command: string) {\n    if (/[\\r\\n\\0]/.test(command)) {\n        throw new Error(`Invalid command: Contains control characters. (${command})`);\n    }\n```\n\nThat works when send() is called from handle(), because it runs inside a Promise executor and the throw becomes a rejection. But in two places send() runs inside a response handler, which is called synchronously from the socket’s data event:\n\nClient.login() (src/Client.ts ~L246-248): after the server answers 331 to USER, the handler calls this.ftp.send(\"PASS \" + password). The USER command was already sent and is valid, so the password is only checked at this point. The call chain is socket.on(\"data\") → _onControlSocketData → _passToHandler → responseHandler → send() → throw. No try/catch exists anywhere in that chain, so it becomes an uncaughtException.\ndownloadTo() (src/transfer.ts ~L368-370): when startAt > 0, the initial command is REST n, which contains no user-controlled text. When the server answers 350, the handler calls config.ftp.send(\"RETR \" + config.remotePath). This remotePath was never checked before. A path with a control character throws in the data handler in the same way. (Found by code reading, not reproduced.)\n\nBecause the exception happens during data event dispatch, it can’t be handled with try/catch or .catch() around client.access() or downloadTo(). The application only survives if it has a global process.on(\"uncaughtException\") handler.\n\n### Proof of concept\n\nTested on Node.js v22.22.0 against basic-ftp 6.2.2 (built from source). It uses a minimal mock FTP server on localhost:\n\n```js\nconst net = require(\"net\")\nconst ftp = require(\"basic-ftp\")\n\nconst srv = net.createServer(s => {\n  s.write(\"220 hi\\r\\n\")\n  s.on(\"data\", d => {\n    const c = d.toString()\n    if (c.startsWith(\"USER\")) s.write(\"331 pw\\r\\n\")\n    else if (c.startsWith(\"PASS\")) s.write(\"230 ok\\r\\n\")\n    else s.write(\"200 ok\\r\\n\")\n  })\n})\n\nsrv.listen(0, async () => {\n  const c = new ftp.Client(2000)\n  try {\n    await c.access({\n      host: \"127.0.0.1\",\n      port: srv.address().port,\n      user: \"bob\",\n      password: \"x\\r\\nDELE important\"   // attacker-controlled value\n    })\n  } catch (e) {\n    console.log(\"rejected:\", e.message)  // never reached\n  }\n  console.log(\"handled gracefully\")      // never reached\n  process.exit(0)\n})\n```\nResult: the process terminates with an uncaught exception and neither catch nor the final console.log runs:\n```\nError: Invalid command: Contains control characters. (PASS x\nDELE important)\n    at FTPContext.send (dist/FtpContext.js:189:19)\n    at Object.responseHandler (dist/Client.js:186:26)\n    at FTPContext._passToHandler (dist/FtpContext.js:322:24)\n    at FTPContext._onControlSocketData (dist/FtpContext.js:312:18)\n    at Socket.<anonymous> (dist/FtpContext.js:133:44)\n```\nThe server in the PoC is benign. Any normal FTP server that replies 331 to USER triggers the same crash.\n\n### Impact\n\nDenial of service (uncaught exception, process termination), CWE-248.\n\nIt affects applications that pass untrusted or user-influenced values as the FTP password, or as the remote path of a resumed download. Examples are web apps or services that let users enter their own FTP credentials, or file-transfer or backup tools driven by user-supplied paths. One request crashes the whole Node.js process and every other client it serves. It needs no authentication to the FTP server and no special server behaviour. The attacker only needs control over the string. Confidentiality and integrity are not affected, and the CRLF guard still blocks command injection.\n\nSuggested fix:\n\nIn FTPContext._passToHandler(), wrap the handler call in a try/catch that rejects the task and calls closeWithError(err).\nValidate password in login(), and remotePath in _downloadToStream(), before any I/O and reject early.","summary":"basic-ftp throws an uncaught exception inside its socket data handler when the FTP password or a resumed-download remote path contains CR, LF or NUL characters. An attacker who controls that string can terminate the Node.js process with one request, unless the app has a global uncaughtException handler.","severity":"medium","cvss":5.9,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"basic-ftp","fixed":["6.2.3"],"affected":"<= 6.2.2","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["basic-ftp","cwe-248","cwe-755","ftp","node","package"],"not_affected_if":["fixed-credentials","no-user-input-in-paths"],"how_to_check":"- Run `npm ls basic-ftp` to find the installed version and whether it is pulled in transitively.\n- Check whether any code passes user-supplied values as the `password` in `client.access()` or as the remote path in `downloadTo()`.\n- Check for a global `process.on(\"uncaughtException\")` handler in the app.","fix":"- basic-ftp: move to `6.2.3` (affected: `<= 6.2.2`).","status":"open","published_at":"2026-11-12T22:19:19+00:00","first_seen_at":"2026-11-12T22:26:37+00:00","triaged_at":"2026-11-12T22:27:58+00:00","updated_at":"2026-11-12T22:40:37+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-11-12T22:27:58Z","model":"claude-haiku-5-5","output":{"fix":"- basic-ftp: move to `6.2.3` (affected: `<= 6.2.2`).","reach":"internet","title":"basic-ftp: a password with CR/LF crashes the whole Node.js process","impact":"low","summary":"basic-ftp throws an uncaught exception inside its socket data handler when the FTP password or a resumed-download remote path contains CR, LF or NUL characters. An attacker who controls that string can terminate the Node.js process with one request, unless the app has a global uncaughtException handler.","audience":"developers","category":"package","nicknames":[],"who_cares":["node","basic-ftp","ftp"],"confidence":0.85,"fix_status":"available","needs_auth":"no","worst_case":"one request crashes the entire Node.js process","analyst_note":"","how_to_check":"- Run `npm ls basic-ftp` to find the installed version and whether it is pulled in transitively.\n- Check whether any code passes user-supplied values as the `password` in `client.access()` or as the remote path in `downloadTo()`.\n- Check for a global `process.on(\"uncaughtException\")` handler in the app.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["fixed-credentials","no-user-input-in-paths"],"who_is_affected":"Applications that use basic-ftp and pass untrusted or user-influenced values as the FTP password or as the remote path of a resumed download, such as apps where users enter their own FTP credentials or backup tools driven by user paths. Apps that only use fixed, server-controlled credentials and paths are not exposed to this crash path.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"no","severity_if_unknown":"unknown"},"cost_usd":0.00092,"input_tokens":6189,"output_tokens":602,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-126346':22A,30A '-2026':21A,29A '-248':38B,257C,597C '-370':315C '-755':40B '/.test':202C '0':193C,201C,318C,477C,516C '127.0.0.1':488C '133':566C '18':562C '186':551C '189':546C '19':547C '200':472C '2000':483C '220':443C '230':466C '24':557C '26':552C '29pc':26A '312':561C '322':556C '331':262C,457C,581C '350':336C '44':567C '6.2.2':414C 'affect':599C 'alreadi':275C 'answer':261C,335C 'anywher':303C 'app':89B,625C 'applic':144C,392C,600C 'around':387C 'async':478C 'attack':74B,500C 'attacker-control':499C 'await':485C 'backup':641C 'basic':2A,32A,35B,45B,412C,436C 'basic-ftp':1A,31A,34B,44B,411C,435C 'becom':231C,309C 'benign':574C 'bob':493C 'built':415C 'c':451C,480C 'c.access':486C 'c.startswith':454C,463C 'call':97C,218C,246C,267C,290C,339C 'catch':117C,386C,503C,527C 'chain':291C,306C 'charact':72B,176C,211C,354C,538C 'check':285C,347C 'clie':659C 'client.access':388C 'client.login':96C,254C 'code':366C 'command':188C,196C,203C,208C,212C,273C,321C,535C 'concept':405C 'config.ftp.send':340C 'config.remotepath':342C 'console.log':505C,510C,531C 'const':428C,432C,438C,450C,479C 'contain':18A,67B,122C,174C,189C,209C,326C,536C 'control':76B,103C,175C,210C,330C,353C,501C,537C 'control-socket':102C 'correct':184C 'cr':68B,111C 'cr/lf':19A 'crash':6A,155C,587C,651C 'credenti':635C 'crlf':179C 'cve':20A,28A 'cwe':37B,39B,596C 'd':449C 'd.tostring':452C 'data':54B,105C,252C,294C,358C,375C,448C 'dele':541C 'denial':589C 'detail':177C 'dispatch':377C 'dist/client.js':550C 'dist/ftpcontext.js':545C,555C,560C,565C 'download':16A,64B,621C 'downloadto':169C,312C,390C 'driven':643C 'e':504C 'e.message':507C 'els':461C,470C 'enter':631C 'error':206C,533C 'event':106C,253C,376C 'everi':657C 'exampl':622C 'except':5A,50B,129C,372C,524C,593C 'executor':227C 'exist':163C,302C 'file':638C 'file-transf':637C 'final':530C 'found':364C 'ftp':3A,33A,36B,41B,46B,58B,151C,413C,423C,433C,437C,577C,611C,634C 'ftp.client':482C 'ftpcontext._oncontrolsocketdata':559C 'ftpcontext._passtohandler':554C 'ftpcontext.send':98C,181C,544C 'gfqr':27A 'ghsa':24A 'ghsa-r4jh-29pc-gfqr':23A 'global':92B,399C 'grace':512C 'guard':180C 'handl':220C,382C,511C 'handler':55B,94B,107C,243C,266C,338C,359C,402C 'happen':373C 'hi':444C 'host':487C 'impact':588C 'import':498C,542C 'influenc':607C 'initi':320C 'input':148C 'insid':51B,100C,224C,240C 'invalid':207C,534C 'js':427C 'l233':183C 'l246':256C 'l368':314C 'let':629C 'lf':69B,112C 'localhost':426C 'minim':421C 'mock':422C 'n':124C,191C,200C,324C,446C,460C,469C,475C 'ndele':497C 'neither':526C 'net':429C,431C 'net.createserver':440C 'never':346C,508C,513C 'new':205C,481C 'node':42B 'node.js':8A,82B,134C,408C,654C 'normal':576C 'noth':116C 'nul':71B,114C 'object.responsehandler':549C 'ok':467C,473C 'oncontrolsocketdata':295C 'one':85B,158C,649C 'packag':43B 'pass':146C,269C,464C,539C,602C 'passtohandl':296C 'password':12A,59B,121C,152C,270C,282C,494C,612C 'path':17A,66B,167C,173C,350C,617C,648C 'pattern':162C 'place':237C 'poc':572C 'point':288C 'port':489C,491C 'process':9A,83B,135C,519C,594C,655C 'process.exit':515C 'process.on':400C 'promis':142C,226C 'proof':403C 'pw':458C 'r':123C,190C,199C,445C,459C,468C,474C,496C 'r4jh':25A 'rais':126C 'reach':509C,514C 'read':367C 'refus':185C 'reject':139C,233C,506C 'remot':65B,156C,172C,616C 'remotepath':344C 'repli':580C 'reproduc':369C 'request':86B,159C,650C 'requir':430C,434C 'respons':242C 'responsehandl':297C 'rest':323C 'rest/retr':166C 'result':517C 'resum':15A,63B,620C 'resumed-download':14A,62B 'retr':341C 'return':141C 'run':223C,239C,532C 's.on':447C 's.write':442C,456C,465C,471C 'send':108C,187C,195C,216C,238C,298C 'sent':276C 'server':260C,334C,424C,569C,578C 'servic':591C,627C 'socket':53B,104C,250C,564C 'socket.on':293C 'sourc':417C 'src/client.ts':255C 'src/ftpcontext.ts':182C 'src/transfer.ts':313C 'srv':439C 'srv.address':490C 'srv.listen':476C 'startat':317C 'string':78B,197C 'summari':95C 'suppli':647C 'surviv':394C 'synchron':247C 'termin':80B,131C,520C,595C 'test':406C 'text':331C 'therefor':125C 'this.ftp.send':268C 'throw':47B,109C,119C,204C,230C,299C,355C 'tool':642C 'transfer':639C 'tri':484C 'trigger':584C 'try/catch':301C,384C 'ts':194C 'two':236C 'uncaught':4A,49B,128C,523C,592C 'uncaughtexcept':93B,311C,401C 'unless':87B 'untrust':147C,603C 'use':419C 'user':264C,272C,329C,455C,492C,583C,606C,630C,646C 'user-control':328C 'user-influenc':605C 'user-suppli':645C 'v22.22.0':409C 'valid':279C 'valu':502C,608C 'way':363C 'web':624C 'whole':133C,653C 'work':214C 'x':495C,540C","nicknames":[],"mitigations":[],"who_affected":"Applications that use basic-ftp and pass untrusted or user-influenced values as the FTP password or as the remote path of a resumed download, such as apps where users enter their own FTP credentials or backup tools driven by user paths. Apps that only use fixed, server-controlled credentials and paths are not exposed to this crash path.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["basicftp"],"ptokens":["basic","ftp"],"labels":["basicftp","cwe248","cwe755","ftp","medium","node","npm","package"],"campaign_type":null,"search_a":"'-126346':22A,30A '-2026':21A,29A '29pc':26A 'basic':2A,32A 'basic-ftp':1A,31A 'contain':18A 'cr/lf':19A 'crash':6A 'cve':20A,28A 'download':16A 'except':5A 'ftp':3A,33A 'gfqr':27A 'ghsa':24A 'ghsa-r4jh-29pc-gfqr':23A 'node.js':8A 'password':12A 'path':17A 'process':9A 'r4jh':25A 'resum':15A 'resumed-download':14A 'uncaught':4A","search_b":"'-248':38B '-755':40B 'app':89B 'attack':74B 'basic':35B,45B 'basic-ftp':34B,44B 'charact':72B 'contain':67B 'control':76B 'cr':68B 'cwe':37B,39B 'data':54B 'download':64B 'except':50B 'ftp':36B,41B,46B,58B 'global':92B 'handler':55B,94B 'insid':51B 'lf':69B 'node':42B 'node.js':82B 'nul':71B 'one':85B 'packag':43B 'password':59B 'path':66B 'process':83B 'remot':65B 'request':86B 'resum':63B 'resumed-download':62B 'socket':53B 'string':78B 'termin':80B 'throw':47B 'uncaught':49B 'uncaughtexcept':93B 'unless':87B","search_c":"'-248':257C,597C '-370':315C '/.test':202C '0':193C,201C,318C,477C,516C '127.0.0.1':488C '133':566C '18':562C '186':551C '189':546C '19':547C '200':472C '2000':483C '220':443C '230':466C '24':557C '26':552C '312':561C '322':556C '331':262C,457C,581C '350':336C '44':567C '6.2.2':414C 'affect':599C 'alreadi':275C 'answer':261C,335C 'anywher':303C 'app':625C 'applic':144C,392C,600C 'around':387C 'async':478C 'attack':500C 'attacker-control':499C 'await':485C 'backup':641C 'basic':412C,436C 'basic-ftp':411C,435C 'becom':231C,309C 'benign':574C 'bob':493C 'built':415C 'c':451C,480C 'c.access':486C 'c.startswith':454C,463C 'call':97C,218C,246C,267C,290C,339C 'catch':117C,386C,503C,527C 'chain':291C,306C 'charact':176C,211C,354C,538C 'check':285C,347C 'clie':659C 'client.access':388C 'client.login':96C,254C 'code':366C 'command':188C,196C,203C,208C,212C,273C,321C,535C 'concept':405C 'config.ftp.send':340C 'config.remotepath':342C 'console.log':505C,510C,531C 'const':428C,432C,438C,450C,479C 'contain':122C,174C,189C,209C,326C,536C 'control':103C,175C,210C,330C,353C,501C,537C 'control-socket':102C 'correct':184C 'cr':111C 'crash':155C,587C,651C 'credenti':635C 'crlf':179C 'cwe':596C 'd':449C 'd.tostring':452C 'data':105C,252C,294C,358C,375C,448C 'dele':541C 'denial':589C 'detail':177C 'dispatch':377C 'dist/client.js':550C 'dist/ftpcontext.js':545C,555C,560C,565C 'download':621C 'downloadto':169C,312C,390C 'driven':643C 'e':504C 'e.message':507C 'els':461C,470C 'enter':631C 'error':206C,533C 'event':106C,253C,376C 'everi':657C 'exampl':622C 'except':129C,372C,524C,593C 'executor':227C 'exist':163C,302C 'file':638C 'file-transf':637C 'final':530C 'found':364C 'ftp':151C,413C,423C,433C,437C,577C,611C,634C 'ftp.client':482C 'ftpcontext._oncontrolsocketdata':559C 'ftpcontext._passtohandler':554C 'ftpcontext.send':98C,181C,544C 'global':399C 'grace':512C 'guard':180C 'handl':220C,382C,511C 'handler':107C,243C,266C,338C,359C,402C 'happen':373C 'hi':444C 'host':487C 'impact':588C 'import':498C,542C 'influenc':607C 'initi':320C 'input':148C 'insid':100C,224C,240C 'invalid':207C,534C 'js':427C 'l233':183C 'l246':256C 'l368':314C 'let':629C 'lf':112C 'localhost':426C 'minim':421C 'mock':422C 'n':124C,191C,200C,324C,446C,460C,469C,475C 'ndele':497C 'neither':526C 'net':429C,431C 'net.createserver':440C 'never':346C,508C,513C 'new':205C,481C 'node.js':134C,408C,654C 'normal':576C 'noth':116C 'nul':114C 'object.responsehandler':549C 'ok':467C,473C 'oncontrolsocketdata':295C 'one':158C,649C 'pass':146C,269C,464C,539C,602C 'passtohandl':296C 'password':121C,152C,270C,282C,494C,612C 'path':167C,173C,350C,617C,648C 'pattern':162C 'place':237C 'poc':572C 'point':288C 'port':489C,491C 'process':135C,519C,594C,655C 'process.exit':515C 'process.on':400C 'promis':142C,226C 'proof':403C 'pw':458C 'r':123C,190C,199C,445C,459C,468C,474C,496C 'rais':126C 'reach':509C,514C 'read':367C 'refus':185C 'reject':139C,233C,506C 'remot':156C,172C,616C 'remotepath':344C 'repli':580C 'reproduc':369C 'request':159C,650C 'requir':430C,434C 'respons':242C 'responsehandl':297C 'rest':323C 'rest/retr':166C 'result':517C 'resum':620C 'retr':341C 'return':141C 'run':223C,239C,532C 's.on':447C 's.write':442C,456C,465C,471C 'send':108C,187C,195C,216C,238C,298C 'sent':276C 'server':260C,334C,424C,569C,578C 'servic':591C,627C 'socket':104C,250C,564C 'socket.on':293C 'sourc':417C 'src/client.ts':255C 'src/ftpcontext.ts':182C 'src/transfer.ts':313C 'srv':439C 'srv.address':490C 'srv.listen':476C 'startat':317C 'string':197C 'summari':95C 'suppli':647C 'surviv':394C 'synchron':247C 'termin':131C,520C,595C 'test':406C 'text':331C 'therefor':125C 'this.ftp.send':268C 'throw':109C,119C,204C,230C,299C,355C 'tool':642C 'transfer':639C 'tri':484C 'trigger':584C 'try/catch':301C,384C 'ts':194C 'two':236C 'uncaught':128C,523C,592C 'uncaughtexcept':311C,401C 'untrust':147C,603C 'use':419C 'user':264C,272C,329C,455C,492C,583C,606C,630C,646C 'user-control':328C 'user-influenc':605C 'user-suppli':645C 'v22.22.0':409C 'valid':279C 'valu':502C,608C 'way':363C 'web':624C 'whole':133C,653C 'work':214C 'x':495C,540C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-10-30T06:33:17+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-r4jh-29pc-gfqr","url":"https://github.com/advisories/GHSA-r4jh-29pc-gfqr","severity":"medium","published_at":"2026-11-12T22:23:50+00:00","modified_at":"2026-11-12T22:23:50+00:00","first_seen_at":"2026-11-12T22:26:37+00:00","fetched_at":"2026-11-12T22:31:52+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-126346","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-126346","severity":null,"published_at":"2026-11-12T22:19:19+00:00","modified_at":"2026-11-12T22:19:19+00:00","first_seen_at":"2026-11-12T22:40:37+00:00","fetched_at":"2026-11-12T22:40:38+00:00","note":null}],"documents":[{"id":139708,"url":"https://api.github.com/repos/patrickjuchli/basic-ftp/security-advisories/GHSA-r4jh-29pc-gfqr","title":"Maintainer's advisory: api.github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: api.github.com"},{"id":139709,"url":"https://github.com/patrickjuchli/basic-ftp/security/advisories/GHSA-r4jh-29pc-gfqr","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":"2026-10-30T06:33:17+00:00","relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"},{"id":139710,"url":"https://github.com/patrickjuchli/basic-ftp/commit/0495b906bdf891e68806721b07b333dba65ec88e","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":139711,"url":"https://github.com/patrickjuchli/basic-ftp/releases/tag/v6.2.3","title":"Release: github.com","kind":"release","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Release: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}