fix: preserve browser form origins for update panel authentication

This commit is contained in:
Codex
2026-10-24 14:49:43 +08:00
parent 2f11a8c904
commit 9a198fe466
4 changed files with 76 additions and 1 deletions
@@ -0,0 +1,40 @@
// Run against a fresh, disposable HTTPS update server. Never use production credentials.
const assert = require('node:assert/strict');
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
(async () => {
const base = process.env.UPDATE_SMOKE_ORIGIN;
assert(base && base.startsWith('https://127.0.0.1:'), 'Use a local disposable HTTPS server');
const browser = await chromium.launch({ channel: 'msedge', headless: true });
try {
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
await page.goto(base + '/update');
await page.locator('input[name="username"]').fill('admin');
await page.locator('input[name="password"]').fill('only-for-tests-password');
const loginResponse = page.waitForResponse(r => r.url() === base + '/update/login');
await page.locator('form[action="/update/login"] button').click();
const login = await loginResponse;
const headers = await login.request().allHeaders();
console.log(JSON.stringify({ loginStatus: login.status(), browserOrigin: headers.origin }));
assert.equal(login.status(), 303, 'Real browser form login must succeed');
assert.equal(headers.origin, base, 'Same-origin forms must preserve their Origin');
await page.locator('input[name="version"]').fill('0.8.0');
await page.locator('input[name="build_number"]').fill('25');
await page.locator('textarea[name="notes"]').fill('浏览器表单回归验证');
await page.locator('input[name="download_url"]').fill('https://example.com/test.apk');
const publishResponse = page.waitForResponse(r => r.url() === base + '/update/publish');
await page.locator('form[action="/update/publish"] button').click();
assert.equal((await publishResponse).status(), 303, 'Browser publishing must succeed');
const manifest = await (await context.request.get(base + '/v1/updates/android')).json();
assert.equal(manifest.latest.build_number, 25);
assert.equal(manifest.latest.notes, '浏览器表单回归验证');
const logoutResponse = page.waitForResponse(r => r.url() === base + '/update/logout');
await page.locator('form[action="/update/logout"] button').click();
assert.equal((await logoutResponse).status(), 303, 'Browser logout must succeed');
await page.locator('input[name="password"]').waitFor();
console.log('Real HTTPS browser login, publication and logout passed.');
} finally {
await browser.close();
}
})().catch(error => { console.error(error); process.exitCode = 1; });
+17
View File
@@ -101,6 +101,23 @@ def test_csrf_and_cross_origin_login_publish_logout_are_rejected(service):
assert client.post('/update/logout', data={'csrf': 'bad'}, headers={'Origin': BASE}).status_code == 403
@pytest.mark.parametrize('origin', [None, 'null', 'https://evil.test'])
def test_valid_csrf_does_not_allow_missing_null_or_foreign_origin(service, origin):
client, _ = service
headers = {} if origin is None else {'Origin': origin}
page = client.get('/update')
values = {'username': 'admin', 'password': PASSWORD, 'csrf': csrf(page)}
assert client.post('/update/login', data=values, headers=headers).status_code == 403
assert login(client).status_code == 303
page = client.get('/update')
values = {'csrf': csrf(page), 'version': '0.8.0', 'build_number': '25',
'notes': '不得发布', 'download_url': BASE + '/test.apk', 'revision': 'none'}
assert client.post('/update/publish', data=values, headers=headers).status_code == 403
assert client.get('/v1/updates/android').json()['latest'] is None
assert client.post('/update/logout', data={'csrf': csrf(page)}, headers=headers).status_code == 403
assert 'name="download_url"' in client.get('/update').text
def test_login_rejects_unicode_username_and_limits_password_guesses(service):
client, _ = service
page = client.get('/update')