fix(audit): durcir les validations de securite
This commit is contained in:
+16
-1
@@ -31,6 +31,14 @@ INLINE_HANDLER = re.compile(
|
||||
re.I,
|
||||
)
|
||||
|
||||
# An event attribute assembled inside a JavaScript string is absent from the
|
||||
# template DOM, so the expression above cannot see it. Once assigned through
|
||||
# innerHTML it is still an inline handler and the CSP still refuses to run it.
|
||||
DYNAMIC_INLINE_HANDLER = re.compile(
|
||||
r'''["']on(?:click|change|submit|input|load|keyup|keydown|mouseover|focus|blur)\s*=''',
|
||||
re.I,
|
||||
)
|
||||
|
||||
#: Remaining inline handlers, per template. Lower these as you migrate;
|
||||
#: never raise one. Templates absent from this map must have none.
|
||||
#: No template may carry an inline event handler. The migration is done;
|
||||
@@ -52,7 +60,8 @@ def _templates():
|
||||
|
||||
def _count_handlers(path):
|
||||
with open(path, encoding='utf-8') as handle:
|
||||
return len(INLINE_HANDLER.findall(handle.read()))
|
||||
content = handle.read()
|
||||
return len(INLINE_HANDLER.findall(content)) + len(DYNAMIC_INLINE_HANDLER.findall(content))
|
||||
|
||||
|
||||
class TestPolicyHeader:
|
||||
@@ -102,6 +111,12 @@ class TestPolicyHeader:
|
||||
|
||||
|
||||
class TestInlineHandlerRatchet:
|
||||
def test_a_handler_built_inside_a_javascript_string_is_counted(self, tmp_path):
|
||||
template = tmp_path / 'dynamic-handler.html'
|
||||
template.write_text("html += '<button onclick=\"work()\">';", encoding='utf-8')
|
||||
|
||||
assert _count_handlers(template) == 1
|
||||
|
||||
@pytest.mark.parametrize('relative,full', list(_templates()))
|
||||
def test_a_template_never_gains_an_inline_handler(self, relative, full):
|
||||
allowed = HANDLER_BUDGET.get(relative, 0)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""The security scanner must fail closed when its dependency audit cannot run."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import urllib.error
|
||||
|
||||
from app.supporting_scripts import security_scan
|
||||
|
||||
|
||||
def _result(returncode, stdout='', stderr=''):
|
||||
return subprocess.CompletedProcess([], returncode, stdout=stdout, stderr=stderr)
|
||||
|
||||
|
||||
def test_debug_mode_fails_the_environment_check(monkeypatch):
|
||||
monkeypatch.setenv('SECRET_KEY', 'x' * 32)
|
||||
monkeypatch.setenv('DATABASE_URL', 'sqlite:///:memory:')
|
||||
monkeypatch.setenv('FLASK_DEBUG', 'true')
|
||||
|
||||
assert security_scan.check_environment() is False
|
||||
|
||||
|
||||
def test_a_missing_database_url_fails_the_environment_check(monkeypatch):
|
||||
monkeypatch.setenv('SECRET_KEY', 'x' * 32)
|
||||
monkeypatch.delenv('DATABASE_URL', raising=False)
|
||||
monkeypatch.setenv('FLASK_DEBUG', 'false')
|
||||
|
||||
assert security_scan.check_environment() is False
|
||||
|
||||
|
||||
def test_an_unreachable_http_target_cannot_report_success(monkeypatch):
|
||||
def unreachable(*args, **kwargs):
|
||||
raise urllib.error.URLError('connection refused')
|
||||
|
||||
monkeypatch.setattr(security_scan.urllib.request, 'urlopen', unreachable)
|
||||
|
||||
assert security_scan.check_https_headers('https://example.test') is False
|
||||
|
||||
|
||||
def test_a_clean_dependency_audit_passes(monkeypatch):
|
||||
command = []
|
||||
|
||||
def clean_audit(args, **kwargs):
|
||||
command.extend(args)
|
||||
return _result(0)
|
||||
|
||||
monkeypatch.setattr(
|
||||
security_scan.subprocess,
|
||||
'run',
|
||||
clean_audit,
|
||||
)
|
||||
|
||||
assert security_scan.check_dependencies() is True
|
||||
requirement_flag = command.index('--requirement')
|
||||
assert command[requirement_flag + 1] == str(security_scan.REQUIREMENTS_FILE)
|
||||
|
||||
|
||||
def test_reported_vulnerabilities_fail_the_scan(monkeypatch):
|
||||
report = {
|
||||
'dependencies': [
|
||||
{
|
||||
'name': 'example',
|
||||
'version': '1.0',
|
||||
'vulns': [{'id': 'PYSEC-TEST'}],
|
||||
}
|
||||
]
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
security_scan.subprocess,
|
||||
'run',
|
||||
lambda *args, **kwargs: _result(1, stdout=json.dumps(report)),
|
||||
)
|
||||
|
||||
assert security_scan.check_dependencies() is False
|
||||
|
||||
|
||||
def test_an_audit_that_crashes_cannot_report_success(monkeypatch, capsys):
|
||||
monkeypatch.setattr(
|
||||
security_scan.subprocess,
|
||||
'run',
|
||||
lambda *args, **kwargs: _result(1, stderr='audit unavailable'),
|
||||
)
|
||||
|
||||
assert security_scan.check_dependencies() is False
|
||||
assert '[FAIL] audit unavailable' in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_an_audit_timeout_cannot_report_success(monkeypatch):
|
||||
def timeout(*args, **kwargs):
|
||||
raise subprocess.TimeoutExpired('pip-audit', 60)
|
||||
|
||||
monkeypatch.setattr(security_scan.subprocess, 'run', timeout)
|
||||
|
||||
assert security_scan.check_dependencies() is False
|
||||
Reference in New Issue
Block a user