Skip to content

Commit 04babec

Browse files
committed
A simple, configurable solution to blacklist usernames
1 parent c827613 commit 04babec

File tree

8 files changed

+179
-21
lines changed

8 files changed

+179
-21
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Note that this example creates the `fullchain.pem` and `privkey.pem` files
8282
in a directory one level higher from the current, so that you don't
8383
accidentally commit your certificates to `solid` while you're developing.
8484

85-
If you would like to get rid of the browser warnings, import your fullchain.pem certificate into your 'Trusted Root Certificate' store.
85+
If you would like to get rid of the browser warnings, import your fullchain.pem certificate into your 'Trusted Root Certificate' store.
8686

8787
### Run multi-user server (intermediate)
8888

@@ -375,6 +375,12 @@ In order to test a single component, you can run
375375
npm run test-(acl|formats|params|patch)
376376
```
377377

378+
## Blacklisted usernames
379+
380+
By default Solid will ban [certain usernames as they might cause
381+
confusion or allow vulnerabilies for social engineering](https://github.com/marteinn/The-Big-Username-Blacklist).
382+
This list is configurable via `config/usernames-blacklist.json`. Solid does not
383+
blacklist profanities by default.
378384

379385
## Contributing
380386

config/usernames-blacklist.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"useTheBigUsernameBlacklist": true,
3+
"customBlacklistedUsernames": []
4+
}

lib/common/blacklist-service.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const blacklistConfig = require('../../config/usernames-blacklist.json')
2+
const blacklist = require('the-big-username-blacklist').list
3+
4+
class BlacklistService {
5+
constructor () {
6+
this.reset()
7+
}
8+
9+
addWord (word) {
10+
this.list.push(BlacklistService._prepareWord(word))
11+
}
12+
13+
reset (config) {
14+
this.list = BlacklistService._initList(config)
15+
}
16+
17+
validate (word) {
18+
return this.list.indexOf(BlacklistService._prepareWord(word)) === -1
19+
}
20+
21+
static _initList (config = blacklistConfig) {
22+
return [
23+
...(config.useTheBigUsernameBlacklist ? blacklist : []),
24+
...config.customBlacklistedUsernames
25+
]
26+
}
27+
28+
static _prepareWord (word) {
29+
return word.trim().toLocaleLowerCase()
30+
}
31+
}
32+
33+
module.exports = new BlacklistService()

lib/requests/create-account-request.js

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const AuthRequest = require('./auth-request')
44
const WebIdTlsCertificate = require('../models/webid-tls-certificate')
55
const debug = require('../debug').accounts
6+
const blacklistService = require('../common/blacklist-service')
67

78
/**
89
* Represents a 'create new user account' http request (either a POST to the
@@ -115,30 +116,28 @@ class CreateAccountRequest extends AuthRequest {
115116
/**
116117
* Creates an account for a given user (from a POST to `/api/accounts/new`)
117118
*
118-
* @throws {Error} An http 400 error if an account already exists
119+
* @throws {Error} If errors were encountering while validating the username.
119120
*
120121
* @return {Promise<UserAccount>} Resolves with newly created account instance
121122
*/
122-
createAccount () {
123+
async createAccount () {
123124
let userAccount = this.userAccount
124125
let accountManager = this.accountManager
125126

126-
return Promise.resolve(userAccount)
127-
.then(this.cancelIfUsernameInvalid.bind(this))
128-
.then(this.cancelIfAccountExists.bind(this))
129-
.then(this.createAccountStorage.bind(this))
130-
.then(this.saveCredentialsFor.bind(this))
131-
.then(this.sendResponse.bind(this))
132-
.then(userAccount => {
133-
// 'return' not used deliberately, no need to block and wait for email
134-
if (userAccount && userAccount.email) {
135-
debug('Sending Welcome email')
136-
accountManager.sendWelcomeEmail(userAccount)
137-
}
138-
})
139-
.then(() => {
140-
return userAccount
141-
})
127+
this.cancelIfUsernameInvalid(userAccount)
128+
this.cancelIfBlacklistedUsername(userAccount)
129+
await this.cancelIfAccountExists(userAccount)
130+
await this.createAccountStorage(userAccount)
131+
await this.saveCredentialsFor(userAccount)
132+
await this.sendResponse(userAccount)
133+
134+
// 'return' not used deliberately, no need to block and wait for email
135+
if (userAccount && userAccount.email) {
136+
debug('Sending Welcome email')
137+
accountManager.sendWelcomeEmail(userAccount)
138+
}
139+
140+
return userAccount
142141
}
143142

144143
/**
@@ -196,12 +195,33 @@ class CreateAccountRequest extends AuthRequest {
196195
* @throws {Error} If errors were encountering while validating the
197196
* username.
198197
*
199-
* @return {Promise<UserAccount>} Chainable
198+
* @return {UserAccount} Chainable
200199
*/
201200
cancelIfUsernameInvalid (userAccount) {
202201
if (!userAccount.username || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(userAccount.username)) {
203202
debug('Invalid username ' + userAccount.username)
204-
const error = new Error('Invalid username')
203+
const error = new Error('Invalid username (contains invalid characters)')
204+
error.status = 400
205+
throw error
206+
}
207+
208+
return userAccount
209+
}
210+
211+
/**
212+
* Check if a username is a valid slug.
213+
*
214+
* @param userAccount {UserAccount} Instance of the account to be created
215+
*
216+
* @throws {Error} If username is blacklisted
217+
*
218+
* @return {UserAccount} Chainable
219+
*/
220+
cancelIfBlacklistedUsername (userAccount) {
221+
const validUsername = blacklistService.validate(userAccount.username)
222+
if (!validUsername) {
223+
debug('Invalid username ' + userAccount.username)
224+
const error = new Error('Invalid username (username is blacklisted)')
205225
error.status = 400
206226
throw error
207227
}

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"solid-permissions": "^0.6.0",
7878
"solid-ws": "^0.2.3",
7979
"text-encoder-lite": "^1.0.1",
80+
"the-big-username-blacklist": "^1.5.1",
8081
"ulid": "^0.1.0",
8182
"uuid": "^3.0.0",
8283
"valid-url": "^1.0.9",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict'
2+
3+
const chai = require('chai')
4+
const expect = chai.expect
5+
6+
const blacklist = require('the-big-username-blacklist').list
7+
const blacklistService = require('../../lib/common/blacklist-service')
8+
9+
describe('BlacklistService', () => {
10+
afterEach(() => blacklistService.reset())
11+
12+
describe('addWord', () => {
13+
it('allows adding words', () => {
14+
const numberOfBlacklistedWords = blacklistService.list.length
15+
blacklistService.addWord('foo')
16+
expect(blacklistService.list.length).to.equal(numberOfBlacklistedWords + 1)
17+
})
18+
})
19+
20+
describe('reset', () => {
21+
it('will reset list of blacklisted words', () => {
22+
blacklistService.addWord('foo')
23+
blacklistService.reset()
24+
expect(blacklistService.list.length).to.equal(blacklist.length)
25+
})
26+
27+
it('can configure service via reset', () => {
28+
blacklistService.reset({
29+
useTheBigUsernameBlacklist: false,
30+
customBlacklistedUsernames: ['foo']
31+
})
32+
expect(blacklistService.list.length).to.equal(1)
33+
})
34+
35+
it('is a singleton', () => {
36+
const instanceA = blacklistService
37+
blacklistService.reset({ customBlacklistedUsernames: ['foo'] })
38+
expect(instanceA.validate('foo')).to.equal(blacklistService.validate('foo'))
39+
})
40+
})
41+
42+
describe('validate', () => {
43+
it('validates given a default list of blacklisted usernames', () => {
44+
const validWords = blacklist.reduce((memo, word) => memo + (blacklistService.validate(word) ? 1 : 0), 0)
45+
expect(validWords).to.equal(0)
46+
})
47+
})
48+
})

test/unit/create-account-request-test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ const sinonChai = require('sinon-chai')
77
chai.use(sinonChai)
88
chai.should()
99
const HttpMocks = require('node-mocks-http')
10+
const blacklist = require('the-big-username-blacklist')
1011

1112
const LDP = require('../../lib/ldp')
1213
const AccountManager = require('../../lib/models/account-manager')
1314
const SolidHost = require('../../lib/models/solid-host')
1415
const defaults = require('../../config/defaults')
1516
const { CreateAccountRequest } = require('../../lib/requests/create-account-request')
17+
const blacklistService = require('../../lib/common/blacklist-service')
1618

1719
describe('CreateAccountRequest', () => {
1820
let host, store, accountManager
@@ -127,6 +129,45 @@ describe('CreateAccountRequest', () => {
127129
expect(invalidUsernamesCount).to.eq(invalidUsernames.length)
128130
})
129131
})
132+
133+
describe('Blacklisted usernames', () => {
134+
const invalidUsernames = [...blacklist.list, 'foo']
135+
136+
before(() => {
137+
const accountManager = AccountManager.from({ host })
138+
accountManager.accountExists = sinon.stub().returns(Promise.resolve(false))
139+
blacklistService.addWord('foo')
140+
})
141+
142+
after(() => blacklistService.reset())
143+
144+
it('should return a 400 error if a username is blacklisted', async () => {
145+
const locals = { authMethod: defaults.auth, accountManager, oidc: { users: {} } }
146+
147+
let invalidUsernamesCount = 0
148+
149+
const requests = invalidUsernames.map((username) => {
150+
let req = HttpMocks.createRequest({
151+
app: { locals },
152+
body: { username, password: '1234' }
153+
})
154+
let request = CreateAccountRequest.fromParams(req, res)
155+
156+
return request.createAccount()
157+
.then(() => {
158+
throw new Error('should not happen')
159+
})
160+
.catch(err => {
161+
invalidUsernamesCount++
162+
expect(err.message).to.match(/Invalid username/)
163+
expect(err.status).to.equal(400)
164+
})
165+
})
166+
167+
await Promise.all(requests)
168+
expect(invalidUsernamesCount).to.eq(invalidUsernames.length)
169+
})
170+
})
130171
})
131172
})
132173

0 commit comments

Comments
 (0)