Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
22 changes: 18 additions & 4 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
module.exports = {
extends: ["@stellar/eslint-config"],
env: {
browser: true,
es2021: true,
node: true,
},
extends: ["eslint:recommended", "prettier"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
plugins: ["@typescript-eslint"],
rules: {
"no-console": "off",
"import/no-unresolved": "off",
"no-await-in-loop": "off",
"no-constant-condition": "off",
"@typescript-eslint/naming-convention": ["warn"],
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"prefer-const": "warn",
"no-var": "warn",
eqeqeq: "warn",
},
ignorePatterns: ["node_modules/", "dist/", "*.min.js"],
};
43 changes: 24 additions & 19 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
name: "CodeQL"
name: "CodeQL Security Analysis"

on:
push:
branches: [ "master" ]
branches: ["master", "main"]
pull_request:
branches: [ "master" ]
branches: ["master", "main"]
schedule:
- cron: '26 17 * * 6'
- cron: "26 17 * * 6"

jobs:
analyze:
Expand All @@ -16,26 +16,31 @@ jobs:
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read

strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
- language: javascript-typescript
build-mode: none

steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Checkout repository
uses: actions/checkout@v4

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
56 changes: 41 additions & 15 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,55 @@
name: Test and build
name: Test and Build

on:
push:
branches:
- master
- master
- main
pull_request:

jobs:
build:
test-and-build:
runs-on: ubuntu-latest

services:
redis:
image: redis
image: redis:7-alpine
# Set health checks to wait until redis has started
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
--health-cmd "redis-cli ping" --health-interval 10s --health-timeout
5s --health-retries 5
ports:
- 6379:6379
- 6379:6379

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"

- name: Install dependencies
run: npm install --legacy-peer-deps
# Note: Using --legacy-peer-deps due to muicss package compatibility with React 18

- name: Run linter
run: npx eslint backend/ --ext .ts

- name: Run tests
run: npm test
env:
DEV: true

- name: Build application
run: npm run build

- name: Upload build artifacts
uses: actions/upload-artifact@v4
if: success()
with:
node-version: 16
- run: yarn install
- run: yarn test
- run: yarn build
name: build-files
path: dist/
retention-days: 7
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/node_modules
/.tmp
/dist
*.eslintcache
*service-account.json
*.env
/.vscode
/.idea
/.kiro
/.cursor
dump.rdb
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
.tmp

16 changes: 10 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ubuntu:22.04
FROM ubuntu:24.04

MAINTAINER SDF Ops Team <ops@stellar.org>

Expand All @@ -8,16 +8,20 @@ WORKDIR /app/src
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
gpg curl ca-certificates git apt-transport-https && \
curl -sSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key|gpg --dearmor >/etc/apt/trusted.gpg.d/nodesource-key.gpg && \
echo "deb https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg |gpg --dearmor >/etc/apt/trusted.gpg.d/yarnpkg.gpg && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs yarn && \
yarn install && /app/src/node_modules/gulp/bin/gulp.js build
echo "deb https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs && \
npm ci --legacy-peer-deps && npm run build

ENV PORT=80 UPDATE_DATA=false
EXPOSE 80

RUN node_modules/typescript/bin/tsc

# Copy common directory to dist for runtime access
RUN cp -r common dist/

# Change working directory to dist for runtime
WORKDIR /app/src/dist

ENTRYPOINT ["/usr/bin/node"]
CMD ["./backend/app.js"]
2 changes: 1 addition & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
web: node app.js
web: npx tsx backend/app.ts
5 changes: 0 additions & 5 deletions backend/app.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
// need to manually import regeneratorRuntime for babel w/ async
// https://github.com/babel/babel/issues/9849#issuecomment-487040428
// require("regenerator-runtime/runtime");
import "regenerator-runtime/runtime";

import "dotenv/config";

// Run backend with cache updates.
Expand Down
6 changes: 3 additions & 3 deletions backend/ledgers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import stellarSdk from "stellar-sdk";
import { Horizon } from "@stellar/stellar-sdk";
import { findIndex } from "lodash";
import { Response, NextFunction } from "express";

Expand Down Expand Up @@ -64,7 +64,7 @@ export async function updateLedgers() {

await catchup(REDIS_LEDGER_KEY, pagingToken, REDIS_PAGING_TOKEN_KEY, 0);

const horizon = new stellarSdk.Server("https://horizon.stellar.org");
const horizon = new Horizon.Server("https://horizon.stellar.org");
horizon
.ledgers()
.cursor(CURSOR_NOW)
Expand All @@ -82,7 +82,7 @@ export async function catchup(
pagingTokenKey: string,
limit: number, // if 0, catchup until now
) {
const horizon = new stellarSdk.Server("https://horizon.stellar.org");
const horizon = new Horizon.Server("https://horizon.stellar.org");
let ledgers: LedgerRecord[] = [];
let total = 0;
let pagingToken = pagingTokenStart;
Expand Down
100 changes: 99 additions & 1 deletion backend/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import express from "express";
import proxy from "express-http-proxy";
import logger from "morgan";
import path from "path";

import * as lumens from "./lumens";
import * as lumensV2V3 from "./v2v3/lumens";
Expand All @@ -13,6 +14,7 @@
app.use(logger("combined"));

if (process.env.DEV) {
// Development: proxy to Vite dev server
app.use(
"/",
proxy("localhost:3000", {
Expand All @@ -24,7 +26,103 @@
}),
);
} else {
app.use(express.static("dist"));
// Production: serve built static files
// Determine the correct static directory based on environment
let staticDir: string;

if (process.cwd().endsWith("/dist")) {
// Docker environment: already in dist directory
staticDir = ".";
} else {
// Heroku/other environments: serve from dist directory
staticDir = path.join(__dirname, "..", "..", "dist");
}

console.log(`Serving static files from: ${path.resolve(staticDir)}`);

// Verify the static directory exists and contains expected files
try {
const fs = require("fs");
const indexPath = path.join(staticDir, "index.html");
if (!fs.existsSync(indexPath)) {
console.error(`ERROR: index.html not found at ${indexPath}`);
console.error(
'Make sure to run "npm run build" before starting the server',
);
process.exit(1);
}
} catch (error) {
console.error("ERROR: Unable to verify static directory:", error);
process.exit(1);
}

// Serve static files with security headers and restrictions
app.use(
express.static(staticDir, {
// Security options
dotfiles: "deny", // Don't serve hidden files (.env, .git, etc.)
index: "index.html",
maxAge: "1d", // Cache static assets for 1 day
// Restrict to specific file extensions for security
extensions: [
"html",
"js",
"css",
"png",
"jpg",
"jpeg",
"gif",
"ico",
"svg",
"woff",
"woff2",
"ttf",
"eot",
],
setHeaders: (res, filePath) => {
// Add security headers
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("X-XSS-Protection", "1; mode=block");
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");

// Set appropriate cache headers based on file type
if (filePath.endsWith(".html")) {
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
} else if (
filePath.match(
/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/,
)
) {
res.setHeader("Cache-Control", "public, max-age=86400, immutable"); // 1 day with immutable
}
},
}),
);
Comment on lines +119 to +162

Check warning

Code scanning / CodeQL

Exposure of private files Medium

Serves the current working folder, which can contain private information.

Copilot Autofix

AI 4 months ago

In general, the problem is that express.static is configured with "." (the process working directory) in some environments, which can unintentionally expose files outside the intended build output. To fix this, we should never serve "." or any directory derived from process.cwd(); instead, we should always use a known, explicit path to the built static assets, and ensure that this path cannot “expand” to include private files.

The best minimal fix without changing existing behavior is:

  • Remove the special-case branch that sets staticDir = "." in Docker.
  • Always compute staticDir from __dirname to the known dist directory (which the rest of the code already expects via index.html in dist).
  • Optionally allow overriding via an environment variable (e.g., STATIC_DIR) if needed, but still resolve it from __dirname (not process.cwd()).
  • Keep all the current security options on express.static (dotfiles deny, limited extensions, security headers).

Concretely, in backend/routes.ts:

  • Replace the conditional block at lines 92–98 with a deterministic resolution of staticDir to the intended distribution folder, e.g. path.join(__dirname, "..", "..", "dist"). If you want a Docker-specific path, it should still be explicit (e.g. "/usr/share/app/dist"), but not ".".
  • Leave the rest of the static-serving configuration (logging, checks for index.html, headers) unchanged.

No new imports are required; path is already imported, and fs is required locally in the existing code.

Suggested changeset 1
backend/routes.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/backend/routes.ts b/backend/routes.ts
--- a/backend/routes.ts
+++ b/backend/routes.ts
@@ -89,13 +89,8 @@
   // Determine the correct static directory based on environment
   let staticDir: string;
 
-  if (process.cwd().endsWith("/dist")) {
-    // Docker environment: already in dist directory
-    staticDir = ".";
-  } else {
-    // Heroku/other environments: serve from dist directory
-    staticDir = path.join(__dirname, "..", "..", "dist");
-  }
+  // Always serve static files from the built dist directory, regardless of CWD
+  staticDir = path.join(__dirname, "..", "..", "dist");
 
   console.log(`Serving static files from: ${path.resolve(staticDir)}`);
 
EOF
@@ -89,13 +89,8 @@
// Determine the correct static directory based on environment
let staticDir: string;

if (process.cwd().endsWith("/dist")) {
// Docker environment: already in dist directory
staticDir = ".";
} else {
// Heroku/other environments: serve from dist directory
staticDir = path.join(__dirname, "..", "..", "dist");
}
// Always serve static files from the built dist directory, regardless of CWD
staticDir = path.join(__dirname, "..", "..", "dist");

console.log(`Serving static files from: ${path.resolve(staticDir)}`);

Copilot is powered by AI and may make mistakes. Always verify output.

// Fallback to index.html for SPA routing (must come after API routes)
app.get("*", (req, res, next) => {
// Skip API routes
if (req.path.startsWith("/api/")) {
return next();
}

// Only serve index.html for GET requests to prevent issues with other HTTP methods
if (req.method !== "GET") {
return next();
}

// Serve index.html for all other routes (SPA routing)
const indexPath = path.join(path.resolve(staticDir), "index.html");
res.sendFile(indexPath, (err) => {
if (err) {
console.error("Error serving index.html:", err);
res.status(500).send("Internal Server Error");
}
});
});
Comment thread Fixed
}

app.get("/api/ledgers/public", ledgers.handler);
Expand Down
16 changes: 10 additions & 6 deletions common/lumens.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
export var ORIGINAL_SUPPLY_AMOUNT: string;
export function getLumenBalance(horizonURL: string, accountId: string): string;
export function totalLumens(horizonURL: string): string;
export const ORIGINAL_SUPPLY_AMOUNT: string;
export function getLumenBalance(
horizonURL: string,
accountId: string,
): Promise<string>;
export function totalLumens(horizonURL: string): Promise<string>;
export function inflationLumens(): Promise<BigNumber>;
export function feePool(): string;
export function burnedLumens(): string;
export function feePool(): Promise<string>;
export function burnedLumens(): Promise<string>;
export function directDevelopmentAll(): Promise<string>;
export function distributionEcosystemSupport(): Promise<string>;
export function distributionUseCaseInvestment(): Promise<string>;
export function distributionUserAcquisition(): Promise<string>;
export function getUpgradeReserve(): string;
export function distributionAll(): Promise<BigNumber>;
export function getUpgradeReserve(): Promise<string>;
export function sdfAccounts(): Promise<BigNumber>;
export function totalSupply(): Promise<BigNumber>;
export function noncirculatingSupply(): Promise<BigNumber>;
Expand Down
Loading
Loading