Skip to content

Commit 1dec881

Browse files
authored
fix(socks5): enforce authenticated state before CONNECT (#5097)
1 parent 2a6f9c7 commit 1dec881

3 files changed

Lines changed: 49 additions & 8 deletions

File tree

lib/core/socks5-client.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const STATES = {
5151
INITIAL: 'initial',
5252
HANDSHAKING: 'handshaking',
5353
AUTHENTICATING: 'authenticating',
54+
AUTHENTICATED: 'authenticated',
5455
CONNECTING: 'connecting',
5556
CONNECTED: 'connected',
5657
ERROR: 'error',
@@ -139,6 +140,11 @@ class Socks5Client extends EventEmitter {
139140
}
140141
}
141142

143+
markAuthenticated () {
144+
this.state = STATES.AUTHENTICATED
145+
this.emit('authenticated')
146+
}
147+
142148
/**
143149
* Start the SOCKS5 handshake
144150
*/
@@ -189,7 +195,7 @@ class Socks5Client extends EventEmitter {
189195
debug('server selected auth method', method)
190196

191197
if (method === AUTH_METHODS.NO_AUTH) {
192-
this.emit('authenticated')
198+
this.markAuthenticated()
193199
} else if (method === AUTH_METHODS.USERNAME_PASSWORD) {
194200
this.state = STATES.AUTHENTICATING
195201
this.sendAuthRequest()
@@ -254,7 +260,7 @@ class Socks5Client extends EventEmitter {
254260

255261
this.buffer = this.buffer.subarray(2)
256262
debug('authentication successful')
257-
this.emit('authenticated')
263+
this.markAuthenticated()
258264
}
259265

260266
/**
@@ -263,8 +269,12 @@ class Socks5Client extends EventEmitter {
263269
* @param {number} port - Target port
264270
*/
265271
connect (address, port) {
266-
if (this.state === STATES.CONNECTED) {
267-
throw new InvalidArgumentError('Already connected')
272+
if (this.state === STATES.CONNECTING || this.state === STATES.CONNECTED) {
273+
throw new InvalidArgumentError('Connection already in progress')
274+
}
275+
276+
if (this.state !== STATES.AUTHENTICATED) {
277+
throw new InvalidArgumentError('Client must be authenticated before CONNECT')
268278
}
269279

270280
debug('connecting to', address, port)

lib/dispatcher/socks5-proxy-agent.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const { URL } = require('node:url')
66
let tls // include tls conditionally since it is not always available
77
const DispatcherBase = require('./dispatcher-base')
88
const { InvalidArgumentError } = require('../core/errors')
9-
const { Socks5Client } = require('../core/socks5-client')
9+
const { Socks5Client, STATES } = require('../core/socks5-client')
1010
const { kDispatch, kClose, kDestroy } = require('../core/symbols')
1111
const Pool = require('./pool')
1212
const buildConnector = require('../core/connect')
@@ -133,7 +133,7 @@ class Socks5ProxyAgent extends DispatcherBase {
133133
}
134134

135135
// Check if already authenticated (for NO_AUTH method)
136-
if (socks5Client.state === 'authenticated') {
136+
if (socks5Client.state === STATES.AUTHENTICATED) {
137137
clearTimeout(authenticationTimeout)
138138
authenticationReady.resolve()
139139
} else {

test/socks5-client.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,15 @@ test('Socks5Client - handshake flow', async (t) => {
5151
p.equal(client.state, STATES.INITIAL, 'should start in INITIAL state')
5252

5353
client.on('authenticated', () => {
54-
p.equal(client.state, STATES.HANDSHAKING, 'should be in HANDSHAKING state after auth')
54+
p.equal(client.state, STATES.AUTHENTICATED, 'should be in AUTHENTICATED state after auth')
5555
p.ok(true, 'should emit authenticated event')
5656
})
5757

5858
await client.handshake()
5959

6060
// Wait for the authenticated event
6161
await new Promise((resolve) => {
62-
if (client.state !== STATES.HANDSHAKING) {
62+
if (client.state === STATES.AUTHENTICATED) {
6363
resolve()
6464
} else {
6565
client.once('authenticated', resolve)
@@ -72,6 +72,37 @@ test('Socks5Client - handshake flow', async (t) => {
7272
await p.completed
7373
})
7474

75+
test('Socks5Client - connect requires authenticated state', async (t) => {
76+
const p = tspl(t, { plan: 2 })
77+
78+
class MockSocket {
79+
constructor () {
80+
this.destroyed = false
81+
this.writes = []
82+
}
83+
84+
on () {}
85+
86+
write (chunk) {
87+
this.writes.push(chunk)
88+
}
89+
90+
destroy () {
91+
this.destroyed = true
92+
}
93+
}
94+
95+
const socket = new MockSocket()
96+
const client = new Socks5Client(socket)
97+
98+
p.throws(() => {
99+
client.connect('example.com', 80)
100+
}, InvalidArgumentError, 'should reject connect before authentication')
101+
p.equal(socket.writes.length, 0, 'should not write CONNECT before authentication')
102+
103+
await p.completed
104+
})
105+
75106
test('Socks5Client - username/password authentication', async (t) => {
76107
const p = tspl(t, { plan: 7 })
77108

0 commit comments

Comments
 (0)