Security:
- Add SecurityHeadersListener (X-Content-Type-Options, X-Frame-Options,
CSP, Referrer-Policy, HSTS)
- Replace document.write() with document.documentElement.innerHTML
in login JS to avoid CSP violations
- Add CSS escaping (|e('css')) to env color values in _style.html.twig
- Document CSRF protection model: nonce serves as CSRF token for POST
form path (single-use, server-generated, 120s TTL)
- Reduce TOTP verification window from 10 periods (±5 min) to 1 (±30s)
- Remove hardcoded APP_SECRET from bin/franken.sh (now uses env or
generates random)
- Remove backup code values from debug log output
- Add .env to .gitignore
Bug fixes:
- Fix ->json access on possibly-null in LoginListener
(uses null-safe operator ?->)
- Fix validReturn() not checking false from parse_url (could cause
TypeError on malformed URLs)
- Add isHit() race condition check in AcceptListener and AllowListener
- Add try/finally in Kernel::terminate() so parent::terminate() always
runs even if persist() throws
- Add input validation to GenerateBackupCodesCommand (reject count < 1)
- Use Response::HTTP_INTERNAL_SERVER_ERROR constant in GetTotpTrait
instead of literal 500
Docker/CI:
- Explicitly install curl in Docker final image (needed for healthcheck)
- Update workflow tag pattern to v*.*.* (standardize on v-prefix)
- Extract version without v-prefix for Docker image tag
- Remove stale develop branch from CI triggers
- Fix publish.yaml git remote add to use set-url on re-runs
Code quality:
- Add declare(strict_types=1) to all interface files
- Add #[AsCommand] attribute to GenerateBackupCodesCommand
- Fix BackupCodeInterface default count to match implementation (10)
- Lowercase host before TLD lookup in DomainManager
- Expand TLD list with many missing multi-part TLDs (.com.au, .co.jp,
.com.br, .co.kr, .com.tw, .co.za, etc.) to prevent open redirect
vulnerabilities
- Disable unused Symfony sessions in framework.yaml
Tests:
- Update DomainManagerTest for corrected TLD parsing (.com.au, .co.jp,
.com.br now correctly recognized as multi-part)
- Update GetTotpTraitTest for corrected error message
- Update GenerateBackupCodesCommandTest: zero count now throws exception
83 lines
3.2 KiB
Twig
83 lines
3.2 KiB
Twig
<script>
|
|
const form = document.getElementById('preauth-form');
|
|
const message = document.getElementById('preauth-message');
|
|
const body = document.getElementById('preauth-body');
|
|
const style = document.getElementById('preauth-style');
|
|
|
|
form.addEventListener('submit', (event) => {
|
|
event.preventDefault();
|
|
|
|
{# make base64url string containing our payload json object #}
|
|
const data = btoa(JSON.stringify({
|
|
id: form.username.value?.trim() ?? '',
|
|
token: form.totp.value?.trim() ?? '',
|
|
nonce: form.nonce.value?.trim() ?? '',
|
|
json: true
|
|
})).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
|
|
{# send our request to the server #}
|
|
fetch(window.location.href, {
|
|
method: 'GET',
|
|
headers: { 'X-Preauth': data },
|
|
}).then((response) => {
|
|
{% if env.debug > 2 -%}
|
|
console.log(response);
|
|
{% endif -%}
|
|
if (response.headers.has('Location')) {
|
|
{# follow redirect (probably not needed) #}
|
|
{% if env.debug > 2 -%}
|
|
console.log('got redirect response');
|
|
{% endif -%}
|
|
window.location.href = response.headers.get('Location');
|
|
} else if (response.headers.get('Content-Type')?.toLowerCase().includes('application/json') ?? false) {
|
|
{# got json, update the page #}
|
|
{% if env.debug > 2 -%}
|
|
console.log('got json response');
|
|
{% endif -%}
|
|
response.json().then((content) => {
|
|
if (Object.hasOwn(content, 'message')) {
|
|
message.innerText = content.message;
|
|
}
|
|
if (Object.hasOwn(content, 'nonce')) {
|
|
form.nonce.value = content.nonce;
|
|
form.totp.value = '';
|
|
form.totp.focus();
|
|
}
|
|
}).catch((error) => {
|
|
console.log('failed to parse json from response');
|
|
console.log(error);
|
|
});
|
|
} else if (response.headers.get('Content-Type')?.toLowerCase().includes('text/html') ?? false) {
|
|
{# got html, replace the page #}
|
|
{% if env.debug > 2 -%}
|
|
console.log('got html response');
|
|
{% endif -%}
|
|
response.text().then((html) => {
|
|
document.documentElement.innerHTML = html;
|
|
}).catch((error) => {
|
|
console.log('failed to get html from response');
|
|
console.log(error);
|
|
});
|
|
} else {
|
|
{# non-json, non-html, non-redirect response #}
|
|
{# update the page, change style to plain text #}
|
|
{% if env.debug > 2 -%}
|
|
console.log('got misc response');
|
|
{% endif -%}
|
|
response.text().then((text) => {
|
|
body.innerText = text;
|
|
style.disabled = true;
|
|
body.style.whiteSpace = 'pre-wrap';
|
|
body.style.wordWrap = 'break-word';
|
|
}).catch((error) => {
|
|
console.log('failed to get text from response');
|
|
console.log(error);
|
|
});
|
|
}
|
|
}).catch((error) => {
|
|
console.log('failed to get response');
|
|
console.log(error);
|
|
});
|
|
});
|
|
</script>
|