hearth-dash 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/package.json +1 -1
- package/worker.js +27 -4
package/README.md
CHANGED
|
@@ -126,6 +126,10 @@ Version 1.1.2 accepts Chrome's legitimate `Origin: null` on a form submission on
|
|
|
126
126
|
|
|
127
127
|
Version 1.1.3 replaces the OAuth consent page's double-submit CSRF cookie with a short-lived, one-time D1 token bound to the complete authorization request and consumed atomically. This avoids browser cookie-context failures during Railway/Claude handoff without weakening same-origin checks or replay protection.
|
|
128
128
|
|
|
129
|
+
### 1.1.4 OAuth callback CSP fix
|
|
130
|
+
|
|
131
|
+
Version 1.1.4 gives each authorization page a narrow `form-action` policy containing only Hearth itself and that request's already validated HTTP(S) OAuth callback origin. This allows the browser to follow the successful POST redirect without permitting arbitrary form destinations.
|
|
132
|
+
|
|
129
133
|
## Security notes
|
|
130
134
|
|
|
131
135
|
- There are no functional default credentials, bearer tokens or secret-bearing connector URLs.
|
package/package.json
CHANGED
package/worker.js
CHANGED
|
@@ -68,9 +68,11 @@ async function verifySession(token, secret) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
const CSP_BASE = "default-src 'self'; img-src 'self' data: https://openweathermap.org; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'";
|
|
72
|
+
|
|
71
73
|
function securityHeaders(extra = {}) {
|
|
72
74
|
return {
|
|
73
|
-
'Content-Security-Policy':
|
|
75
|
+
'Content-Security-Policy': `${CSP_BASE}; form-action 'self'`,
|
|
74
76
|
'Referrer-Policy': 'no-referrer',
|
|
75
77
|
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
|
|
76
78
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -80,6 +82,21 @@ function securityHeaders(extra = {}) {
|
|
|
80
82
|
};
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
function oauthCallbackCspSource(redirectUri) {
|
|
86
|
+
const redirect = new URL(redirectUri);
|
|
87
|
+
if (!['http:', 'https:'].includes(redirect.protocol) || redirect.origin === 'null') {
|
|
88
|
+
throw new Error('Unsupported OAuth redirect URI scheme');
|
|
89
|
+
}
|
|
90
|
+
return redirect.origin;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function oauthConsentHeaders(callbackSource, extra = {}) {
|
|
94
|
+
return securityHeaders({
|
|
95
|
+
...extra,
|
|
96
|
+
'Content-Security-Policy': `${CSP_BASE}; form-action 'self' ${callbackSource}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
function sessionCookie(value, maxAge = 604800) {
|
|
84
101
|
return `__Host-hearth_session=${value}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=${maxAge}`;
|
|
85
102
|
}
|
|
@@ -747,7 +764,7 @@ async function consumeOAuthCsrf(env, oauthRequest, token) {
|
|
|
747
764
|
function validAuthorizationParameterMultiplicity(url) {
|
|
748
765
|
const singleton = [
|
|
749
766
|
'response_type', 'client_id', 'redirect_uri', 'scope', 'state',
|
|
750
|
-
'code_challenge', 'code_challenge_method',
|
|
767
|
+
'code_challenge', 'code_challenge_method', 'resource',
|
|
751
768
|
];
|
|
752
769
|
return singleton.every(name => url.searchParams.getAll(name).length <= 1);
|
|
753
770
|
}
|
|
@@ -805,6 +822,12 @@ export const oauthDefaultHandler = {
|
|
|
805
822
|
}
|
|
806
823
|
const client = await env.OAUTH_PROVIDER.lookupClient(oauthRequest.clientId);
|
|
807
824
|
if (!client) return new Response('Unknown OAuth client', { status: 400, headers: securityHeaders() });
|
|
825
|
+
let callbackCspSource;
|
|
826
|
+
try {
|
|
827
|
+
callbackCspSource = oauthCallbackCspSource(oauthRequest.redirectUri);
|
|
828
|
+
} catch {
|
|
829
|
+
return new Response('Unsupported OAuth callback scheme', { status: 400, headers: securityHeaders({ 'Cache-Control': 'no-store' }) });
|
|
830
|
+
}
|
|
808
831
|
|
|
809
832
|
const sessionToken = getCookie(request, '__Host-hearth_session');
|
|
810
833
|
const signedIn = await verifySession(sessionToken, config.SESSION_SECRET);
|
|
@@ -819,7 +842,7 @@ export const oauthDefaultHandler = {
|
|
|
819
842
|
} catch {
|
|
820
843
|
return csrfStorageUnavailable();
|
|
821
844
|
}
|
|
822
|
-
const headers = new Headers(
|
|
845
|
+
const headers = new Headers(oauthConsentHeaders(callbackCspSource, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }));
|
|
823
846
|
return new Response(renderOAuthConsentPage(request.url, oauthRequest, client, csrf, signedIn), { headers });
|
|
824
847
|
}
|
|
825
848
|
|
|
@@ -861,7 +884,7 @@ export const oauthDefaultHandler = {
|
|
|
861
884
|
} catch {
|
|
862
885
|
return csrfStorageUnavailable();
|
|
863
886
|
}
|
|
864
|
-
const headers = new Headers(
|
|
887
|
+
const headers = new Headers(oauthConsentHeaders(callbackCspSource, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }));
|
|
865
888
|
return new Response(renderOAuthConsentPage(request.url, oauthRequest, client, csrf, false, 'Wrong dashboard password'), { status: 401, headers });
|
|
866
889
|
}
|
|
867
890
|
|