@@ -86,6 +86,38 @@ describe('serializeCredentials', () => {
8686 expect ( text ) . toContain ( 'api_key = sk' ) ;
8787 expect ( text ) . not . toContain ( 'api_url' ) ;
8888 } ) ;
89+
90+ it ( 'strips newline characters from values to prevent INI injection' , ( ) => {
91+ // A malicious apiUrl with embedded newlines could inject new key-value
92+ // pairs or section headers into the credentials file. The serializer
93+ // must strip \n and \r so the written file has exactly one value per
94+ // field and no injected content parsed as separate keys/sections.
95+ const malicious = 'https://evil.com\napi_key = sk-HIJACKED\n[admin]\napi_key = sk-admin' ;
96+ const text = serializeCredentials ( { default : { apiKey : 'sk-real' , apiUrl : malicious } } ) ;
97+ // The output must NOT contain a standalone [admin] section header
98+ // (it would be on its own line if injection succeeded)
99+ const lines = text . split ( '\n' ) ;
100+ // Only one section header exists: [default]
101+ const sectionHeaders = lines . filter ( l => / ^ \[ .+ \] $ / . test ( l . trim ( ) ) ) ;
102+ expect ( sectionHeaders ) . toEqual ( [ '[default]' ] ) ;
103+ // Only one api_key line exists (the real one, not an injected duplicate)
104+ const apiKeyLines = lines . filter ( l => l . trim ( ) . startsWith ( 'api_key' ) ) ;
105+ expect ( apiKeyLines ) . toHaveLength ( 1 ) ;
106+ expect ( apiKeyLines [ 0 ] ) . toContain ( 'sk-real' ) ;
107+ // Round-trip: reading back must return only the real key, not the injected one
108+ const parsed = parseCredentials ( text ) ;
109+ expect ( parsed [ 'default' ] ?. apiKey ) . toBe ( 'sk-real' ) ;
110+ expect ( parsed [ 'admin' ] ) . toBeUndefined ( ) ;
111+ } ) ;
112+
113+ it ( 'strips \\r\\n (CRLF) injection from values' , ( ) => {
114+ const text = serializeCredentials ( { default : { apiUrl : 'https://x.com\r\napi_key = pwned' } } ) ;
115+ const parsed = parseCredentials ( text ) ;
116+ // The injected api_key must NOT be parsed as a real key
117+ expect ( parsed [ 'default' ] ?. apiKey ) . toBeUndefined ( ) ;
118+ // The api_url value is on one line (newlines stripped)
119+ expect ( parsed [ 'default' ] ?. apiUrl ) . toContain ( 'https://x.com' ) ;
120+ } ) ;
89121} ) ;
90122
91123describe ( 'readCredentialsFile / readProfile' , ( ) => {
0 commit comments