Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": "./node_modules/gts/"
"extends": [
"./node_modules/gts/",
"./.eslintrc.override.json"
]
}
6 changes: 6 additions & 0 deletions .eslintrc.override.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"rules": {
"n/no-extraneous-import": "off",
"@typescript-eslint/no-unsafe-function-type": "off"
}
}
76 changes: 76 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Security Audit

on:
push:
branches: [master, dev]
pull_request:
branches: [master, dev]
schedule:
# Weekly full scan every Sunday at 02:00 UTC
- cron: '0 2 * * 0'

jobs:
# -----------------------------------------------------------------------
# 1. Semgrep SAST — static application security testing
# -----------------------------------------------------------------------
semgrep:
name: Semgrep SAST
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v4

- name: Run Semgrep — OWASP Top 10
run: |
semgrep \
--config p/owasp-top-ten \
--config p/nodejs \
--config p/expressjs \
--config p/typescript \
--config .semgrep/rules.yml \
--no-strict \
--sarif \
--output semgrep-results.sarif \
--error \
src/

- name: Upload Semgrep SARIF to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: semgrep-results.sarif
category: semgrep

# -----------------------------------------------------------------------
# 2. npm audit — vulnerable dependency check (SCA)
# -----------------------------------------------------------------------
npm-audit:
name: npm Dependency Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

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

- name: Install dependencies
run: npm ci

- name: Run npm audit (fail on critical)
run: npm audit --audit-level=critical

- name: Generate full audit report (artifact)
if: always()
run: npm audit --json > npm-audit-report.json || true

- name: Upload audit report
uses: actions/upload-artifact@v4
if: always()
with:
name: npm-audit-report
path: npm-audit-report.json
retention-days: 30
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ node_modules
.idea
dist/
config.json5
semgrep-results.sarif
npm-audit-report.json
3 changes: 3 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm audit --audit-level=high
110 changes: 110 additions & 0 deletions .semgrep/rules.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
rules:
# -----------------------------------------------------------------------
# Rule: express-error-message-leak
#
# Detects Express response handlers that pass err.message directly into
# the response body. Error messages from third-party libraries often
# contain internal file paths, stack frames, or database schema details
# that should never be exposed to API consumers.
#
# Safe pattern: return a static string and log the full error server-side.
# -----------------------------------------------------------------------
- id: express-error-message-leak
patterns:
- pattern: "res.$METHOD(..., { ..., error: $ERR.message, ... })"
- pattern-not: "res.$METHOD(..., { ..., error: \"...\", ... })"
message: >
Potential internal error details exposed to the client via err.message.
Return a safe static message (e.g. "Internal Server Error") and log the
full error server-side with a correlation ID.
languages: [typescript, javascript]
severity: WARNING
metadata:
category: security
owasp: "A09:2021"
cwe: "CWE-209"
references:
- https://cwe.mitre.org/data/definitions/209.html

# -----------------------------------------------------------------------
# Rule: express-500-error-message-leak
#
# Stricter variant of express-error-message-leak. Targets the specific
# pattern of res.status(500).send({ error: err.message }) which is an
# unambiguous information leak — a 500 always means an unhandled
# exception whose message is internal by definition.
# -----------------------------------------------------------------------
- id: express-500-error-message-leak
patterns:
- pattern: |
res.status(500).send({ error: $ERR.message })
message: >
res.status(500) with err.message leaks internal exception details
(file paths, stack frames, library internals) to the client.
Replace with a safe static message.
languages: [typescript, javascript]
severity: ERROR
metadata:
category: security
owasp: "A09:2021"
cwe: "CWE-209"

# -----------------------------------------------------------------------
# -----------------------------------------------------------------------
# Rule: multer-unsafe-originalname
#
# Flags uses of file.originalname in DANGEROUS contexts only:
# filesystem paths (path.join/resolve/dirname/basename), direct fs calls,
# and HTTP response bodies. Logging and sanitization calls are excluded.
#
# Why dangerous contexts only: Semgrep OSS cannot do cross-function
# data-flow analysis. A catch-all pattern on every .originalname read
# produces too many false positives (post-sanitization uses, log calls)
# and gets suppressed with nosemgrep, which defeats the purpose.
# Targeting dangerous sinks gives actionable findings with low FP rate.
# -----------------------------------------------------------------------
- id: multer-unsafe-originalname
patterns:
- pattern-either:
- pattern: path.join(..., $FILE.originalname, ...)
- pattern: path.resolve(..., $FILE.originalname, ...)
- pattern: path.basename($FILE.originalname, ...)
- pattern: path.dirname($FILE.originalname, ...)
- pattern: fs.$METHOD($FILE.originalname, ...)
- pattern: fsPromises.$METHOD($FILE.originalname, ...)
- pattern: "res.send({..., $KEY: $FILE.originalname, ...})"
- pattern: "res.json({..., $KEY: $FILE.originalname, ...})"
message: >
file.originalname is attacker-controlled (from the Content-Disposition
header). Sanitize before use: strip path separators, enforce max length,
normalize Unicode.
languages: [typescript, javascript]
severity: WARNING
metadata:
category: security
owasp: "A03:2021"
cwe: "CWE-22"
references:
- https://cwe.mitre.org/data/definitions/22.html

# -----------------------------------------------------------------------
# Rule: no-console-log
#
# Enforces use of a structured logger (pino, winston, etc.) instead of
# console.log. console.log has no severity level, no structured fields,
# and no output filtering — making it easy to inadvertently log sensitive
# values (tokens, CIDs, PII) in a format that cannot be suppressed
# in production without code changes.
# -----------------------------------------------------------------------
- id: no-console-log
pattern: console.log(...)
message: >
Avoid console.log in production code. Use a structured logger with
severity levels (e.g. pino, winston) so sensitive data can be filtered
and log verbosity can be controlled without code changes.
languages: [typescript, javascript]
severity: INFO
metadata:
category: best-practice
references:
- https://getpino.io
86 changes: 81 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pm2 start dist/index.js --name="IPFS node"
```


> **Security notice:** `ipfs-node` does not handle TLS itself. Always deploy it behind a TLS-terminating reverse proxy (nginx, Caddy, etc.). Never expose port 4000 directly to the internet without HTTPS.

## How to configure
Using `config.default.json5` as a template, you can create various configuration files.

Expand Down Expand Up @@ -62,12 +64,63 @@ For example:
maxFileCount: 5, // Maximum upload count of files per request
findFileTimeout: 20000, // Time limit for searching for a file on the IPFS network
cors: {
// Allowed URLs to make request to node. Set using regular expressions
originRegexps: ['.*\.adamant\.im', 'adm.im', '.*\.vercel\.app', '.*\.surge\.sh', 'localhost:8080']
}
// Allowed URLs to make requests to node. Set using properly anchored regular expressions.
// Dots in domain names must be escaped (\\.), and patterns must be anchored (^ and $)
// to prevent unintended matches (e.g. 'adm.im' without anchors would also match 'admXim').
originRegexps: [
'^https://.*\\.adamant\\.im$',
'^https://adm\\.im$',
'^https://.*\\.vercel\\.app$',
'^https://.*\\.surge\\.sh$',
'^http://localhost:8080$'
]
},

// API key for the protected admin endpoint GET /api/node/info.
// This endpoint exposes sensitive node topology data (peerId, IP addresses, disk usage)
// and must not be publicly accessible.
//
// Generate a strong random key before starting the node:
// openssl rand -hex 32
//
// If this field is missing or empty, GET /api/node/info returns 503.
// GET /api/node/health remains public and does not require this key.
adminApiKey: 'replace-with-output-of-openssl-rand-hex-32'
}
```

## Security

### Protected endpoints

`GET /api/node/info` requires authentication via the `x-api-key` HTTP header. This endpoint exposes sensitive node topology data — `peerId`, `multiAddresses`, disk usage statistics — which could be used to enumerate and map ADAMANT IPFS infrastructure.

Before starting the node, generate a strong API key and add it to your config:

```bash
openssl rand -hex 32
```

Then set it in `config.json5`:

```jsonc
adminApiKey: 'your-generated-key-here'
```

To call the protected endpoint:

```bash
curl -H "x-api-key: your-generated-key-here" http://localhost:4000/api/node/info
```

If `adminApiKey` is not set in config, the endpoint returns `503 Service not configured`.

`GET /api/node/health` is **public** and does not require authentication. It is safe to use for uptime monitoring and client availability checks.

### CORS

Allowed browser origins are configured via `cors.originRegexps` in your config file. Patterns must be properly anchored and use escaped dots to avoid unintended matches. See `config.default.json5` for examples.

## How to use

### Upload file
Expand Down Expand Up @@ -107,12 +160,35 @@ Content-Type: application/octet-stream
Hello ipfs-node!
```

### Get node info
### Get node health

#### Request
```GET /api/node/health```
```bash
curl -i --location 'http://localhost:4000/api/node/health'
```

#### Response
```
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
...

{
"timestamp": 1720614998797,
"heliaStatus": "started"
}
```

### Get node info (protected)

Requires `x-api-key` header matching `adminApiKey` from config.

#### Request
```GET /api/node/info```
```bash
curl -i --location 'http://localhost:4000/api/node/info'
curl -i --location 'http://localhost:4000/api/node/info' \
--header 'x-api-key: your-generated-key-here'
```

#### Response
Expand Down
12 changes: 9 additions & 3 deletions config.default.json5
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@
maxFileCount: 10,
findFileTimeout: 20000,
cors: {
origin: '*',
credentials: true
}
originRegexps: [
'^https://.*\\.adamant\\.im$',
'^https://adm\\.im$',
'^https://.*\\.vercel\\.app$',
'^https://.*\\.surge\\.sh$',
'^http://localhost:8080$'
]
},
adminApiKey: 'change-me-use-openssl-rand-hex-32'
}
Loading
Loading