Skip to content

Commit b08afb0

Browse files
security: fix XSS and improve robustness in status page
- Add escapeHtml() function to sanitize all API response data - Add null safety with optional chaining and defaults - Improve error handling with CORS-specific hints - Add API URL override via query param for local testing - Remove broken link to API reference, document endpoint inline
1 parent ed6efe3 commit b08afb0

1 file changed

Lines changed: 64 additions & 35 deletions

File tree

docs/osa/status.md

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,21 @@
112112
</style>
113113

114114
<script>
115-
// API endpoint - configure based on deployment
116-
const API_BASE = 'https://api.osc.earth/osa';
115+
// API endpoint - allow override via query param for development/testing
116+
// Usage: ?api=http://localhost:38528 for local testing
117+
const urlParams = new URLSearchParams(window.location.search);
118+
const API_BASE = urlParams.get('api') || 'https://api.osc.earth/osa';
119+
120+
// HTML escape to prevent XSS from API response data
121+
function escapeHtml(str) {
122+
if (str === null || str === undefined) return '';
123+
return String(str)
124+
.replace(/&/g, '&amp;')
125+
.replace(/</g, '&lt;')
126+
.replace(/>/g, '&gt;')
127+
.replace(/"/g, '&quot;')
128+
.replace(/'/g, '&#039;');
129+
}
117130

118131
async function fetchStatus() {
119132
const container = document.getElementById('status-container');
@@ -134,15 +147,19 @@ async function fetchStatus() {
134147

135148
function formatDate(isoString) {
136149
if (!isoString) return 'Never';
137-
const date = new Date(isoString);
138-
return date.toLocaleString('en-US', {
139-
year: 'numeric',
140-
month: 'short',
141-
day: 'numeric',
142-
hour: '2-digit',
143-
minute: '2-digit',
144-
timeZoneName: 'short'
145-
});
150+
try {
151+
const date = new Date(isoString);
152+
return date.toLocaleString('en-US', {
153+
year: 'numeric',
154+
month: 'short',
155+
day: 'numeric',
156+
hour: '2-digit',
157+
minute: '2-digit',
158+
timeZoneName: 'short'
159+
});
160+
} catch {
161+
return 'Invalid date';
162+
}
146163
}
147164

148165
function formatAge(hours) {
@@ -164,7 +181,10 @@ function getHealthText(healthy, ageHours) {
164181
}
165182

166183
function renderStatus(container, data) {
167-
const { github, papers, scheduler, health } = data;
184+
const github = data?.github || {};
185+
const papers = data?.papers || {};
186+
const scheduler = data?.scheduler || {};
187+
const health = data?.health || {};
168188

169189
container.innerHTML = `
170190
<div class="status-grid">
@@ -175,30 +195,30 @@ function renderStatus(container, data) {
175195
</h3>
176196
<div class="status-details">
177197
<span class="status-label">Status:</span>
178-
<span class="status-value">${getHealthText(health.github_healthy, health.github_age_hours)}</span>
198+
<span class="status-value">${escapeHtml(getHealthText(health.github_healthy, health.github_age_hours))}</span>
179199
180200
<span class="status-label">Last sync:</span>
181-
<span class="status-value">${formatAge(health.github_age_hours)}</span>
201+
<span class="status-value">${escapeHtml(formatAge(health.github_age_hours))}</span>
182202
183203
<span class="status-label">Total items:</span>
184-
<span class="status-value">${github.total_items.toLocaleString()}</span>
204+
<span class="status-value">${(github.total_items ?? 0).toLocaleString()}</span>
185205
186206
<span class="status-label">Issues:</span>
187-
<span class="status-value">${github.issues.toLocaleString()}</span>
207+
<span class="status-value">${(github.issues ?? 0).toLocaleString()}</span>
188208
189209
<span class="status-label">Pull Requests:</span>
190-
<span class="status-value">${github.prs.toLocaleString()}</span>
210+
<span class="status-value">${(github.prs ?? 0).toLocaleString()}</span>
191211
192212
<span class="status-label">Open items:</span>
193-
<span class="status-value">${github.open_items.toLocaleString()}</span>
213+
<span class="status-value">${(github.open_items ?? 0).toLocaleString()}</span>
194214
</div>
195215
196216
<div class="repo-list">
197217
<strong>Repositories:</strong>
198-
${Object.entries(github.repos).map(([repo, info]) => `
218+
${Object.entries(github.repos || {}).map(([repo, info]) => `
199219
<div class="repo-item">
200-
${repo}: ${info.items} items
201-
${info.last_sync ? `(synced ${formatDate(info.last_sync)})` : ''}
220+
${escapeHtml(repo)}: ${(info?.items ?? 0)} items
221+
${info?.last_sync ? `(synced ${escapeHtml(formatDate(info.last_sync))})` : ''}
202222
</div>
203223
`).join('')}
204224
</div>
@@ -211,21 +231,21 @@ function renderStatus(container, data) {
211231
</h3>
212232
<div class="status-details">
213233
<span class="status-label">Status:</span>
214-
<span class="status-value">${getHealthText(health.papers_healthy, health.papers_age_hours)}</span>
234+
<span class="status-value">${escapeHtml(getHealthText(health.papers_healthy, health.papers_age_hours))}</span>
215235
216236
<span class="status-label">Last sync:</span>
217-
<span class="status-value">${formatAge(health.papers_age_hours)}</span>
237+
<span class="status-value">${escapeHtml(formatAge(health.papers_age_hours))}</span>
218238
219239
<span class="status-label">Total papers:</span>
220-
<span class="status-value">${papers.total_items.toLocaleString()}</span>
240+
<span class="status-value">${(papers.total_items ?? 0).toLocaleString()}</span>
221241
</div>
222242
223243
<div class="source-list">
224244
<strong>Sources:</strong>
225-
${Object.entries(papers.sources).map(([source, info]) => `
245+
${Object.entries(papers.sources || {}).map(([source, info]) => `
226246
<div class="source-item">
227-
${source}: ${info.items} papers
228-
${info.last_sync ? `(synced ${formatDate(info.last_sync)})` : ''}
247+
${escapeHtml(source)}: ${(info?.items ?? 0)} papers
248+
${info?.last_sync ? `(synced ${escapeHtml(formatDate(info.last_sync))})` : ''}
229249
</div>
230250
`).join('')}
231251
</div>
@@ -241,38 +261,44 @@ function renderStatus(container, data) {
241261
<span class="status-value">${scheduler.running ? 'Yes' : 'No'}</span>
242262
243263
<span class="status-label">GitHub schedule:</span>
244-
<span class="status-value">${scheduler.github_cron} (UTC)</span>
264+
<span class="status-value">${escapeHtml(scheduler.github_cron ?? 'Not configured')} (UTC)</span>
245265
246266
<span class="status-label">Papers schedule:</span>
247-
<span class="status-value">${scheduler.papers_cron} (UTC)</span>
267+
<span class="status-value">${escapeHtml(scheduler.papers_cron ?? 'Not configured')} (UTC)</span>
248268
249269
${scheduler.next_github_sync ? `
250270
<span class="status-label">Next GitHub sync:</span>
251-
<span class="status-value">${formatDate(scheduler.next_github_sync)}</span>
271+
<span class="status-value">${escapeHtml(formatDate(scheduler.next_github_sync))}</span>
252272
` : ''}
253273
254274
${scheduler.next_papers_sync ? `
255275
<span class="status-label">Next papers sync:</span>
256-
<span class="status-value">${formatDate(scheduler.next_papers_sync)}</span>
276+
<span class="status-value">${escapeHtml(formatDate(scheduler.next_papers_sync))}</span>
257277
` : ''}
258278
</div>
259279
</div>
260280
</div>
261281
262282
<div class="last-updated">
263-
Last checked: ${new Date().toLocaleString()}
283+
Last checked: ${escapeHtml(new Date().toLocaleString())}
264284
<br>
265285
<button class="refresh-btn" onclick="refreshStatus()">Refresh</button>
266286
</div>
267287
`;
268288
}
269289
270290
function renderError(container, error) {
291+
// Detect likely CORS or network issues
292+
let hint = 'The API may be temporarily unavailable. Try again later.';
293+
if (error instanceof TypeError && error.message.includes('fetch')) {
294+
hint = 'Unable to reach the API. This may be a network or CORS issue.';
295+
}
296+
271297
container.innerHTML = `
272298
<div class="status-error">
273299
<strong>Unable to fetch status</strong>
274-
<p>${error.message}</p>
275-
<p>The API may be temporarily unavailable. Try again later.</p>
300+
<p>${escapeHtml(error.message)}</p>
301+
<p>${escapeHtml(hint)}</p>
276302
<button class="refresh-btn" onclick="refreshStatus()">Retry</button>
277303
</div>
278304
`;
@@ -320,4 +346,7 @@ The status data is fetched from the OSA API:
320346
GET /sync/status
321347
```
322348
323-
See the [API Reference](api-reference.md) for more details.
349+
Returns JSON with `github`, `papers`, `scheduler`, and `health` objects.
350+
351+
!!! tip "Local Testing"
352+
To test with a local API server, add `?api=http://localhost:38528` to this page's URL.

0 commit comments

Comments
 (0)