Compare commits

..

2 Commits

Author SHA1 Message Date
LeChristopher Blackwell
5884573a69 fix: validate X-Refreshed-Token before storing it as the auth token (#971)
* fix: validate X-Refreshed-Token before storing it as the auth token

authenticatedFetch stored any X-Refreshed-Token response header value
straight into localStorage. A malformed or injected header value would
silently overwrite the stored auth token, breaking every subsequent
authenticated request. Only accept values that match the JWT shape the
server actually issues (three base64url segments).

* fix: apply refreshed-token validation to file upload path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYpHw7VYW1d2HwfL9EZKx4

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:38:23 +03:00
Simos Mikelatos
123d244a51 fix: harden docker cloudcli install 2026-07-08 09:22:10 +00:00
5 changed files with 43 additions and 9 deletions

View File

@@ -14,18 +14,22 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
template: [claude-code, codex, gemini] template: [claude-code, codex]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
with:
platforms: arm64
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
with:
version: latest
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -40,7 +44,7 @@ jobs:
echo "tags=$TAGS" >> "$GITHUB_OUTPUT" echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
context: ./docker context: ./docker
file: ./docker/${{ matrix.template }}/Dockerfile file: ./docker/${{ matrix.template }}/Dockerfile

View File

@@ -5,7 +5,16 @@ COPY shared/install-cloudcli.sh /tmp/install-cloudcli.sh
RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh
USER agent USER agent
RUN npm install -g @cloudcli-ai/cloudcli && cloudcli --version RUN --mount=type=cache,target=/tmp/npm-cache,sharing=locked,mode=0777 \
npm install -g @cloudcli-ai/cloudcli \
--cache=/tmp/npm-cache \
--fetch-retries=5 \
--fetch-retry-mintimeout=20000 \
--fetch-retry-maxtimeout=120000 \
--fetch-timeout=600000 \
--no-audit \
--no-fund && \
cloudcli --version
COPY --chown=agent:agent shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh COPY --chown=agent:agent shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh
RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc

View File

@@ -5,7 +5,16 @@ COPY shared/install-cloudcli.sh /tmp/install-cloudcli.sh
RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh
USER agent USER agent
RUN npm install -g @cloudcli-ai/cloudcli && cloudcli --version RUN --mount=type=cache,target=/tmp/npm-cache,sharing=locked,mode=0777 \
npm install -g @cloudcli-ai/cloudcli \
--cache=/tmp/npm-cache \
--fetch-retries=5 \
--fetch-retry-mintimeout=20000 \
--fetch-retry-maxtimeout=120000 \
--fetch-timeout=600000 \
--no-audit \
--no-fund && \
cloudcli --version
COPY --chown=agent:agent shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh COPY --chown=agent:agent shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh
RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc

View File

@@ -3,6 +3,7 @@ import type { DragEvent } from 'react';
import { IS_PLATFORM } from '../../../constants/config'; import { IS_PLATFORM } from '../../../constants/config';
import type { Project } from '../../../types/app'; import type { Project } from '../../../types/app';
import { isValidRefreshedToken } from '../../../utils/api';
import { import {
MAX_FILE_UPLOAD_COUNT, MAX_FILE_UPLOAD_COUNT,
MAX_FILE_UPLOAD_SIZE_BYTES, MAX_FILE_UPLOAD_SIZE_BYTES,
@@ -131,7 +132,7 @@ const uploadFormDataWithProgress = (
xhr.onload = () => { xhr.onload = () => {
const refreshedToken = xhr.getResponseHeader('X-Refreshed-Token'); const refreshedToken = xhr.getResponseHeader('X-Refreshed-Token');
if (refreshedToken) { if (isValidRefreshedToken(refreshedToken)) {
localStorage.setItem('auth-token', refreshedToken); localStorage.setItem('auth-token', refreshedToken);
} }

View File

@@ -1,5 +1,16 @@
import { IS_PLATFORM } from "../constants/config"; import { IS_PLATFORM } from "../constants/config";
// Only accept a refreshed token that has this app's issued JWT shape
// (three base64url segments). An attacker-injected/malformed header value
// must never overwrite the stored auth token.
/**
* @param {unknown} token
* @returns {token is string}
*/
export const isValidRefreshedToken = (token) =>
typeof token === 'string' &&
/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token);
// Utility function for authenticated API calls // Utility function for authenticated API calls
export const authenticatedFetch = (url, options = {}) => { export const authenticatedFetch = (url, options = {}) => {
const token = localStorage.getItem('auth-token'); const token = localStorage.getItem('auth-token');
@@ -23,7 +34,7 @@ export const authenticatedFetch = (url, options = {}) => {
}, },
}).then((response) => { }).then((response) => {
const refreshedToken = response.headers.get('X-Refreshed-Token'); const refreshedToken = response.headers.get('X-Refreshed-Token');
if (refreshedToken) { if (isValidRefreshedToken(refreshedToken)) {
localStorage.setItem('auth-token', refreshedToken); localStorage.setItem('auth-token', refreshedToken);
} }
return response; return response;