Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1c234bf
fix(auth): check token version in calendar and stream endpoints
seonghobae Aug 2, 2026
9169338
fix(parser): replace dynamic RegExp with safe string matching to prev…
seonghobae Aug 2, 2026
51f08b3
fix(deps): bump @hono/node-server and prevent ReDoS in parseMsProjectXml
seonghobae Aug 2, 2026
c4c6a5d
test(security): cover query-token session revocation
seonghobae Aug 3, 2026
355b9e7
test(security): move revocation coverage to Node 22 API suite
seonghobae Aug 3, 2026
a1aa5bb
test(security): remove Node 22 API test from Node 20 fuzz suite
seonghobae Aug 3, 2026
82c8e63
test(security): run revocation regression in API suite
seonghobae Aug 3, 2026
f13726b
chore(security): align API test manifest with security base
seonghobae Aug 3, 2026
47ac0b2
docs(security): record query-token revocation verification
seonghobae Aug 3, 2026
27040a0
ci: synchronize lockfile and supported Node runtimes
seonghobae Aug 3, 2026
00b3c66
ci: run only tests present on session-revocation branch
seonghobae Aug 3, 2026
fbaae29
chore(security): remove stale vulnerable pnpm lockfile
seonghobae Aug 3, 2026
7194681
fix(security): use linear MS Project XML parsing
seonghobae Aug 3, 2026
bc16089
ci: apply reviewed session auth refactor
seonghobae Aug 3, 2026
f8ad361
ci(security): scope one-shot write permission to repair job
seonghobae Aug 3, 2026
ce90839
ci: repair PR 397 session authorization boundary
seonghobae Aug 3, 2026
281e1cc
ci: make PR 397 repair fail-diagnostic
seonghobae Aug 3, 2026
fd8bae8
ci: extract PR 397 repair script
seonghobae Aug 3, 2026
4088faf
ci: simplify PR 397 repair workflow
seonghobae Aug 3, 2026
3590b4c
fix(security): centralize revoked session validation
github-actions[bot] Aug 3, 2026
b4cc736
docs(changelog): record URL-token revocation coverage
seonghobae Aug 3, 2026
ba9a06b
chore(ci): remove completed one-shot auth repair workflow
seonghobae Aug 3, 2026
5ed7fa1
docs(security): cover every URL-token revocation path
seonghobae Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '20'
node-version: '22.13.0'
cache: 'npm'

- name: Install dependencies
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/server-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node 22
- name: Setup Node 22.13
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 22
node-version: 22.13.0
- name: Install
run: npm ci
- name: Unit tests (EVM · CPM · baseline · workload)
Expand All @@ -45,10 +45,10 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup Node 22
- name: Setup Node 22.13
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 22
node-version: 22.13.0
- name: Install
run: npm ci
- name: Install Playwright (chromium)
Expand Down
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,7 @@
**Vulnerability:** The backend CSV export for audit logs neutralized `=`, `+`, `-`, and `@` but failed to neutralize `|` (pipe) characters, allowing potential DDE (Dynamic Data Exchange) injection if exported logs were opened in spreadsheet software.
**Learning:** Spreadsheet formula defenses must cover all command-style prefixes including `|` across all CSV export boundaries, both frontend and backend.
**Prevention:** Update the sanitization regex in the backend export function to `/^[=+\-@|]/` so that all potentially executable spreadsheet payloads are prefixed with a single quote.
## 2026-08-02 - Fix session revocation bypass in calendar and stream endpoints
**Vulnerability:** The /api/projects/:id/calendar.ics and /api/projects/:id/stream endpoints verified the JWT signature but failed to check the token_version against the database, allowing revoked sessions to continue accessing data.
**Learning:** Auth middleware abstractions must be uniformly applied, or endpoints that accept tokens via query parameters (for non-browser clients) may inadvertently skip session state checks.
**Prevention:** Always extract reusable token validation logic (including DB revocation checks) into a shared utility function used by both header and query token authentication paths.
13 changes: 13 additions & 0 deletions .jules/verification-session-revocation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# URL-token session revocation verification

## Security invariant

Calendar, server-sent-event, and attachment-view endpoints that accept a session JWT through the query string must enforce the same database-backed `token_version` revocation check as bearer-token authentication.

## Regression evidence

`tests/api/session-revocation.test.mjs` creates two device sessions, confirms that calendar, SSE, and attachment-view authentication accept both live tokens before revocation, invokes `logout-all`, then verifies that both stale tokens receive HTTP 401 while the replacement token continues through the shared authentication boundary.

The attachment regression deliberately requests a missing attachment: a valid session reaches tenant-scoped lookup and receives HTTP 404, while a revoked session is rejected earlier with HTTP 401. This proves authentication ordering without requiring a fixture attachment.

The regression is part of `npm run test:api`. Every synchronized head must rerun Server Tests, Security Scan, SAST Semgrep, Dependency Review, OSV Scanner, and Fuzz before merge.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
workflows stay inherited from `ContextualWisdomLab/.github`, not copied
into this repository.

### Security

- Centralized session JWT verification and database-backed `token_version`
revocation across bearer middleware, calendar feeds, server-sent events, and
attachment-view URL transports.
- Added cross-device regression coverage proving that `logout-all` rejects stale
tokens before calendar, SSE, or attachment lookup while the replacement token
continues through the same authentication boundary.

### Changed

- 프로젝트 이름 입력 필드에 입력 예시(placeholder)를 추가하여 사용자 편의성을 개선했습니다.
Expand All @@ -46,4 +55,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [1.0.1] - 2026-06-25
### 성능 개선 (Performance)
- 드래그 앤 드롭 동작 중 `dragover` 이벤트에서 발생하는 O(N) 작업 리스트 검색 성능 병목 문제를, O(1) 해시맵(Map) 기반의 캐싱 조회 로직으로 개선하여 큰 크기의 WBS 리스트에서의 버벅임 현상을 해결했습니다.
- 드래그 앤 드롭 동작 중 `dragover` 이벤트에서 발생하는 O(N) 작업 리스트 검색 성능 병목 문제를, O(1) 해시맵(Map) 기반의 캐싱 조회 로직으로 개선하여 큰 크기의 WBS 리스트에서의 버벅임 현상을 해결했습니다.
39 changes: 34 additions & 5 deletions cloud-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -739,25 +739,54 @@ function openReportModal() {
// no DOMParser needed → node-testable); swap for a real XML parser if
// hand-edited files ever matter.
export function parseMsProjectXml(xml) {
// Fully linear extract (indexOf/slice) — no dynamic RegExp and no lazy
// [\s\S]*? block collectors (those can quadratic-backtrack on truncated input).
const tag = (block, name) => {
const m = block.match(new RegExp(`<${name}>([^<]*)</${name}>`));
return m ? m[1].trim() : '';
const openingTag = `<${name}>`;
const closingTag = `</${name}>`;
const valueStart = block.indexOf(openingTag);
if (valueStart === -1) return '';
const contentStart = valueStart + openingTag.length;
const valueEnd = block.indexOf(closingTag, contentStart);
return valueEnd === -1 ? '' : block.slice(contentStart, valueEnd).trim();
};
const collectBlocks = (source, openTag, closeTag) => {
const out = [];
let from = 0;
for (;;) {
const start = source.indexOf(openTag, from);
if (start === -1) break;
const contentStart = start + openTag.length;
const end = source.indexOf(closeTag, contentStart);
// Incomplete open tag: stop linearly (do not rescan the remainder).
if (end === -1) break;
out.push(source.slice(start, end + closeTag.length));
from = end + closeTag.length;
}
return out;
};
const predecessorIds = (block) => {
const ids = [];
for (const link of collectBlocks(block, '<PredecessorLink>', '</PredecessorLink>')) {
const uid = tag(link, 'PredecessorUID');
if (/^\d+$/.test(uid)) ids.push(`msp-${uid}`);
}
return ids;
};
const unescape = (s) => s
.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"')
.replace(/&apos;/g, "'").replace(/&amp;/g, '&');
const day = (s) => (/^\d{4}-\d{2}-\d{2}/.test(s) ? s.slice(0, 10) : '');
const tasks = [];
const parents = {}; // depth -> last task id at that depth
const blocks = xml.match(/<Task>[\s\S]*?<\/Task>/g) || [];
const blocks = collectBlocks(String(xml || ''), '<Task>', '</Task>');
for (const block of blocks) {
const uid = tag(block, 'UID');
const name = unescape(tag(block, 'Name'));
if (!uid || uid === '0' || !name) continue; // project-summary row / blanks
const level = Math.max(1, Number(tag(block, 'OutlineLevel')) || 1);
const depth = Math.min(level, 3); // deeper levels flatten to task level
const preds = [...block.matchAll(/<PredecessorLink>[\s\S]*?<PredecessorUID>(\d+)<\/PredecessorUID>[\s\S]*?<\/PredecessorLink>/g)]
.map((m) => `msp-${m[1]}`);
const preds = predecessorIds(block);
const pct = Number(tag(block, 'PercentComplete')) || 0;
const t = {
id: `msp-${uid}`,
Expand Down
Loading
Loading