• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

haraka / Haraka / 26810897649

02 Jun 2026 09:26AM UTC coverage: 72.991% (-1.3%) from 74.33%
26810897649

Pull #3581

github

web-flow
Merge 135ad14ad into 2531b2fe9
Pull Request #3581: Add logging to address validation errors

1797 of 2340 branches covered (76.79%)

4 of 10 new or added lines in 1 file covered. (40.0%)

149 existing lines in 8 files now uncovered.

8064 of 11048 relevant lines covered (72.99%)

25.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.28
/plugins/auth/auth_base.js
1
// Base authentication plugin.
6✔
2
// This cannot be used on its own. You need to inherit from it.
6✔
3
// See plugins/auth/flat_file.js for an example.
6✔
4

6✔
5
// Note: You can disable setting `connection.notes.auth_passwd` by `plugin.blankout_password = true`
6✔
6

6✔
7
const crypto = require('node:crypto')
6✔
8

6✔
9
const tlds = require('haraka-tld')
6✔
10
const utils = require('haraka-utils')
6✔
11

6✔
12
const AUTH_COMMAND = 'AUTH'
6✔
13
const AUTH_METHOD_CRAM_MD5 = 'CRAM-MD5'
6✔
14
const AUTH_METHOD_PLAIN = 'PLAIN'
6✔
15
const AUTH_METHOD_LOGIN = 'LOGIN'
6✔
16
const LOGIN_STRING1 = 'VXNlcm5hbWU6' //Username: base64 coded
6✔
17
const LOGIN_STRING2 = 'UGFzc3dvcmQ6' //Password: base64 coded
6✔
18

6✔
19
exports.hook_capabilities = (next, connection) => {
6✔
UNCOV
20
    // Don't offer AUTH capabilities unless session is encrypted
×
UNCOV
21
    if (!connection.tls.enabled) return next()
×
UNCOV
22

✔
UNCOV
23
    const methods = ['PLAIN', 'LOGIN', 'CRAM-MD5']
×
UNCOV
24
    connection.capabilities.push(`AUTH ${methods.join(' ')}`)
×
UNCOV
25
    connection.notes.allowed_auth_methods = methods
×
UNCOV
26
    next()
×
UNCOV
27
}
×
UNCOV
28

×
29
// Override this at a minimum. Run cb(passwd) to provide a password.
6✔
30
exports.get_plain_passwd = (user, connection, cb) => cb()
6✔
31

×
32
exports.hook_unrecognized_command = function (next, connection, params) {
6✔
33
    if (params[0].toUpperCase() === AUTH_COMMAND && params[1]) {
4✔
34
        return this.select_auth_method(next, connection, params.slice(1).join(' '))
4✔
35
    }
2✔
36
    if (!connection.notes.authenticating) return next()
2!
37

×
38
    const am = connection.notes.auth_method
4✔
39
    if (am === AUTH_METHOD_CRAM_MD5 && connection.notes.auth_ticket) {
2!
40
        return this.auth_cram_md5(next, connection, params)
4!
41
    }
2✔
42
    if (am === AUTH_METHOD_LOGIN) {
4!
UNCOV
43
        return this.auth_login(next, connection, params)
✔
UNCOV
44
    }
×
UNCOV
45
    if (am === AUTH_METHOD_PLAIN) {
✔
UNCOV
46
        return this.auth_plain(next, connection, params)
×
UNCOV
47
    }
×
UNCOV
48
    next()
×
UNCOV
49
}
×
50

×
51
exports.check_plain_passwd = function (connection, user, passwd, cb) {
6✔
UNCOV
52
    function callback(plain_pw) {
✔
53
        cb(plain_pw === null ? false : plain_pw === passwd)
15✔
54
    }
13✔
UNCOV
55
    if (this.get_plain_passwd.length == 2) {
×
UNCOV
56
        this.get_plain_passwd(user, callback)
✔
UNCOV
57
    } else if (this.get_plain_passwd.length == 3) {
✔
UNCOV
58
        this.get_plain_passwd(user, connection, callback)
×
UNCOV
59
    } else {
×
UNCOV
60
        throw 'Invalid number of arguments for get_plain_passwd'
×
61
    }
×
62
}
×
63

×
64
exports.check_cram_md5_passwd = function (connection, user, passwd, cb) {
6✔
65
    function callback(plain_pw) {
2✔
66
        if (plain_pw == null) return cb(false)
2!
67

×
68
        const hmac = crypto.createHmac('md5', plain_pw.toString())
2✔
69
        hmac.update(connection.notes.auth_ticket)
2✔
70

2✔
71
        if (hmac.digest('hex') === passwd) return cb(true)
2✔
72

1✔
73
        cb(false)
1✔
74
    }
1✔
75
    if (this.get_plain_passwd.length == 2) {
2✔
76
        this.get_plain_passwd(user, callback)
2!
77
    } else if (this.get_plain_passwd.length == 3) {
2✔
78
        this.get_plain_passwd(user, connection, callback)
2✔
79
    } else {
2✔
80
        throw 'Invalid number of arguments for get_plain_passwd'
2!
81
    }
×
82
}
×
83

×
84
exports.check_user = function (next, connection, credentials, method) {
6✔
85
    const plugin = this
2✔
86
    connection.notes.authenticating = false
2✔
87
    if (!(credentials[0] && credentials[1])) {
2✔
88
        connection.respond(504, 'Invalid AUTH string', () => {
2!
UNCOV
89
            connection.reset_transaction(() => next(OK))
✔
90
        })
1✔
UNCOV
91
        return
×
UNCOV
92
    }
×
UNCOV
93

×
94
    // valid: (true|false)
2✔
95
    // opts: ({ message, code }|String)
2✔
96
    function passwd_ok(valid, opts) {
2✔
97
        const status_code = (typeof opts === 'object' && opts.code) || (valid ? 235 : 535)
18!
98
        const status_message =
18✔
99
            (typeof opts === 'object' ? opts.message : opts) ||
18!
100
            (valid ? '2.7.0 Authentication successful' : '5.7.8 Authentication failed')
18✔
101

3✔
102
        // The AUTH username is attacker-controlled (base64-decoded). Strip
18✔
103
        // control chars before it is stored in notes or emitted into the
18✔
104
        // Authentication-Results header (header injection).
18✔
105
        // eslint-disable-next-line no-control-regex
18✔
106
        const safe_user = String(credentials[0] ?? '').replace(/[\x00-\x1f\x7f]/g, '')
18!
107

18✔
108
        if (valid) {
18✔
109
            connection.relaying = true
18✔
110
            connection.results.add({ name: 'relay' }, { pass: plugin.name })
13✔
111

13✔
112
            connection.results.add(
13✔
113
                { name: 'auth' },
13✔
114
                {
13✔
115
                    pass: plugin.name,
13✔
116
                    method,
13✔
117
                    user: safe_user,
13✔
118
                },
13✔
119
            )
13✔
120

13✔
121
            connection.respond(status_code, status_message, () => {
13✔
122
                connection.authheader = '(authenticated bits=0)\n'
13✔
123
                connection.auth_results(`auth=pass (${method.toLowerCase()})`)
13✔
124
                connection.notes.auth_user = safe_user
13✔
125
                if (!plugin.blankout_password) connection.notes.auth_passwd = credentials[1]
13✔
126
                next(OK)
13✔
127
            })
13✔
128
            return
13✔
129
        }
13✔
130

13✔
131
        if (!connection.notes.auth_fails) connection.notes.auth_fails = 0
18✔
132

5✔
133
        connection.notes.auth_fails++
5✔
134
        connection.results.add({ name: 'auth' }, { fail: `${plugin.name}/${method}` })
5✔
135

5✔
136
        let delay = Math.pow(2, connection.notes.auth_fails - 1)
5✔
137
        if (plugin.timeout && delay >= plugin.timeout) {
5✔
138
            delay = plugin.timeout - 1
18!
139
        }
×
140
        connection.lognotice(plugin, `delaying for ${delay} seconds`)
18✔
141
        // here we include the username, as shown in RFC 5451 example
5✔
142
        connection.auth_results(`auth=fail (${method.toLowerCase()}) smtp.auth=${safe_user}`)
5✔
143
        setTimeout(() => {
5✔
144
            connection.respond(status_code, status_message, () => {
5✔
145
                connection.reset_transaction(() => next(OK))
5✔
146
            })
5✔
147
        }, delay * 1000)
5✔
148
    }
5✔
149

5✔
150
    if (method === AUTH_METHOD_PLAIN || method === AUTH_METHOD_LOGIN) {
2✔
151
        plugin.check_plain_passwd(connection, credentials[0], credentials[1], passwd_ok)
2!
152
    } else if (method === AUTH_METHOD_CRAM_MD5) {
2!
153
        plugin.check_cram_md5_passwd(connection, credentials[0], credentials[1], passwd_ok)
2✔
154
    }
2✔
155
}
2✔
156

2✔
157
exports.select_auth_method = function (next, connection, method) {
6✔
158
    const split = method.split(/\s+/)
2✔
159
    method = split.shift().toUpperCase()
2✔
160
    if (!connection.notes.allowed_auth_methods) return next()
2!
161
    if (!connection.notes.allowed_auth_methods.includes(method)) return next()
2!
UNCOV
162

✔
163
    if (connection.notes.authenticating) return next(DENYDISCONNECT, 'bad protocol')
2!
UNCOV
164

×
165
    connection.notes.authenticating = true
2✔
166
    connection.notes.auth_method = method
2✔
167

2✔
168
    if (method === AUTH_METHOD_PLAIN) return this.auth_plain(next, connection, split)
2!
169
    if (method === AUTH_METHOD_LOGIN) return this.auth_login(next, connection, split)
2!
170
    if (method === AUTH_METHOD_CRAM_MD5) return this.auth_cram_md5(next, connection)
2!
171
}
2✔
172

2✔
173
exports.auth_plain = function (next, connection, params) {
6✔
UNCOV
174
    // one parameter given on line, either:
×
UNCOV
175
    //    AUTH PLAIN <param> or
×
UNCOV
176
    //    AUTH PLAIN\n
×
UNCOV
177
    //...
×
UNCOV
178
    //    <param>
×
UNCOV
179
    if (params[0]) {
×
UNCOV
180
        const credentials = utils.unbase64(params[0]).split(/\0/)
✔
UNCOV
181
        credentials.shift() // Discard authid
×
UNCOV
182
        this.check_user(next, connection, credentials, AUTH_METHOD_PLAIN)
×
UNCOV
183
        return
×
UNCOV
184
    }
×
UNCOV
185

×
UNCOV
186
    if (connection.notes.auth_plain_asked_login) {
✔
UNCOV
187
        return next(DENYDISCONNECT, 'bad protocol')
×
188
    }
×
189

×
UNCOV
190
    connection.respond(334, ' ', () => {
✔
UNCOV
191
        connection.notes.auth_plain_asked_login = true
✔
192
        next(OK)
2✔
193
    })
2✔
194
}
2✔
195

2✔
196
exports.auth_login = function (next, connection, params) {
6✔
UNCOV
197
    if (
×
UNCOV
198
        (!connection.notes.auth_login_asked_login && params[0]) ||
✔
UNCOV
199
        (connection.notes.auth_login_asked_login && !connection.notes.auth_login_userlogin)
✔
UNCOV
200
    ) {
×
UNCOV
201
        if (!params[0]) return next(DENYDISCONNECT, 'bad protocol')
×
202

×
UNCOV
203
        const login = utils.unbase64(params[0])
×
UNCOV
204
        connection.respond(334, LOGIN_STRING2, () => {
×
UNCOV
205
            connection.notes.auth_login_userlogin = login
✔
206
            connection.notes.auth_login_asked_login = true
4✔
207
            next(OK)
4✔
208
        })
4✔
UNCOV
209
        return
×
UNCOV
210
    }
×
UNCOV
211

×
UNCOV
212
    if (connection.notes.auth_login_userlogin) {
✔
UNCOV
213
        const credentials = [connection.notes.auth_login_userlogin, utils.unbase64(params[0])]
✔
UNCOV
214

×
UNCOV
215
        connection.notes.auth_login_userlogin = null
×
UNCOV
216
        connection.notes.auth_login_asked_login = false
×
UNCOV
217

×
UNCOV
218
        return this.check_user(next, connection, credentials, AUTH_METHOD_LOGIN)
×
UNCOV
219
    }
×
UNCOV
220

×
UNCOV
221
    connection.respond(334, LOGIN_STRING1, () => {
✔
UNCOV
222
        connection.notes.auth_login_asked_login = true
✔
223
        next(OK)
2✔
224
    })
2✔
225
}
2✔
226

2✔
227
exports.auth_cram_md5 = function (next, connection, params) {
6✔
228
    if (params) {
4✔
229
        const credentials = utils.unbase64(params[0]).split(' ')
4✔
230
        return this.check_user(next, connection, credentials, AUTH_METHOD_CRAM_MD5)
2✔
231
    }
2✔
232

2✔
233
    const ticket = `<${this.hexi(Math.floor(Math.random() * 1000000))}.${this.hexi(Date.now())}@${connection.local.host}>`
2✔
234

2✔
235
    connection.loginfo(this, `ticket: ${ticket}`)
2✔
236
    connection.respond(334, utils.base64(ticket), () => {
2✔
237
        connection.notes.auth_ticket = ticket
2✔
238
        next(OK)
2✔
239
    })
2✔
240
}
2✔
241

2✔
242
exports.hexi = (number) => String(Math.abs(parseInt(number)).toString(16))
6✔
243

4✔
244
exports.constrain_sender = function (next, connection, params) {
6✔
245
    if (this?.cfg?.main?.constrain_sender === false) return next()
3!
246

×
247
    const au = connection.results.get('auth')?.user
3✔
248
    if (!au) return next()
3✔
249

2!
250
    const ad = /@/.test(au) ? au.split('@').pop() : null
3!
251
    const ed = params[0].host
3✔
252

3✔
253
    if (!ad || !ed) return next()
3!
254

1✔
255
    const auth_od = tlds.get_organizational_domain(ad)
3!
UNCOV
256
    const envelope_od = tlds.get_organizational_domain(ed)
×
UNCOV
257

×
UNCOV
258
    if (auth_od === envelope_od) return next()
×
UNCOV
259

✔
UNCOV
260
    next(DENY, `Envelope domain '${envelope_od}' doesn't match AUTH domain '${auth_od}'`)
×
UNCOV
261
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc