Reworked sensitive domain name and restriction logic (#878)
* This is a fix for a potential security issue; the old code had edge cases with unexpected behavior. * LEMUR_RESTRICTED_DOMAINS is no more, instead LEMUR_WHITELISTED_DOMAINS is a list of *allowed* domain name patterns. Per discussion in PR #600 * Domain restrictions are now checked everywhere: in domain name-like CN (common name) values and SAN DNSNames, including raw CSR requests. * Common name values that contain a space are exempt, since they cannot be valid domain names.
This commit is contained in:
@ -21,8 +21,10 @@ SECRET_KEY = 'I/dVhOZNSMZMqrFJa5tWli6VQccOGudKerq3eWPMSzQNmHHVhMAQfQ=='
|
||||
LEMUR_TOKEN_SECRET = 'test'
|
||||
LEMUR_ENCRYPTION_KEYS = 'o61sBLNBSGtAckngtNrfVNd8xy8Hp9LBGDstTbMbqCY='
|
||||
|
||||
# this is a list of domains as regexes that only admins can issue
|
||||
LEMUR_RESTRICTED_DOMAINS = []
|
||||
# List of domain regular expressions that non-admin users can issue
|
||||
LEMUR_WHITELISTED_DOMAINS = [
|
||||
'^[a-zA-Z0-9-]+\.example\.com$'
|
||||
]
|
||||
|
||||
# Mail Server
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import os
|
||||
import pytest
|
||||
from flask import current_app
|
||||
from flask_principal import identity_changed, Identity
|
||||
|
||||
from lemur import create_app
|
||||
from lemur.database import db as _db
|
||||
@ -176,3 +178,17 @@ def source_plugin():
|
||||
from .plugins.source_plugin import TestSourcePlugin
|
||||
register(TestSourcePlugin)
|
||||
return TestSourcePlugin
|
||||
|
||||
|
||||
@pytest.yield_fixture(scope="function")
|
||||
def logged_in_user(session, app):
|
||||
with app.test_request_context():
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(1))
|
||||
yield
|
||||
|
||||
|
||||
@pytest.yield_fixture(scope="function")
|
||||
def logged_in_admin(session, app):
|
||||
with app.test_request_context():
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(2))
|
||||
yield
|
||||
|
@ -1,18 +1,18 @@
|
||||
|
||||
import pytest
|
||||
from lemur.authorities.views import * # noqa
|
||||
|
||||
from lemur.authorities.views import * # noqa
|
||||
from lemur.tests.vectors import VALID_ADMIN_HEADER_TOKEN, VALID_USER_HEADER_TOKEN
|
||||
|
||||
|
||||
def test_authority_input_schema(client, role, issuer_plugin):
|
||||
def test_authority_input_schema(client, role, issuer_plugin, logged_in_user):
|
||||
from lemur.authorities.schemas import AuthorityInputSchema
|
||||
|
||||
input_data = {
|
||||
'name': 'Example Authority',
|
||||
'owner': 'jim@example.com',
|
||||
'description': 'An example authority.',
|
||||
'commonName': 'AnExampleAuthority',
|
||||
'commonName': 'An Example Authority',
|
||||
'plugin': {'slug': 'test-issuer', 'plugin_options': [{'name': 'test', 'value': 'blah'}]},
|
||||
'type': 'root',
|
||||
'signingAlgorithm': 'sha256WithRSA',
|
||||
|
@ -6,9 +6,13 @@ import json
|
||||
import arrow
|
||||
import pytest
|
||||
from cryptography import x509
|
||||
from marshmallow import ValidationError
|
||||
from freezegun import freeze_time
|
||||
|
||||
from lemur.certificates.views import * # noqa
|
||||
from lemur.domains.models import Domain
|
||||
|
||||
|
||||
from lemur.tests.vectors import VALID_ADMIN_HEADER_TOKEN, VALID_USER_HEADER_TOKEN, CSR_STR, \
|
||||
INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR, PRIVATE_KEY_STR
|
||||
|
||||
@ -241,6 +245,89 @@ def test_certificate_valid_dates(client, authority):
|
||||
assert not errors
|
||||
|
||||
|
||||
def test_certificate_cn_admin(client, authority, logged_in_admin):
|
||||
"""Admin is exempt from CN/SAN domain restrictions."""
|
||||
from lemur.certificates.schemas import CertificateInputSchema
|
||||
input_data = {
|
||||
'commonName': '*.admin-overrides-whitelist.com',
|
||||
'owner': 'jim@example.com',
|
||||
'authority': {'id': authority.id},
|
||||
'description': 'testtestest',
|
||||
'validityStart': '2020-01-01T00:00:00',
|
||||
'validityEnd': '2020-01-01T00:00:01',
|
||||
}
|
||||
|
||||
data, errors = CertificateInputSchema().load(input_data)
|
||||
assert not errors
|
||||
|
||||
|
||||
def test_certificate_allowed_names(client, authority, session, logged_in_user):
|
||||
"""Test for allowed CN and SAN values."""
|
||||
from lemur.certificates.schemas import CertificateInputSchema
|
||||
input_data = {
|
||||
'commonName': 'Names with spaces are not checked',
|
||||
'owner': 'jim@example.com',
|
||||
'authority': {'id': authority.id},
|
||||
'description': 'testtestest',
|
||||
'validityStart': '2020-01-01T00:00:00',
|
||||
'validityEnd': '2020-01-01T00:00:01',
|
||||
'extensions': {
|
||||
'subAltNames': {
|
||||
'names': [
|
||||
{'nameType': 'DNSName', 'value': 'allowed.example.com'},
|
||||
{'nameType': 'IPAddress', 'value': '127.0.0.1'},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data, errors = CertificateInputSchema().load(input_data)
|
||||
assert not errors
|
||||
|
||||
|
||||
def test_certificate_disallowed_names(client, authority, session, logged_in_user):
|
||||
"""The CN and SAN are disallowed by LEMUR_WHITELISTED_DOMAINS."""
|
||||
from lemur.certificates.schemas import CertificateInputSchema
|
||||
input_data = {
|
||||
'commonName': '*.example.com',
|
||||
'owner': 'jim@example.com',
|
||||
'authority': {'id': authority.id},
|
||||
'description': 'testtestest',
|
||||
'validityStart': '2020-01-01T00:00:00',
|
||||
'validityEnd': '2020-01-01T00:00:01',
|
||||
'extensions': {
|
||||
'subAltNames': {
|
||||
'names': [
|
||||
{'nameType': 'DNSName', 'value': 'allowed.example.com'},
|
||||
{'nameType': 'DNSName', 'value': 'evilhacker.org'},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data, errors = CertificateInputSchema().load(input_data)
|
||||
assert errors['common_name'][0].startswith("Domain *.example.com does not match whitelisted domain patterns")
|
||||
assert (errors['extensions']['sub_alt_names']['names'][0]
|
||||
.startswith("Domain evilhacker.org does not match whitelisted domain patterns"))
|
||||
|
||||
|
||||
def test_certificate_sensitive_name(client, authority, session, logged_in_user):
|
||||
"""The CN is disallowed by 'sensitive' flag on Domain model."""
|
||||
from lemur.certificates.schemas import CertificateInputSchema
|
||||
input_data = {
|
||||
'commonName': 'sensitive.example.com',
|
||||
'owner': 'jim@example.com',
|
||||
'authority': {'id': authority.id},
|
||||
'description': 'testtestest',
|
||||
'validityStart': '2020-01-01T00:00:00',
|
||||
'validityEnd': '2020-01-01T00:00:01',
|
||||
}
|
||||
session.add(Domain(name='sensitive.example.com', sensitive=True))
|
||||
|
||||
data, errors = CertificateInputSchema().load(input_data)
|
||||
assert errors['common_name'][0].startswith("Domain sensitive.example.com has been marked as sensitive")
|
||||
|
||||
|
||||
def test_create_basic_csr(client):
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
@ -263,6 +350,37 @@ def test_create_basic_csr(client):
|
||||
assert name.value in csr_config.values()
|
||||
|
||||
|
||||
def test_csr_disallowed_cn(client, logged_in_user):
|
||||
"""Domain name CN is disallowed via LEMUR_WHITELISTED_DOMAINS."""
|
||||
from lemur.certificates.service import create_csr
|
||||
from lemur.common import validators
|
||||
|
||||
request, pkey = create_csr(
|
||||
common_name='evilhacker.org',
|
||||
owner='joe@example.com',
|
||||
key_type='RSA2048',
|
||||
)
|
||||
with pytest.raises(ValidationError) as err:
|
||||
validators.csr(request)
|
||||
assert str(err.value).startswith('Domain evilhacker.org does not match whitelisted domain patterns')
|
||||
|
||||
|
||||
def test_csr_disallowed_san(client, logged_in_user):
|
||||
"""SAN name is disallowed by LEMUR_WHITELISTED_DOMAINS."""
|
||||
from lemur.certificates.service import create_csr
|
||||
from lemur.common import validators
|
||||
|
||||
request, pkey = create_csr(
|
||||
common_name="CN with spaces isn't a domain and is thus allowed",
|
||||
owner='joe@example.com',
|
||||
key_type='RSA2048',
|
||||
extensions={'sub_alt_names': {'names': x509.SubjectAlternativeName([x509.DNSName('evilhacker.org')])}}
|
||||
)
|
||||
with pytest.raises(ValidationError) as err:
|
||||
validators.csr(request)
|
||||
assert str(err.value).startswith('Domain evilhacker.org does not match whitelisted domain patterns')
|
||||
|
||||
|
||||
def test_get_name_from_arn(client):
|
||||
from lemur.certificates.service import get_name_from_arn
|
||||
arn = 'arn:aws:iam::11111111:server-certificate/mycertificate'
|
||||
|
Reference in New Issue
Block a user