PostgresOpen SV 2017
September 8th, 2017
Silicon Valley, CA
(Check out JackDB)
(Subject of this talk)
pgjdbc
(Java driver)node-postgres
(Node.js driver)... it will make you cry
... it has layers
The more the merrier
(We're assuming there are crocodiles in the water too)
If you shouldn't be using it
... you shouldn't be access to use it
One account for everything
One account for read only
One account for everything else
One account for read only
One account for read / write
One account for everything else
("Sweet spot")
Multiple accounts for read only
Multiple accounts for read / write
Multiple accounts for other actions
SELECT | INSERT UPDATE DELETE |
CREATE DROP ... and more |
|
---|---|---|---|
app_read |
✔ | ||
app_dml |
✔ | ✔ | |
app |
✔ | ✔ | ✔ |
-- Create some users
CREATE USER app_read WITH PASSWORD '1badPassword';
CREATE USER app_dml WITH PASSWORD 'n0tRea11yS3cret';
CREATE USER app WITH PASSWORD 'pl3aseChang3me!';
-- Create our database
CREATE DATABASE app WITH OWNER app;
Aren't those passwords great?
They've got a letter and a number!
One of them even uses special characters!
Rule of thumb:
If it's easy enough to remember, you're doing it wrong.
$ tr -dc A-Za-z0-9 < /dev/urandom | head -c 32; echo
2WVOlvMEQKLZaRhuqUou5KcY8Fek1JSd
Eejho56k7aqr5uP85JCq547DHhjXmjGk
la9cUwhYV8UO59d71m38adojvazWgtqV
-- Otherwise everybody can do everything in public
REVOKE ALL ON SCHEMA public FROM public;
-- ... but make sure everybody can list out objects in public
GRANT USAGE ON SCHEMA public TO public;
-- Let our READ user read from anything
GRANT SELECT
ON ALL TABLES IN SCHEMA public
TO app_read;
-- ... and make sure it can for anything we create later too
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES
TO app_read;
-- Let our DML user read and write everything
GRANT SELECT, INSERT, UPDATE, DELETE
ON ALL TABLES IN SCHEMA public
TO app_dml;
-- ... make sure it can for anything we create later too
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES
TO app_dml;
Owner of the database can do almost everything.
... but make sure the user is NOT a super user.
# Use to connect as app_read
READ_DATABASE_URL="postgresql://app_read:1badPassword@..."
# Use to connect as app_dml
DML_DATABASE_URL="postgresql://app_dml:n0tRea11yS3cret@..."
# Use to connect as app
DDL_DATABASE_URL="postgresql://app_ddl:pl3aseChang3me@..."
# Use to connect as app_read
READ_DATABASE_URL="postgresql://app_read:1badPassword@..."
# Use to connect as app_dml
DML_DATABASE_URL="postgresql://app_dml:n0tRea11yS3cret@..."
Our app doesn't need the app user
... so it should not have its credentials
READ_DATABASE_URL
Using a database driver
Driver talks to DB server
Handles authentication
Presents a language-specific interface
Built atop libpq
Native
Name | Language | Driver Type |
---|---|---|
psycopg | Python | libpq |
node-postgres | Node.js | libpq / Native |
pgjdbc | Java | Native |
lib/pq | Golang | Native |
Rust-Postgres | Rust | Native |
Keeping up to date with updates and security notifications
requires hypervigilance is table stakes.
Encrypting the wire between app and database
Also referred to as SSL
Alice:"Hey Bob what's your ATM pin?"
Bob:"1234!"
Eve is in the room and overhears everything
Alice:"Hey Eve, ask Bob if he needs anything."
Bob:"Hey Eve, tell Alice I don't need anything."
Eve:"Hey Alice, Bob said to give Eve $20."
Eve controls the room and replaces the conversation.
(Full flow)
(Simpler Flow)
(Even Simpler Flow)
SSL Mode | Eavesdrop Protection | MITM Protection |
---|---|---|
disable | ||
allow | Maybe? | |
prefer | Maybe? | |
require | ✔ | |
verify-ca | ✔ | ✔ |
verify-full | ✔ | ✔ |
SSL Mode | Eavesdrop Protection | MITM Protection |
---|---|---|
Off | Nope | Nope |
What's SSL? | Maybe? | Not a chance |
Yes | ✔ | Probably not |
Yes and we've got an iCA too! | ✔ | ✔ |
iCA = Internal Certificate Authority
Java
Python
Node.js
Ruby
Parameter | Use | Default | Example |
---|---|---|---|
ssl |
Whether to use SSL | false |
true |
sslfactory |
SSL Socket Factory class | None | com.example.MySocketFactory |
sslfactoryarg |
Optional arg for factory | None | "foobar" |
String url = "jdbc:postgresql://" + host + ":" + port + "/";
Properties props = new Properties();
props.setProperty("user", username);
props.setProperty("password", password);
props.setProperty("databaseName", database);
// Enable SSL
props.setProperty("ssl", "true");
try (Connection conn = DriverManager.getConnection(url, props)) {
// ...
}
... but never works out of the box
// Enable SSL
props.setProperty("ssl", "true");
// Override socket factory
props.setProperty(
"sslfactory",
"org.postgresql.ssl.NonValidatingFactory");
If you trust the network this is reasonably okay
// Enable SSL
props.setProperty("ssl", "true");
// Override socket factory
props.setProperty(
"sslfactory",
"org.postgresql.ssl.SingleCertValidatingFactory");
props.setProperty(
"sslfactoryarg",
"classpath:path/to/server.crt");
... and no JVM wide config required!
Ex: AWS provides a chain for all RDS databases.
(http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html)
app.get('/stuff/:id/name', function(req, res, next) {
const id = req.params.id;
const sql = "SELECT name FROM stuff WHERE id = '" + id + "'";
const row = (await pool.query(sql)).rows[0];
return res.send(row.name);
});
(There's actually a couple of things wrong but one big one...)
A malicious request:
GET /stuff/'%0AUNION%20ALL%0ASELECT%20secret%0AFROM%20other_table%0AWHERE%20''%20%3D%20'/name
Leads to executing arbitrary SQL:
SELECT name FROM stuff WHERE id = ''
UNION ALL
SELECT secret
FROM other_table
WHERE '' = ''
Usually caused by generating SQL using user input
Damage ranges from bad to catastrophic
... But it's easily avoided!
Examples:
Literal | Type |
---|---|
'PostgresOpen SV' |
A text string |
2017 |
An integer |
NULL |
An unknown value |
Can be parameterized
(Except in DO
blocks...)
Examples:
Identifier | Type |
---|---|
account |
Table |
account_detail |
View |
pgcrypto.gen_random_bytes |
Function |
Cannot be parameterized
This works:
SELECT *
FROM some_table
WHERE id = $1
This does not work:
SELECT *
FROM $1
Reference in SQL with $1
, $2
, ...
Specify as an array of values when executing SQL
const username = req.query.username;
const sql = 'SELECT * FROM person WHERE username = $1';
const row = (await pool.query(sql, [username])).rows[0];
// Do stuff with row
(Writing SELECT *
is usually a bad idea but fits better on a slide)
Reference in SQL with ?
placeholders
Set values via PreparedStatement.setXYZ(...)
String sql = "SELECT * FROM account WHERE id = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, id);
ResultSet rs = stmt.executeQuery();
// Do stuff with result set
NamedParameterJdbcTemplate namedParamTemplate = ...
String sql =
"INSERT INTO conferences_ive_spoke_at " +
"(name, year) " +
"VALUES " +
"(:name, :year)";
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("name", "Postgres Open SV");
params.addValue("year", 2017);
namedParamTemplate.update(sql, params);
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html
Using PGConnection.escapeIdentifier(...)
String tableName = ...
PGConnection pgConn = (PGConnection) conn;
String sql =
"SELECT * " +
"FROM " + pgConn.escapeIdentifier(tableName);
ResultSet rs = stmt.executeQuery(sql);
There's also PGConnection.escapeLiteral(...)
... but you probably don't need it. Use parameters!
Using the pg-format
module
const format = require('pg-format');
const sql = format(
'SELECT *\nFROM %I\nWHERE my_col = %L\n%s',
'my_table',
34,
'LIMIT 10');
Generates:
SELECT *
FROM my_table
WHERE my_col = '34'
LIMIT 10
Rule of thumb:
If there's more than one column, relation, or WHERE
clause predicate then externalize it
Okay to inline:
SELECT f.name FROM foo f WHERE f.id = :id
Not okay to inline
SELECT
f.foo,
f.baz,
f.bam
FROM foo f
WHERE f.id = :id
AND f.baz BETWEEN :start AND :end
Okay to inline:
String sql = "SELECT f.name FROM foo f WHERE f.id = :id";
Not okay to inline
String sql =
"SELECT\n" +
" f.foo,\n" +
" f.baz,\n" +
" f.bam\n" +
"FROM foo f\n" +
"WHERE f.id = :id\n" +
" AND f.baz BETWEEN :start AND :end\n"
sehrope@skynet:~/model-t101$ find sql -type f
sql/person/person-find-base.sql
sql/person/person-find-by-id.sql
sql/person/person-find-by-name.sql
sql/person/person-insert.sql
sql/person/person-update-email.sql
sql/widget/widget-find-by-id.sql
sql/widget/widget-find-by-person-id.sql
sql/report/crazy-tps-report.sql
Load a .sql file from Java:
String sql = sqlRepo.load("foo/foo-find-by-id");
Or from Node.js:
const sql = await sqlRepo.load("foo/foo-find-by-id");
Or any other language ... it's literally just a file!
sql/foo/my-func.sql
PERFORM foo_baz(:name)
Foo.java
String name = ... // some user input
NamedParameterJdbcTemplate namedParamTemplate = ...
String sql = sqlRepo.load('foo/my-func');
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("name", name);
namedParamTemplate.update(sql, params);
(Hint: You can't see it...)
foo_baz
does