Unprotected Alternate Channel
Description
Unprotected Alternate Channel occurs when a product protects a primary channel but fails to apply equivalent security measures to alternate access paths. While the main communication channel may have proper authentication, encryption, or access controls, alternate channels such as backup interfaces, debug ports, secondary protocols, or mirrored resources may lack these protections. Attackers can exploit these unprotected alternate channels to bypass security measures implemented on the primary channel.
Risk
Unprotected alternate channels have severe implications. Complete bypass of authentication. Access to protected resources via alternate paths. Debug interface exploitation. Backup channel abuse. Mirror/replica access without authorization. Protocol downgrade attacks. Administrative bypass. High likelihood when security is focused only on primary interfaces.
Solution
Identify all alternate channels and use the same protection mechanisms applied to primary channels during architecture and design phase. Audit all access paths to sensitive resources. Disable unnecessary alternate channels. Apply consistent security policies across all interfaces during implementation phase. Monitor alternate channels for unauthorized access.
Common Consequences
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control Gain Privileges or Assume Identity - Attackers can bypass protection mechanisms by exploiting the unprotected alternate channel. |
| Authorization | Scope: Authorization Bypass Protection Mechanism - Security controls on primary channel become ineffective. |
Example Code
Vulnerable Code
// Vulnerable: Hardware with unprotected alternate register access
module vulnerable_register_access (
input wire clk,
input wire rst_n,
input wire [31:0] address,
input wire [31:0] data_in,
input wire write_en,
input wire auth_valid, // Authentication signal
output reg [31:0] data_out,
output reg access_denied
);
// Secure register at primary address
reg [31:0] SECURE_REG;
localparam SECURE_ADDR = 32'h0000_0F00;
// VULNERABLE: Unprotected mirror at alternate address
localparam MIRROR_ADDR = 32'h0080_0F00;
// VULNERABLE: Only primary address is protected
wire addr_needs_auth = (address == SECURE_ADDR);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
SECURE_REG <= 32'h0;
access_denied <= 1'b0;
end else begin
access_denied <= 1'b0;
if (address == SECURE_ADDR) begin
// Primary address - requires authentication
if (auth_valid) begin
if (write_en) begin
SECURE_REG <= data_in;
end
data_out <= SECURE_REG;
end else begin
access_denied <= 1'b1;
end
end
else if (address == MIRROR_ADDR) begin
// VULNERABLE: Mirror address - NO authentication!
if (write_en) begin
SECURE_REG <= data_in; // Writes to same register!
end
data_out <= SECURE_REG; // Reads same register!
end
end
end
// Attack: Access MIRROR_ADDR (0x00800F00) instead of SECURE_ADDR (0x0F00)
// Bypasses all authentication checks
endmodule
# Vulnerable: Application with unprotected alternate endpoints
from flask import Flask, request, jsonify
from functools import wraps
app = Flask(__name__)
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
if not verify_auth_token(request.headers.get('Authorization')):
return jsonify({'error': 'Unauthorized'}), 401
return f(*args, **kwargs)
return decorated
# Primary API - protected
@app.route('/api/v2/users', methods=['GET'])
@require_auth
def get_users_v2():
return jsonify(get_all_users())
@app.route('/api/v2/config', methods=['GET', 'POST'])
@require_auth
def config_v2():
if request.method == 'GET':
return jsonify(get_config())
else:
update_config(request.json)
return jsonify({'status': 'updated'})
# VULNERABLE: Legacy API still active - unprotected!
@app.route('/api/v1/users', methods=['GET'])
def get_users_v1():
# VULNERABLE: No authentication!
return jsonify(get_all_users())
@app.route('/api/v1/config', methods=['GET', 'POST'])
def config_v1():
# VULNERABLE: No authentication!
if request.method == 'GET':
return jsonify(get_config())
else:
update_config(request.json)
return jsonify({'status': 'updated'})
# VULNERABLE: Debug endpoint - alternate channel
@app.route('/debug/dump', methods=['GET'])
def debug_dump():
# VULNERABLE: Complete data dump without auth
return jsonify({
'users': get_all_users(),
'config': get_config(),
'secrets': get_secrets() # Even worse!
})
# VULNERABLE: Health check exposes data
@app.route('/health', methods=['GET'])
def health_check():
# VULNERABLE: Includes sensitive info
return jsonify({
'status': 'ok',
'database': get_db_status(),
'users_count': len(get_all_users()),
'config': get_config() # Exposed!
})
// Vulnerable: Java service with unprotected alternate channel
public class VulnerableService {
// Primary channel - HTTPS with authentication
@RestController
@RequestMapping("/api/secure")
public class SecureController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/data")
public ResponseEntity<Data> getData() {
return ResponseEntity.ok(dataService.getAllData());
}
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/config")
public ResponseEntity<Void> updateConfig(@RequestBody Config config) {
configService.update(config);
return ResponseEntity.ok().build();
}
}
// VULNERABLE: JMX exposed without authentication
@ManagedResource
public class DataManager {
@ManagedOperation
public String getAllDataAsJson() {
// VULNERABLE: JMX accessible without auth
return dataService.getAllData().toJson();
}
@ManagedOperation
public void updateConfiguration(String configJson) {
// VULNERABLE: Config update via JMX
configService.update(Config.fromJson(configJson));
}
}
// VULNERABLE: Actuator endpoints exposed
// application.properties:
// management.endpoints.web.exposure.include=*
// management.endpoint.env.show-values=always
// EXPOSES: /actuator/env, /actuator/configprops, etc.
// VULNERABLE: RMI interface without SSL/auth
public interface RemoteDataService extends Remote {
Data getAllData() throws RemoteException;
void updateConfig(Config config) throws RemoteException;
}
}
Fixed Code
// Fixed: Hardware with protected alternate register access
module secure_register_access (
input wire clk,
input wire rst_n,
input wire [31:0] address,
input wire [31:0] data_in,
input wire write_en,
input wire auth_valid,
output reg [31:0] data_out,
output reg access_denied
);
reg [31:0] SECURE_REG;
localparam SECURE_ADDR = 32'h0000_0F00;
localparam MIRROR_ADDR = 32'h0080_0F00;
// FIXED: Both primary and alternate addresses require authentication
wire addr_needs_auth = (address == SECURE_ADDR) ||
(address == MIRROR_ADDR);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
SECURE_REG <= 32'h0;
access_denied <= 1'b0;
end else begin
access_denied <= 1'b0;
// FIXED: Check auth for both addresses
if (address == SECURE_ADDR || address == MIRROR_ADDR) begin
if (auth_valid) begin
if (write_en) begin
SECURE_REG <= data_in;
end
data_out <= SECURE_REG;
end else begin
// FIXED: Deny access without auth
access_denied <= 1'b1;
data_out <= 32'h0; // Don't leak data
end
end
end
end
endmodule
// FIXED: Alternative - remove the mirror entirely
module secure_register_no_mirror (
input wire clk,
input wire rst_n,
input wire [31:0] address,
input wire [31:0] data_in,
input wire write_en,
input wire auth_valid,
output reg [31:0] data_out,
output reg access_denied
);
reg [31:0] SECURE_REG;
localparam SECURE_ADDR = 32'h0000_0F00;
// FIXED: No alternate address exists
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
SECURE_REG <= 32'h0;
access_denied <= 1'b0;
end else begin
access_denied <= 1'b0;
if (address == SECURE_ADDR) begin
if (auth_valid) begin
if (write_en) begin
SECURE_REG <= data_in;
end
data_out <= SECURE_REG;
end else begin
access_denied <= 1'b1;
data_out <= 32'h0;
end
end
// FIXED: All other addresses return nothing sensitive
end
end
endmodule
# Fixed: Application with consistent protection across all channels
from flask import Flask, request, jsonify
from functools import wraps
app = Flask(__name__)
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
if not verify_auth_token(request.headers.get('Authorization')):
return jsonify({'error': 'Unauthorized'}), 401
return f(*args, **kwargs)
return decorated
# FIXED: Apply auth to ALL API versions
@app.route('/api/v2/users', methods=['GET'])
@app.route('/api/v1/users', methods=['GET']) # FIXED: Same protection
@require_auth
def get_users():
return jsonify(get_all_users())
@app.route('/api/v2/config', methods=['GET', 'POST'])
@app.route('/api/v1/config', methods=['GET', 'POST']) # FIXED
@require_auth
def config():
if request.method == 'GET':
return jsonify(get_config())
else:
update_config(request.json)
return jsonify({'status': 'updated'})
# FIXED: Disable debug endpoints in production
if not app.debug:
@app.route('/debug/<path:path>')
def block_debug(path):
return jsonify({'error': 'Not found'}), 404
else:
# Even in debug, require auth
@app.route('/debug/dump', methods=['GET'])
@require_auth
def debug_dump():
return jsonify({
'users': get_all_users(),
'config': get_config()
})
# FIXED: Health check without sensitive data
@app.route('/health', methods=['GET'])
def health_check():
return jsonify({
'status': 'ok',
'timestamp': datetime.now().isoformat()
# FIXED: No sensitive data
})
# FIXED: Detailed health only for authenticated users
@app.route('/health/detailed', methods=['GET'])
@require_auth
def health_detailed():
return jsonify({
'status': 'ok',
'database': get_db_status(),
'users_count': len(get_all_users())
})
# FIXED: Deprecate old API versions
@app.route('/api/v0/<path:path>')
def deprecated_v0(path):
return jsonify({
'error': 'API v0 is deprecated. Use v2.'
}), 410 # Gone
// Fixed: Java service with protected alternate channels
@Configuration
public class SecurityConfig {
// FIXED: Secure JMX
@Bean
public JmxConfigurator jmxConfigurator() {
JmxConfigurator config = new JmxConfigurator();
config.setAuthenticate(true);
config.setPasswordFile("jmxremote.password");
config.setAccessFile("jmxremote.access");
config.setSslEnabled(true);
return config;
}
// FIXED: Restrict actuator endpoints
// application.properties:
// management.endpoints.web.exposure.include=health,info
// management.endpoint.health.show-details=when_authorized
// management.endpoints.web.base-path=/internal/actuator
}
@RestController
@RequestMapping("/api/secure")
public class SecureController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/data")
public ResponseEntity<Data> getData() {
return ResponseEntity.ok(dataService.getAllData());
}
}
// FIXED: JMX with authentication required
@ManagedResource
public class SecureDataManager {
// FIXED: JMX requires authentication via config
@ManagedOperation
@PreAuthorize("hasRole('ADMIN')") // Additional check
public String getAllDataAsJson(String authToken) {
if (!validateToken(authToken)) {
throw new SecurityException("Invalid token");
}
auditLog("JMX data access");
return dataService.getAllData().toJson();
}
}
// FIXED: Secure RMI configuration
@Configuration
public class RmiConfig {
@Bean
public RmiServiceExporter rmiExporter() {
RmiServiceExporter exporter = new RmiServiceExporter();
exporter.setServiceName("DataService");
exporter.setService(secureDataService);
exporter.setServiceInterface(RemoteDataService.class);
// FIXED: Use SSL
RMIClientSocketFactory csf = new SslRMIClientSocketFactory();
RMIServerSocketFactory ssf = new SslRMIServerSocketFactory();
exporter.setClientSocketFactory(csf);
exporter.setServerSocketFactory(ssf);
return exporter;
}
}
// FIXED: Audit logging for all channels
@Aspect
@Component
public class ChannelAuditAspect {
@Around("@annotation(PreAuthorize)")
public Object auditAccess(ProceedingJoinPoint pjp) throws Throwable {
String channel = determineChannel(); // REST, JMX, RMI, etc.
String user = getCurrentUser();
auditLog(String.format("Access via %s by %s: %s",
channel, user, pjp.getSignature()));
return pjp.proceed();
}
}
CVE Examples
- CVE-2020-3452: Cisco ASA unprotected WebVPN portal allowing file read.
- CVE-2019-1653: Cisco RV320 unprotected diagnostic interface.
- CVE-2018-0101: Cisco ASA SSL VPN alternate path vulnerability.
Related CWEs
- CWE-923: Improper Restriction of Communication Channel to Intended Endpoints (parent)
- CWE-421: Race Condition During Access to Alternate Channel (child)
- CWE-288: Authentication Bypass Using Alternate Path or Channel (peer)
References
- MITRE Corporation. "CWE-420: Unprotected Alternate Channel." https://cwe.mitre.org/data/definitions/420.html
- OWASP. "Testing for Alternate Channel"
- NIST. "Security Assessment Guidelines"