CMMC 2.0 SC.L1-3.13.5 (NIST 800-171r2 3.13.5) requires separation between publicly accessible subnetworks and internal networks. In application terms, this means public-facing routes and protected internal routes must be architecturally separated — not mixed under the same middleware or path structure with inconsistent access controls. OWASP A01 and CWE-284 (Improper Access Control) describe the failure mode: when admin and public endpoints share the same routing layer with no access boundary, a path traversal or misconfigured matcher exposes privileged functionality to public access.
High because routing architecture that mixes public and protected paths creates a structural bypass risk — a single middleware misconfiguration exposes all internal routes simultaneously.
Use Next.js route groups to create a hard architectural boundary between public, protected, and admin routes. Apply middleware targeting each group explicitly rather than relying on path string matching alone:
src/app/
├── (public)/ # No auth — landing, login, about
├── (protected)/ # Auth required — dashboard, documents
└── (admin)/ # Auth + admin role required
// middleware.ts
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const session = request.cookies.get('session')
if (pathname.startsWith('/dashboard') || pathname.startsWith('/documents')) {
if (!session) return NextResponse.redirect(new URL('/login', request.url))
}
if (pathname.startsWith('/admin') || pathname.startsWith('/api/admin')) {
if (!session) return NextResponse.redirect(new URL('/login', request.url))
// Each admin handler must also verify role server-side
}
return NextResponse.next()
}
Verify the middleware matcher pattern does not exclude /api/* paths — that is the most common source of bypass gaps in Next.js applications.
ID: gov-cmmc-level-1.system-comms.public-access-separation
Severity: high
CMMC Practice: SC.L1-3.13.5
What to look for: Look for clear separation between public-facing routes and protected internal routes. In Next.js, check the route structure — public routes under app/(marketing)/ or similar, protected routes under app/(app)/ or app/dashboard/ behind auth middleware. Check whether admin endpoints are grouped and protected separately from regular user endpoints. Look for middleware that applies auth checks to protected route groups but not public pages. Review whether any protected routes are inadvertently accessible via alternate paths.
Pass criteria: Enumerate all route groups and classify each as public or protected. Clear route separation exists between public content and authenticated areas with at least 2 distinct access levels. Admin routes are isolated behind role-checking middleware. Report: "X public routes, Y protected routes, Z admin routes identified."
Fail criteria: No clear separation between public and internal routes. Admin and public endpoints are mixed without access controls. The same middleware (or lack thereof) applies to both public and protected routes. Protected routes accessible without authentication.
Skip (N/A) when: All content is fully public — no protected resources, no user authentication, no sensitive data.
Detail on fail: Describe the architectural gap. Example: "No route grouping or separation. All routes under app/ share identical middleware with no protected vs public distinction. Admin routes at /api/admin/* have no separate protection layer." Keep under 500 characters.
Remediation: Use route grouping to enforce separation at the architectural level:
src/app/
├── (public)/ # No auth required
│ ├── page.tsx # Landing page
│ ├── about/page.tsx
│ └── login/page.tsx
├── (protected)/ # Auth required — covered by middleware
│ ├── dashboard/page.tsx
│ └── documents/page.tsx
└── (admin)/ # Auth + admin role required
└── admin/
└── users/page.tsx
// middleware.ts
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const session = request.cookies.get('session')
if (pathname.startsWith('/dashboard') || pathname.startsWith('/documents')) {
if (!session) return NextResponse.redirect(new URL('/login', request.url))
}
if (pathname.startsWith('/admin') || pathname.startsWith('/api/admin')) {
if (!session) return NextResponse.redirect(new URL('/login', request.url))
// Individual admin routes must also verify role server-side
}
return NextResponse.next()
}