Skip to content

Commit cdecb64

Browse files
ClupaimaplefoxgitNiethin69
committed
fix(security): T2 2026 permission and security fixes
Brings the T2 2026 security work from ontrack-features-t2-2026 11.0.x (reviewed and merged work) onto thoth-tech 11.0.x. Co-authored-by: maplefoxgit <s223932052@deakin.edu.au> Co-authored-by: Maple Fox <s223932052@deakin.edu.au> Co-authored-by: Niethin <niethinrueshil@gmail.com>
1 parent 20d1f38 commit cdecb64

15 files changed

Lines changed: 1297 additions & 128 deletions

‎.env.example‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Optional values for development through the legacy root docker-compose.yml.
2+
# Copy this file to .env. Database authentication remains the safe default.
3+
# Never commit an institution credential or a populated .env file.
4+
DF_AUTH_METHOD=database
5+
6+
# To exercise AAF locally, obtain a dedicated non-production registration from
7+
# the identity owner, change DF_AUTH_METHOD to aaf, and fill every field below.
8+
DF_AAF_ISSUER_URL=
9+
DF_AAF_AUDIENCE_URL=http://localhost:4200
10+
DF_AAF_CALLBACK_URL=http://localhost:4200/api/auth/jwt
11+
DF_AAF_IDENTITY_PROVIDER_URL=
12+
DF_AAF_UNIQUE_URL=
13+
DF_AAF_AUTH_SIGNOUT_URL=
14+
DF_SECRET_KEY_AAF=

‎Dockerfile‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
FROM node:22
1+
FROM node:22.23.2-bookworm-slim@sha256:83f487e0a63425e5b4d146fb5e5be574bcbe1b7b843d3ebafdd95eaf7767a7e5
22

3-
ENV DEBIAN_FRONTEND noninteractive
3+
# The upstream image ships npm 10.9.8. Upgrade to the latest Node 22-compatible
4+
# npm 11 release to pick up patched bundled dependencies before dropping
5+
# privileges.
6+
RUN npm install --global npm@11.19.1 \
7+
&& npm cache clean --force
8+
9+
ENV DEBIAN_FRONTEND=noninteractive
410
ENV USER=node
5-
ENV NODE_ENV docker
11+
ENV NODE_ENV=docker
612

713
# You can not use `${USER}` here, but reference `/home/node`.
814
ENV PATH="/home/node/.npm-global/bin:${PATH}"
@@ -26,5 +32,5 @@ RUN npm ci --force --include=optional
2632

2733
EXPOSE 9876
2834

29-
# Launch - install on launch so that node_modules are updated in volume
30-
CMD /bin/bash -c 'npm install; npm start'
35+
# Install on launch so that bind-mounted source and node_modules stay current.
36+
CMD ["/bin/bash", "-c", "npm install && exec npm start"]

‎deploy.Dockerfile‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
### STAGE 1: Build ###
2-
FROM node:22 AS build
2+
FROM node:22.23.2-bookworm-slim@sha256:83f487e0a63425e5b4d146fb5e5be574bcbe1b7b843d3ebafdd95eaf7767a7e5 AS build
3+
4+
# Keep the build package manager aligned with the development image. npm 11
5+
# removes the vulnerable dependencies bundled with this Node 22 base.
6+
RUN npm install --global npm@11.19.1 \
7+
&& npm cache clean --force
38

49
RUN apt-get update && apt-get install -y --no-install-recommends \
510
gettext-base \
@@ -11,10 +16,9 @@ USER node
1116
# Copy in doubtfire-web code
1217
WORKDIR /doubtfire-web
1318
COPY package.json package-lock.json ./
14-
RUN npm ci --force --include=optional
19+
RUN npm ci --include=optional
1520

1621
COPY --chown=node:node . .
17-
RUN chmod 777 src
1822

1923
ARG SENTRY_DSN
2024
ARG SENTRY_ORG
@@ -42,7 +46,11 @@ RUN --mount=type=secret,id=sentry_auth_token,uid=1000 \
4246

4347

4448
## STAGE 2: Host ###
45-
FROM nginx:1.29.0-alpine
49+
FROM nginx:1.30.4-alpine@sha256:97d490c12ba55b4946b01546d1c3ed324e8d41ab1c9fcb2a616aa470620e5b46
50+
51+
# The pinned Nginx image predates Alpine's OpenSSL 3.5.8 security update.
52+
# Upgrade only the affected runtime libraries rather than the whole base.
53+
RUN apk upgrade --no-cache libcrypto3 libssl3
4654

4755
# Remove the default Nginx configuration file
4856
RUN rm -v /etc/nginx/nginx.conf

‎docker-compose.yml‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
version: '3'
21
services:
32
doubtfire-api:
43
container_name: web-doubtfire-api
@@ -21,15 +20,17 @@ services:
2120
DF_SECRET_KEY_ATTR: test-secret-key-test-secret-key!
2221
DF_SECRET_KEY_DEVISE: test-secret-key-test-secret-key!
2322

24-
# Authentication method - can set to AAF or ldap
25-
DF_AUTH_METHOD: database
26-
DF_AAF_ISSUER_URL: https://rapid.test.aaf.edu.au
27-
DF_AAF_AUDIENCE_URL: http://localhost:4200
28-
DF_AAF_CALLBACK_URL: http://localhost:4200/api/auth/jwt
29-
DF_AAF_IDENTITY_PROVIDER_URL: https://signon-uat.deakin.edu.au/idp/shibboleth
30-
DF_AAF_UNIQUE_URL: https://rapid.test.aaf.edu.au/jwt/authnrequest/research/Ag4EJJhjf0zXHqlKvKZEbg
31-
DF_AAF_AUTH_SIGNOUT_URL: https://sync-uat.deakin.edu.au/auth/logout
32-
DF_SECRET_KEY_AAF: v4~LMFLzzwRGZdju\5QBa@FiHIN9
23+
# Database authentication is the safe local default. Optional AAF
24+
# development values must come from an ignored .env file; never commit an
25+
# institution credential or registration URL here.
26+
DF_AUTH_METHOD: ${DF_AUTH_METHOD:-database}
27+
DF_AAF_ISSUER_URL: ${DF_AAF_ISSUER_URL:-}
28+
DF_AAF_AUDIENCE_URL: ${DF_AAF_AUDIENCE_URL:-http://localhost:4200}
29+
DF_AAF_CALLBACK_URL: ${DF_AAF_CALLBACK_URL:-http://localhost:4200/api/auth/jwt}
30+
DF_AAF_IDENTITY_PROVIDER_URL: ${DF_AAF_IDENTITY_PROVIDER_URL:-}
31+
DF_AAF_UNIQUE_URL: ${DF_AAF_UNIQUE_URL:-}
32+
DF_AAF_AUTH_SIGNOUT_URL: ${DF_AAF_AUTH_SIGNOUT_URL:-}
33+
DF_SECRET_KEY_AAF: ${DF_SECRET_KEY_AAF:-}
3334

3435
# Database settings - for development env
3536
DF_DEV_DB_ADAPTER: mysql2
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
const assert = require('node:assert/strict');
2+
const {readFileSync} = require('node:fs');
3+
const {resolve} = require('node:path');
4+
5+
const root = resolve(__dirname, '..');
6+
const nginx = readFileSync(resolve(root, 'nginx.conf'), 'utf8');
7+
const dockerfile = readFileSync(resolve(root, 'deploy.Dockerfile'), 'utf8');
8+
const developmentCompose = readFileSync(resolve(root, 'docker-compose.yml'), 'utf8');
9+
const deploymentWorkflows = [
10+
['default', readFileSync(resolve(root, '.github/workflows/deployment.yml'), 'utf8')],
11+
[
12+
'institution',
13+
readFileSync(resolve(root, '.github/workflows/deployment-institution.yml'), 'utf8'),
14+
],
15+
];
16+
const applicationBootstrap = readFileSync(resolve(root, 'src/main.ts'), 'utf8');
17+
const scormPlayerTemplate = readFileSync(
18+
resolve(root, 'src/app/common/scorm-player/scorm-player.component.html'),
19+
'utf8',
20+
);
21+
const productionEnvironment = readFileSync(
22+
resolve(root, 'src/environments/environment.prod.ts'),
23+
'utf8',
24+
);
25+
26+
const controlFiles = [
27+
'/index.html',
28+
'/ngsw.json',
29+
'/ngsw-worker.js',
30+
'/safety-worker.js',
31+
'/worker-basic.min.js',
32+
'/manifest.webmanifest',
33+
];
34+
const noStorePolicy = 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
35+
36+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
37+
38+
for (const path of controlFiles) {
39+
const escapedPath = escapeRegExp(path);
40+
41+
assert.match(
42+
nginx,
43+
new RegExp(`^\\s*${escapedPath}\\s+"${noStorePolicy}";`, 'm'),
44+
`${path} must map to the no-store cache policy`,
45+
);
46+
assert.match(
47+
nginx,
48+
new RegExp(`location = ${escapedPath} \\{\\s*try_files \\$uri =404;\\s*\\}`, 'm'),
49+
`${path} must use an exact location and return 404 when absent`,
50+
);
51+
}
52+
53+
assert.match(
54+
nginx,
55+
/add_header Cache-Control \$ontrack_control_file_cache_control always;/,
56+
'the mapped cache policy must be added to responses',
57+
);
58+
assert.match(
59+
nginx,
60+
/try_files \$uri \$uri\/ \$uri\/index\.html \/index\.html;/,
61+
'SPA routes must continue to fall back to /index.html',
62+
);
63+
for (const [name, workflow] of deploymentWorkflows) {
64+
assert.doesNotMatch(
65+
workflow,
66+
/steps\.meta\.outputs\.labels/,
67+
`${name} deployment metadata labels must reference the declared docker_meta step`,
68+
);
69+
assert.match(workflow, /steps\.docker_meta\.outputs\.labels/);
70+
71+
for (const line of workflow.split('\n')) {
72+
if (/^\s*uses:/.test(line)) {
73+
assert.match(
74+
line,
75+
/@[0-9a-f]{40}(?:\s+#.*)?$/,
76+
`${name} production image workflow actions must be pinned to immutable commits`,
77+
);
78+
}
79+
}
80+
81+
assert.match(workflow, /^\s*sbom:\s*true$/m);
82+
assert.match(workflow, /^\s*provenance:\s*mode=max$/m);
83+
}
84+
assert.match(
85+
dockerfile,
86+
/^FROM node:22\.23\.2-bookworm-slim@sha256:[0-9a-f]{64} AS build$/m,
87+
'the release build must pin the supported Node image digest',
88+
);
89+
assert.match(
90+
dockerfile,
91+
/npm install --global npm@11\.19\.1/,
92+
'the release build must install the reviewed npm security update',
93+
);
94+
assert.match(
95+
dockerfile,
96+
/^FROM nginx:1\.30\.4-alpine@sha256:[0-9a-f]{64}$/m,
97+
'the release runtime must pin the Nginx image digest',
98+
);
99+
assert.match(
100+
dockerfile,
101+
/^RUN apk upgrade --no-cache libcrypto3 libssl3$/m,
102+
'the release runtime must install the reviewed Alpine OpenSSL security update',
103+
);
104+
assert.doesNotMatch(
105+
dockerfile,
106+
/npm ci[^\n]*--force/,
107+
'the release build must not force npm resolution',
108+
);
109+
assert.doesNotMatch(
110+
dockerfile,
111+
/chmod\s+777/,
112+
'the release build must not create world-writable source',
113+
);
114+
assert.match(
115+
developmentCompose,
116+
/DF_SECRET_KEY_AAF:\s*\$\{DF_SECRET_KEY_AAF:-\}/,
117+
'optional development AAF credentials must come from the ignored environment',
118+
);
119+
assert.doesNotMatch(
120+
developmentCompose,
121+
/https?:\/\/[^\s$]*(?:aaf\.edu\.au|deakin\.edu\.au)/i,
122+
'institution-specific AAF endpoints must not be committed in development Compose',
123+
);
124+
assert.doesNotMatch(
125+
applicationBootstrap,
126+
/(?:browserTracingIntegration|replayIntegration|tracesSampleRate|replaysSessionSampleRate|replaysOnErrorSampleRate)/,
127+
'tracing and replay must stay disabled while SCORM uses credential-bearing paths',
128+
);
129+
assert.match(
130+
applicationBootstrap,
131+
/enableLogs:\s*false/,
132+
'Sentry Logs must stay disabled for the errors-only telemetry policy',
133+
);
134+
assert.match(
135+
scormPlayerTemplate,
136+
/<iframe[^>]*data-sentry-block[^>]*referrerpolicy="no-referrer"/,
137+
'the SCORM iframe must suppress referrers and remain blocked from any future replay integration',
138+
);
139+
assert.match(
140+
productionEnvironment,
141+
/enableDemoTools:\s*false/,
142+
'production must keep demo tools disabled',
143+
);
144+
145+
console.log('Deployment configuration checks passed.');

0 commit comments

Comments
 (0)