Skip to content
This repository was archived by the owner on Apr 3, 2019. It is now read-only.

Commit a229ddc

Browse files
rfkvladikoff
authored andcommitted
feat(mysql): Add config option for REQUIRED_SQL_MODES. (#334) r=@philbooth,@vladikoff
This also tweaks the implementation to preserve any existing mode flags on the connection, rather than overwriting them with just the modes from the required list.
1 parent 1400a26 commit a229ddc

File tree

3 files changed

+77
-27
lines changed

3 files changed

+77
-27
lines changed

config/config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ module.exports = function (fs, path, url, convict) {
7070
format: 'duration',
7171
env: 'SIGNIN_CODES_MAX_AGE',
7272
},
73+
requiredSQLModes: {
74+
doc: 'Comma-separated list of SQL mode flags to enforce on each connection',
75+
default: '',
76+
format: 'String',
77+
env: 'REQUIRED_SQL_MODES',
78+
},
7379
master: {
7480
user: {
7581
doc: 'The user to connect to for MySql',

lib/db/mysql.js

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,16 @@ module.exports = function (log, error) {
7272
options.master.charset = options.charset
7373
options.slave.charset = options.charset
7474

75-
var mode = REQUIRED_SQL_MODES.join(',')
76-
if (options.sql_mode && options.sql_mode !== mode) {
77-
log.error('createPoolCluster.invalidSqlMode', { sql_mode: options.sql_mode })
78-
throw new Error(`You cannot use any sql mode other than ${mode}`)
79-
} else {
80-
options.sql_mode = REQUIRED_SQL_MODES.join(',')
75+
this.requiredModes = REQUIRED_SQL_MODES
76+
if (options.requiredSQLModes) {
77+
this.requiredModes = options.requiredSQLModes.split(',')
78+
this.requiredModes.forEach(mode => {
79+
if (! /^[A-Z0-9_]+$/.test(mode)) {
80+
throw new Error('Invalid SQL mode: ' + mode)
81+
}
82+
})
8183
}
8284

83-
options.master.sql_mode = options.sql_mode
84-
options.slave.sql_mode = options.sql_mode
85-
86-
8785
// Use separate pools for master and slave connections.
8886
this.poolCluster.add('MASTER', options.master)
8987
this.poolCluster.add('SLAVE', options.slave)
@@ -1155,22 +1153,37 @@ module.exports = function (log, error) {
11551153
return resolve(connection)
11561154
}
11571155

1158-
var mode = REQUIRED_SQL_MODES.join(',')
1159-
connection.query(`SET SESSION sql_mode = '${mode}';`, (err) => {
1156+
// Enforce sane defaults on every new connection.
1157+
// These *should* be set by the database by default, but it's nice
1158+
// to have an additional layer of protection here.
1159+
connection.query('SELECT @@sql_mode AS mode;', (err, rows) => {
11601160
if (err) {
11611161
return reject(err)
11621162
}
11631163

1164-
connection.query('SET NAMES utf8mb4 COLLATE utf8mb4_bin;', (err) => {
1164+
const currentModes = rows[0]['mode'].split(',')
1165+
this.requiredModes.forEach(requiredMode => {
1166+
if (currentModes.indexOf(requiredMode) === -1) {
1167+
currentModes.push(requiredMode)
1168+
}
1169+
})
1170+
1171+
const newMode = currentModes.join(',')
1172+
connection.query(`SET SESSION sql_mode = '${newMode}';`, (err) => {
11651173
if (err) {
11661174
return reject(err)
11671175
}
11681176

1169-
connection._fxa_initialized = true
1170-
resolve(connection)
1177+
connection.query('SET NAMES utf8mb4 COLLATE utf8mb4_bin;', (err) => {
1178+
if (err) {
1179+
return reject(err)
1180+
}
1181+
1182+
connection._fxa_initialized = true
1183+
resolve(connection)
1184+
})
11711185
})
11721186
})
1173-
11741187
})
11751188
})
11761189
}

test/local/mysql_tests.js

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('MySQL', () => {
2626
})
2727

2828
it(
29-
'forces REQUIRED_CHARSET for connections',
29+
'validates REQUIRED_CHARSET for connections',
3030
() => {
3131
const configCharset = Object.assign({}, config)
3232
configCharset.charset = 'wat'
@@ -42,20 +42,51 @@ describe('MySQL', () => {
4242
)
4343

4444
it(
45-
'forces REQUIRED_SQL_MODES for connections',
45+
'accepts REQUIRED_SQL_MODES from config',
4646
() => {
47-
const configSqlMode = Object.assign({}, config)
48-
const REQUIRED_SQL_MODES = [
49-
'STRICT_ALL_TABLES',
50-
'NO_ENGINE_SUBSTITUTION',
51-
]
52-
var mode = REQUIRED_SQL_MODES.join(',')
53-
configSqlMode.sql_mode = 'xyz'
54-
return DB.connect(configSqlMode)
47+
const configModes = Object.assign({}, config)
48+
configModes.requiredSQLModes = 'STRICT_TRANS_TABLES,NO_ZERO_DATE'
49+
50+
return DB.connect(configModes)
51+
.then(
52+
db => {
53+
assert.deepEqual(db.requiredModes, [
54+
'STRICT_TRANS_TABLES',
55+
'NO_ZERO_DATE'
56+
])
57+
},
58+
assert.fail
59+
)
60+
}
61+
)
62+
63+
it(
64+
'rejects unrecognized REQUIRED_SQL_MODES values from config',
65+
() => {
66+
const configModes = Object.assign({}, config)
67+
configModes.requiredSQLModes = 'UNRECOGNIZED_SQL_MODE_NONSENSE'
68+
69+
return DB.connect(configModes)
70+
.then(
71+
assert.fail,
72+
err => {
73+
assert.equal(err.message, 'ER_WRONG_VALUE_FOR_VAR')
74+
}
75+
)
76+
}
77+
)
78+
79+
it(
80+
'rejects badly-formed REQUIRED_SQL_MODES from config, for safety',
81+
() => {
82+
const configModes = Object.assign({}, config)
83+
configModes.requiredSQLModes = 'TEST,MODE,\'; DROP TABLE users;'
84+
85+
return DB.connect(configModes)
5586
.then(
5687
assert.fail,
5788
err => {
58-
assert.equal(err.message, `You cannot use any sql mode other than ${mode}`)
89+
assert.ok(err.message.indexOf('Invalid SQL mode') === 0)
5990
}
6091
)
6192
}

0 commit comments

Comments
 (0)