Skip to content
Closed
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
1 change: 1 addition & 0 deletions lib/defaultargs.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export default argv => {
argv.security_type = 'wiki-security-' + argv.security_type
}
argv.security_legacy ||= false
argv.public_proxy ||= false

// resolve all relative paths
argv.root = path.resolve(argv.root)
Expand Down
35 changes: 33 additions & 2 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import fs from 'fs'
import path from 'path'
import url from 'url'
import { pipeline } from 'node:stream/promises'
import dns from 'node:dns/promises'

// From npm
import express from 'express'
Expand All @@ -37,6 +38,16 @@ const DOMPurify = createDOMPurify(window)

// Using native fetch API (available in Node.js 18+)

// Loopback, RFC1918, link-local IPv4; loopback/link-local/ULA IPv6.
const isPrivateIP = addr => {
if (addr.includes(':')) return addr === '::1' || addr.startsWith('fe80:') || addr.startsWith('fc') || addr.startsWith('fd')
const [a, b, c, d] = addr.split('.').map(Number)
return (
[a, b, c, d].every(n => n >= 0 && n <= 255) &&
(a === 10 || a === 127 || (a === 192 && b === 168) || (a === 172 && b >= 16 && b <= 31) || (a === 169 && b === 254))
)
}

// Express 4 middleware
import logger from 'morgan'
import cookieParser from 'cookie-parser'
Expand Down Expand Up @@ -810,11 +821,30 @@ export default async argv => {

// ##### Proxy routes #####

app.get('/proxy/*splat', authorized, (req, res) => {
app.get('/proxy/*splat', async (req, res) => {
const signedIn = securityhandler.isAuthorized(req)
if (!signedIn && !argv.public_proxy) return res.sendStatus(403)
const pathParts = req.originalUrl.split('/')
const remoteHost = pathParts[2]
pathParts.splice(0, 3)
const remoteResource = pathParts.join('/')
// Anonymous: refuse hosts that are or resolve to private IPs (owners unchanged).
if (!signedIn) {
let host = remoteHost.toLowerCase()
if (host.startsWith('[')) {
const end = host.indexOf(']')
if (end < 0) return res.sendStatus(403)
host = host.slice(1, end)
} else {
host = host.split(':')[0]
}
try {
const { address } = await dns.lookup(host)
if (isPrivateIP(address.toLowerCase())) return res.sendStatus(403)
} catch {
return res.sendStatus(403)
}
}
// this will fail if remote is TLS only!
const requestURL = 'http://' + remoteHost + '/' + remoteResource
console.log('PROXY Request: ', requestURL)
Expand All @@ -824,7 +854,8 @@ export default async argv => {
requestURL.endsWith('.jpg') ||
pathParts[0] === 'plugin'
) {
fetch(requestURL, { signal: AbortSignal.timeout(2000) })
// Anonymous: do not follow redirects (could land on a private IP).
fetch(requestURL, { signal: AbortSignal.timeout(2000), redirect: signedIn ? 'follow' : 'error' })
.then(async fetchRes => {
if (fetchRes.ok) {
res.set('content-type', fetchRes.headers.get('content-type'))
Expand Down