Skip to content

Random fixes#322

Open
avoidwork wants to merge 4 commits intomasterfrom
randomFixes
Open

Random fixes#322
avoidwork wants to merge 4 commits intomasterfrom
randomFixes

Conversation

@avoidwork
Copy link
Owner

  • Random fixes
  • Simplifying README.md

avoidwork and others added 4 commits February 21, 2026 18:13
…hance path traversal protection

Changes made:
- Removed automatic X-Powered-By header to prevent server information leakage
- Fixed etags validation to use !== null check
- Enhanced IPv6 validation with stricter regex pattern
- Improved path traversal protection using realpath for symlink handling
- Fixed parse function to properly default connection IP
- Created PLAN.md documenting audit progress and remaining work

Current status: 358/367 tests passing (9 failing)
Coverage: 99.79% (99.18% branches)
Security improvements completed
…andling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrote README.md to accurately reflect the actual source code in the src directory.
Removed extensive benchmark data, security assessments, and testing coverage details
that weren't reflected in the actual implementation.

New README focuses on:
- Core HTTP framework functionality
- Routing with parameters and RegExp
- Middleware support (Express-compatible)
- Static file serving
- CORS and security features
- CLI tool
- Complete API reference

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@avoidwork avoidwork self-assigned this Feb 25, 2026
@augmentcode
Copy link

augmentcode bot commented Feb 25, 2026

🤖 Augment PR Summary

Summary: This PR bundles several small security/correctness fixes across Woodland and streamlines project documentation.

Changes:

  • Stop setting the X-Powered-By response header by default; update unit/integration tests to assert it is absent.
  • Harden static file serving path traversal checks by resolving symlinks (realpath) before verifying the request stays within the configured folder.
  • Adjust ETag emission logic to check this.etags !== null when deciding whether to attach an ETag header.
  • Refactor utility.parse() to be more defensive when request host/connection fields are missing.
  • Tighten IP validation: stricter IPv4 matching, expanded IPv6/compressed handling, and broader edge-case test coverage.
  • Add a planning doc (PLAN.md), capture a full passing test run (test-output.txt), and significantly simplify README content/examples.

Technical Notes: The included test run shows 367 tests passing with 100% coverage.

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 6 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

const port = host.includes(":") && !host.startsWith("[") ? host.split(":")[1] : "8000";
const connectionIP = arg.connection?.remoteAddress || arg.socket?.remoteAddress || "localhost";

return new URL(`${arg.url}`, `http://${connectionIP}:${port}`);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse() builds the base URL from connectionIP, which will throw when remoteAddress is IPv6 (e.g. ::1 / ::ffff:127.0.0.1) because IPv6 needs bracket notation in URLs. Since parse() is called during decorate() without a try/catch, this can crash request handling on IPv6 connections.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

}

const host = arg.headers?.host || "localhost:8000";
const port = host.includes(":") && !host.startsWith("[") ? host.split(":")[1] : "8000";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Port extraction from host skips bracketed IPv6 hosts like [::1]:3000, so port falls back to 8000 even when the Host header specifies a different port. That makes req.parsed.port inconsistent with the incoming request for IPv6 hosts.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

const ipv4MappedMatch = IPV4_MAPPED_PATTERN.exec(ip);
if (ipv4MappedMatch) {
return isValidIP(ipv4MappedMatch[1]);
return isValidIP(ipv4MappedMatch[0].replace(/^::ffff:/, ""));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the IPv4-mapped IPv6 branch, replace(/^::ffff:/, "") is case-sensitive while IPV4_MAPPED_PATTERN is case-insensitive, so inputs like ::FFFF:127.0.0.1 will recurse with the same value and can overflow the stack. This looks attacker-controllable via headers such as X-Forwarded-For.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

const IPV4_MAPPED_PATTERN = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i;
const HEX_GROUP_PATTERN = /^[0-9a-fA-F]{1,4}$/;
const IPV4_PATTERN = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const IPV6_CHAR_PATTERN = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}|[0-9a-fA-F]{1,4}::(?:[0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::1|::$/;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because IPV6_CHAR_PATTERN isn’t grouped/anchored as ^(?:...|...|...)$, test() can succeed on substrings and allow non-hex characters elsewhere in the string to slip past this “quick validation”. Combined with the non-compressed branch only checking group.length > 4, some invalid full-notation IPv6 strings may be accepted.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.


try {
// Use realpath to resolve symlinks and normalize the path
fpPath = await realpath(fp);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only fp is resolved via realpath(), but folder isn’t, so if the configured folder is itself a symlink the subsequent containment comparison can reject legitimate files (or compare inconsistent roots). This could be a breaking behavior change for deployments that intentionally serve from a symlinked directory.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

const normalizedFp = resolve(fpPath);

// Check if the resolved file is outside the allowed directory
if (!normalizedFp.startsWith(normalizedFolder)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The startsWith containment check can be bypassed by sibling paths that share the same prefix (e.g., allowed /var/www vs resolved /var/www2/...), so traversal like ../www2/... may still pass this guard. Since this code is a security boundary for static serving, it’s worth tightening to avoid prefix-based false positives.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant