Skip to content

Commit

Permalink
Security tests (saltcorn#715)
Browse files Browse the repository at this point in the history
* initial

* org as pytest

* content

* reset

* sc session

* misc login conditions

* fix hard crash

* start, kill server

* in github actions

* path to saltcorn bin

* order back

* timeout. test to fixtures

* readme

* futher tests

* signup tests

* reset first

* different port
  • Loading branch information
glutamate authored Apr 27, 2021
1 parent dcff129 commit d804ea1
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 8 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jobs:
- run: lerna bootstrap
env:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true"
- run: pip3 install requests pytest
- run: echo '127.0.0.1 example.com sub.example.com sub1.example.com sub2.example.com sub3.example.com sub4.example.com sub5.example.com' | sudo tee -a /etc/hosts
- run: echo '127.0.0.1 otherexample.com' | sudo tee -a /etc/hosts
- run: packages/saltcorn-cli/bin/saltcorn run-tests
Expand All @@ -59,6 +60,17 @@ jobs:
PGPASSWORD:
postgres
# The default PostgreSQL port
- run: pytest
env:
CI: true
SALTCORN_MULTI_TENANT: true
SALTCORN_SESSION_SECRET: "rehjtyjrtjr"
PGHOST: localhost
PGUSER: postgres
PGDATABASE: saltcorn_test
PGPASSWORD:
postgres
# The default PostgreSQL port
- run: packages/saltcorn-cli/bin/saltcorn run-tests saltcorn-data
env:
SQLITE_FILEPATH: /tmp/testdb.sqlite
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,6 @@ sessions.sqlite

.greenlockrc

infosec_scan_tmp/
infosec_scan_tmp/

*.pyc
9 changes: 9 additions & 0 deletions infosec-scan/py-sectest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Python security tests for Saltcorn

### Install

`pip3 install requests pytest`

### To run

`PGDATABASE=saltcorn_test pytest` in the saltcorn repository root directory
114 changes: 114 additions & 0 deletions infosec-scan/py-sectest/login_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from scsession import SaltcornSession

sess = SaltcornSession(3000)
email = "[email protected]"
password="AhGGr6rhu45"

# helpers
def cannot_access_admin():
sess.get('/table')
assert sess.status == 302
assert "Your tables" not in sess.content

def is_incorrect_user_or_password():
assert sess.redirect_url == '/auth/login'
sess.follow_redirect()
assert "Incorrect user or password" in sess.content


def test_public_cannot_access_admin():
sess.reset()
cannot_access_admin()

def test_can_login_as_admin():
sess.reset()
sess.get('/auth/login')
assert "Login" in sess.content
assert sess.status == 200
sess.postForm('/auth/login',
{'email': email,
'password': password,
'_csrf': sess.csrf()
})
assert sess.redirect_url == '/'
sess.get('/table')
assert sess.status == 200
assert "Your tables" in sess.content

def test_login_without_csrf():
sess.reset()
sess.get('/auth/login')
sess.postForm('/auth/login',
{'email': email,
'password': password,
})
assert sess.redirect_url == '/auth/login'
cannot_access_admin()

def test_login_with_wrong_csrf():
sess.reset()
sess.get('/auth/login')
sess.postForm('/auth/login',
{'email': email,
'password': password,
'_csrf': 'ytjutydetjk'
})
assert sess.redirect_url == '/auth/login'
cannot_access_admin()

def test_login_with_blank_csrf():
sess.reset()
sess.get('/auth/login')
sess.postForm('/auth/login',
{'email': email,
'password': password,
'_csrf': ''
})
assert sess.redirect_url == '/auth/login'
cannot_access_admin()

def test_login_with_wrong_password():
sess.reset()
sess.get('/auth/login')
sess.postForm('/auth/login',
{'email': email,
'password': 'fidelio',
'_csrf': sess.csrf()
})
is_incorrect_user_or_password()
cannot_access_admin()

def test_login_with_no_password():
sess.reset()
sess.get('/auth/login')
sess.postForm('/auth/login', {'email': email , '_csrf': sess.csrf()})
is_incorrect_user_or_password()
cannot_access_admin()

def test_login_with_no_email():
sess.reset()
sess.get('/auth/login')
sess.postForm('/auth/login', {'password': password, '_csrf': sess.csrf()})
is_incorrect_user_or_password()
cannot_access_admin()

def test_login_with_blank_email():
sess.reset()
sess.get('/auth/login')
sess.postForm('/auth/login', {'email':'', 'password': password, '_csrf': sess.csrf()})
is_incorrect_user_or_password()
cannot_access_admin()

def test_login_with_nothing():
sess.reset()
sess.get('/auth/login')
sess.postForm('/auth/login', {'_csrf': sess.csrf()})
is_incorrect_user_or_password()
cannot_access_admin()

def test_login_with_blank_password():
sess.reset()
sess.get('/auth/login')
sess.postForm('/auth/login', {'email': email,'password': '', '_csrf': sess.csrf()})
is_incorrect_user_or_password()
cannot_access_admin()
20 changes: 20 additions & 0 deletions infosec-scan/py-sectest/scsession.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from sectest import Session
import re
import subprocess

class SaltcornSession(Session):
def __init__(self, port=3000):
self.salcorn_process = subprocess.Popen(["packages/saltcorn-cli/bin/saltcorn", "serve", "-p", str(port)])
Session.__init__(self, 'http://localhost:%d/' % port)
self.wait_for_port_open()

def __del__(self):
self.salcorn_process.kill()

def csrf(self):
m = re.findall('_sc_globalCsrf = "([^"]*)"', self.content)
return m[0]

@staticmethod
def reset_to_fixtures():
subprocess.run(["packages/saltcorn-cli/bin/saltcorn", "fixtures", "-r"], check=True)
50 changes: 50 additions & 0 deletions infosec-scan/py-sectest/sectest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import requests
from urllib.parse import urljoin
from urllib.request import urlopen
import time

class Session:
def __init__(self, base_url):
self.base_url = base_url
self.reset()

def wait_for_port_open(self):
i=0
while i<30:
try:
response = urlopen(self.base_url,timeout=1)
return
except:
print("Closed, retry")
time.sleep(0.25)
i=i+1
pass
raise ValueError("wait_for_port_open: Iterations exceeded")

def __read_response(self, resp):
self.status = resp.status_code
self.content = resp.text
if self.status >= 300 and self.status <400:
ws = self.content.split()
self.redirect_url=ws[len(ws)-1]
else:
self.redirect_url = None

def get(self, url):
resp = self.session.get(urljoin(self.base_url, url), allow_redirects=False)
self.__read_response(resp)

def postForm(self, url, data):
resp = self.session.post(urljoin(self.base_url, url), data=data, allow_redirects=False)
self.__read_response(resp)

def follow_redirect(self):
self.get(self.redirect_url)


def reset(self):
self.status = None
self.content = None
self.redirect_url = None
self.session = requests.Session()

85 changes: 85 additions & 0 deletions infosec-scan/py-sectest/signup_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from scsession import SaltcornSession

SaltcornSession.reset_to_fixtures()
sess = SaltcornSession(3001)

# helpers
def cannot_access_admin():
sess.get('/table')
assert sess.status == 302
assert "Your tables" not in sess.content

def test_public_home_is_redirect():
sess.reset()
sess.get('/')
assert sess.redirect_url == '/auth/login'
cannot_access_admin()

def test_can_signup():
sess.reset()
sess.get('/auth/signup')
assert "Sign up" in sess.content
assert sess.status == 200
sess.postForm('/auth/signup',
{'email': '[email protected]',
'password': 'ty435y5OPtyj',
'_csrf': sess.csrf()
})
assert sess.redirect_url == '/'
sess.follow_redirect()
assert 'Welcome to Saltcorn!' in sess.content
cannot_access_admin()
sess.get('/auth/logout')
assert sess.redirect_url == '/auth/login'
sess.follow_redirect()
sess.postForm('/auth/login',
{'email': '[email protected]',
'password': 'ty435y5OPtyj',
'_csrf': sess.csrf()
})
assert sess.redirect_url == '/'

def test_cannot_signup_again():
sess.reset()
sess.get('/auth/signup')
sess.postForm('/auth/signup',
{'email': '[email protected]',
'password': 'ty435y5OPtyj',
'_csrf': sess.csrf()
})
assert sess.redirect_url == '/auth/signup'

def test_signup_no_csrf():
sess.reset()
sess.get('/auth/signup')
sess.postForm('/auth/signup',
{'email': '[email protected]',
'password': 'ty435yqpiOPtyj',
})
assert sess.redirect_url == '/'
sess.follow_redirect()
assert sess.redirect_url == '/auth/login'
sess.follow_redirect()
sess.postForm('/auth/login',
{'email': '[email protected]',
'password': 'ty435yqpiOPtyj',
'_csrf': sess.csrf()
})
assert sess.redirect_url == '/auth/login'
sess.follow_redirect()
assert "Incorrect user or password" in sess.content

def test_cannot_inject_role_id():
sess.reset()
sess.get('/auth/signup')
assert "Sign up" in sess.content
assert sess.status == 200
sess.postForm('/auth/signup',
{'email': '[email protected]',
'password': 'ty11y5OPtyj',
'role_id': '1',
'_csrf': sess.csrf()
})
assert sess.redirect_url == '/'
sess.follow_redirect()
cannot_access_admin()
2 changes: 2 additions & 0 deletions packages/saltcorn-cli/src/commands/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class FixturesCommand extends Command {
await reset();
}
await fixtures();
this.exit(0);

}
}

Expand Down
11 changes: 4 additions & 7 deletions packages/saltcorn-data/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class User {
if (urows.length !== 1) return false;
const [urow] = urows;
if (urow.disabled) return false;
const cmp = urow.checkPassword(password);
const cmp = urow.checkPassword(password || "");
if (cmp) return new User(urow);
else return false;
}
Expand Down Expand Up @@ -244,12 +244,9 @@ class User {
}
}
relogin(req) {
req.login(
this.session_object,
function (err) {
if (err) req.flash("danger", err);
}
);
req.login(this.session_object, function (err) {
if (err) req.flash("danger", err);
});
}
}

Expand Down

0 comments on commit d804ea1

Please sign in to comment.